github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/cmd/snap/cmd_warnings.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "os" 26 "path/filepath" 27 "time" 28 29 "github.com/jessevdk/go-flags" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/i18n" 34 "github.com/snapcore/snapd/osutil" 35 "github.com/snapcore/snapd/strutil/quantity" 36 ) 37 38 type cmdWarnings struct { 39 clientMixin 40 timeMixin 41 unicodeMixin 42 All bool `long:"all"` 43 Verbose bool `long:"verbose"` 44 } 45 46 type cmdOkay struct{ clientMixin } 47 48 var shortWarningsHelp = i18n.G("List warnings") 49 var longWarningsHelp = i18n.G(` 50 The warnings command lists the warnings that have been reported to the system. 51 52 Once warnings have been listed with 'snap warnings', 'snap okay' may be used to 53 silence them. A warning that's been silenced in this way will not be listed 54 again unless it happens again, _and_ a cooldown time has passed. 55 56 Warnings expire automatically, and once expired they are forgotten. 57 `) 58 59 var shortOkayHelp = i18n.G("Acknowledge warnings") 60 var longOkayHelp = i18n.G(` 61 The okay command acknowledges the warnings listed with 'snap warnings'. 62 63 Once acknowledged a warning won't appear again unless it re-occurrs and 64 sufficient time has passed. 65 `) 66 67 func init() { 68 addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(unicodeDescs).also(map[string]string{ 69 // TRANSLATORS: This should not start with a lowercase letter. 70 "all": i18n.G("Show all warnings"), 71 // TRANSLATORS: This should not start with a lowercase letter. 72 "verbose": i18n.G("Show more information"), 73 }), nil) 74 addCommand("okay", shortOkayHelp, longOkayHelp, func() flags.Commander { return &cmdOkay{} }, nil, nil) 75 } 76 77 func (cmd *cmdWarnings) Execute(args []string) error { 78 if len(args) > 0 { 79 return ErrExtraArgs 80 } 81 now := time.Now() 82 83 warnings, err := cmd.client.Warnings(client.WarningsOptions{All: cmd.All}) 84 if err != nil { 85 return err 86 } 87 if len(warnings) == 0 { 88 if t, _ := lastWarningTimestamp(); t.IsZero() { 89 fmt.Fprintln(Stdout, i18n.G("No warnings.")) 90 } else { 91 fmt.Fprintln(Stdout, i18n.G("No further warnings.")) 92 } 93 return nil 94 } 95 96 if err := writeWarningTimestamp(now); err != nil { 97 return err 98 } 99 100 termWidth, _ := termSize() 101 if termWidth > 100 { 102 // any wider than this and it gets hard to read 103 termWidth = 100 104 } 105 106 esc := cmd.getEscapes() 107 w := tabWriter() 108 for i, warning := range warnings { 109 if i > 0 { 110 fmt.Fprintln(w, "---") 111 } 112 if cmd.Verbose { 113 fmt.Fprintf(w, "first-occurrence:\t%s\n", cmd.fmtTime(warning.FirstAdded)) 114 } 115 fmt.Fprintf(w, "last-occurrence:\t%s\n", cmd.fmtTime(warning.LastAdded)) 116 if cmd.Verbose { 117 lastShown := esc.dash 118 if !warning.LastShown.IsZero() { 119 lastShown = cmd.fmtTime(warning.LastShown) 120 } 121 fmt.Fprintf(w, "acknowledged:\t%s\n", lastShown) 122 // TODO: cmd.fmtDuration() using timeutil.HumanDuration 123 fmt.Fprintf(w, "repeats-after:\t%s\n", quantity.FormatDuration(warning.RepeatAfter.Seconds())) 124 fmt.Fprintf(w, "expires-after:\t%s\n", quantity.FormatDuration(warning.ExpireAfter.Seconds())) 125 } 126 fmt.Fprintln(w, "warning: |") 127 printDescr(w, warning.Message, termWidth) 128 w.Flush() 129 } 130 131 return nil 132 } 133 134 func (cmd *cmdOkay) Execute(args []string) error { 135 if len(args) > 0 { 136 return ErrExtraArgs 137 } 138 139 last, err := lastWarningTimestamp() 140 if err != nil { 141 return err 142 } 143 144 return cmd.client.Okay(last) 145 } 146 147 const warnFileEnvKey = "SNAPD_LAST_WARNING_TIMESTAMP_FILENAME" 148 149 func warnFilename(homedir string) string { 150 if fn := os.Getenv(warnFileEnvKey); fn != "" { 151 return fn 152 } 153 154 return filepath.Join(dirs.GlobalRootDir, homedir, ".snap", "warnings.json") 155 } 156 157 type clientWarningData struct { 158 Timestamp time.Time `json:"timestamp"` 159 } 160 161 func writeWarningTimestamp(t time.Time) error { 162 user, err := osutil.UserMaybeSudoUser() 163 if err != nil { 164 return err 165 } 166 uid, gid, err := osutil.UidGid(user) 167 if err != nil { 168 return err 169 } 170 171 filename := warnFilename(user.HomeDir) 172 if err := osutil.MkdirAllChown(filepath.Dir(filename), 0700, uid, gid); err != nil { 173 return err 174 } 175 176 aw, err := osutil.NewAtomicFile(filename, 0600, 0, uid, gid) 177 if err != nil { 178 return err 179 } 180 // Cancel once Committed is a NOP :-) 181 defer aw.Cancel() 182 183 enc := json.NewEncoder(aw) 184 if err := enc.Encode(clientWarningData{Timestamp: t}); err != nil { 185 return err 186 } 187 188 return aw.Commit() 189 } 190 191 func lastWarningTimestamp() (time.Time, error) { 192 user, err := osutil.UserMaybeSudoUser() 193 if err != nil { 194 return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) 195 } 196 197 f, err := os.Open(warnFilename(user.HomeDir)) 198 if err != nil { 199 if os.IsNotExist(err) { 200 return time.Time{}, fmt.Errorf("you must have looked at the warnings before acknowledging them. Try 'snap warnings'.") 201 } 202 return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err) 203 204 } 205 dec := json.NewDecoder(f) 206 var d clientWarningData 207 if err := dec.Decode(&d); err != nil { 208 return time.Time{}, fmt.Errorf("cannot decode timestamp file: %v", err) 209 } 210 if dec.More() { 211 return time.Time{}, fmt.Errorf("spurious extra data in timestamp file") 212 } 213 return d.Timestamp, nil 214 } 215 216 func maybePresentWarnings(count int, timestamp time.Time) { 217 if count == 0 { 218 return 219 } 220 221 if last, _ := lastWarningTimestamp(); !timestamp.After(last) { 222 return 223 } 224 225 fmt.Fprintf(Stderr, 226 i18n.NG("WARNING: There is %d new warning. See 'snap warnings'.\n", 227 "WARNING: There are %d new warnings. See 'snap warnings'.\n", 228 count), 229 count) 230 }