github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/state/warning.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 state 21 22 import ( 23 "encoding/json" 24 "errors" 25 "fmt" 26 "sort" 27 "strings" 28 "time" 29 30 "github.com/snapcore/snapd/logger" 31 ) 32 33 var ( 34 DefaultRepeatAfter = time.Hour * 24 35 DefaultExpireAfter = time.Hour * 24 * 28 36 37 errNoWarningMessage = errors.New("warning has no message") 38 errBadWarningMessage = errors.New("malformed warning message") 39 errNoWarningFirstAdded = errors.New("warning has no first-added timestamp") 40 errNoWarningExpireAfter = errors.New("warning has no expire-after duration") 41 errNoWarningRepeatAfter = errors.New("warning has no repeat-after duration") 42 ) 43 44 type jsonWarning struct { 45 Message string `json:"message"` 46 FirstAdded time.Time `json:"first-added"` 47 LastAdded time.Time `json:"last-added"` 48 LastShown *time.Time `json:"last-shown,omitempty"` 49 ExpireAfter string `json:"expire-after,omitempty"` 50 RepeatAfter string `json:"repeat-after,omitempty"` 51 } 52 53 type Warning struct { 54 // the warning text itself. Only one of these in the system at a time. 55 message string 56 // the first time one of these messages was created 57 firstAdded time.Time 58 // the last time one of these was created 59 lastAdded time.Time 60 // the last time one of these was shown to the user 61 lastShown time.Time 62 // how much time since one of these was last added should we drop the message 63 expireAfter time.Duration 64 // how much time since one of these was last shown should we repeat it 65 repeatAfter time.Duration 66 } 67 68 func (w *Warning) String() string { 69 return w.message 70 } 71 72 func (w *Warning) MarshalJSON() ([]byte, error) { 73 jw := jsonWarning{ 74 Message: w.message, 75 FirstAdded: w.firstAdded, 76 LastAdded: w.lastAdded, 77 ExpireAfter: w.expireAfter.String(), 78 RepeatAfter: w.repeatAfter.String(), 79 } 80 if !w.lastShown.IsZero() { 81 jw.LastShown = &w.lastShown 82 } 83 84 return json.Marshal(jw) 85 } 86 87 func (w *Warning) UnmarshalJSON(data []byte) error { 88 var jw jsonWarning 89 err := json.Unmarshal(data, &jw) 90 if err != nil { 91 return err 92 } 93 w.message = jw.Message 94 w.firstAdded = jw.FirstAdded 95 w.lastAdded = jw.LastAdded 96 if jw.LastShown != nil { 97 w.lastShown = *jw.LastShown 98 } 99 if jw.ExpireAfter != "" { 100 w.expireAfter, err = time.ParseDuration(jw.ExpireAfter) 101 if err != nil { 102 return err 103 } 104 } 105 if jw.RepeatAfter != "" { 106 w.repeatAfter, err = time.ParseDuration(jw.RepeatAfter) 107 if err != nil { 108 return err 109 } 110 } 111 112 return w.validate() 113 } 114 115 func (w *Warning) validate() (e error) { 116 if w.message == "" { 117 return errNoWarningMessage 118 } 119 if strings.TrimSpace(w.message) != w.message { 120 return errBadWarningMessage 121 } 122 if w.firstAdded.IsZero() { 123 return errNoWarningFirstAdded 124 } 125 if w.expireAfter == 0 { 126 return errNoWarningExpireAfter 127 } 128 if w.repeatAfter == 0 { 129 return errNoWarningRepeatAfter 130 } 131 return nil 132 } 133 134 func (w *Warning) ExpiredBefore(now time.Time) bool { 135 return w.lastAdded.Add(w.expireAfter).Before(now) 136 } 137 138 func (w *Warning) ShowAfter(t time.Time) bool { 139 if w.lastShown.IsZero() { 140 // warning was never shown before; was it added after the cutoff? 141 return !w.firstAdded.After(t) 142 } 143 144 return w.lastShown.Add(w.repeatAfter).Before(t) 145 } 146 147 // flattenWarning loops over the warnings map, and returns all 148 // non-expired warnings therein as a flat list, for serialising. 149 // Call with the lock held. 150 func (s *State) flattenWarnings() []*Warning { 151 now := time.Now() 152 flat := make([]*Warning, 0, len(s.warnings)) 153 for _, w := range s.warnings { 154 if w.ExpiredBefore(now) { 155 continue 156 } 157 flat = append(flat, w) 158 } 159 return flat 160 } 161 162 // unflattenWarnings takes a flat list of warnings and replaces the 163 // warning map with them, ignoring expired warnings in the process. 164 // Call with the lock held. 165 func (s *State) unflattenWarnings(flat []*Warning) { 166 now := time.Now() 167 s.warnings = make(map[string]*Warning, len(flat)) 168 for _, w := range flat { 169 if w.ExpiredBefore(now) { 170 continue 171 } 172 s.warnings[w.message] = w 173 } 174 } 175 176 // Warnf records a warning: if it's the first Warning with this 177 // message it'll be added (with its firstAdded and lastAdded set to the 178 // current time), otherwise the existing one will have its lastAdded 179 // updated. 180 func (s *State) Warnf(template string, args ...interface{}) { 181 var message string 182 if len(args) > 0 { 183 message = fmt.Sprintf(template, args...) 184 } else { 185 message = template 186 } 187 s.addWarning(Warning{ 188 message: message, 189 expireAfter: DefaultExpireAfter, 190 repeatAfter: DefaultRepeatAfter, 191 }, time.Now().UTC()) 192 } 193 194 func (s *State) addWarning(w Warning, t time.Time) { 195 s.writing() 196 197 if s.warnings[w.message] == nil { 198 w.firstAdded = t 199 if err := w.validate(); err != nil { 200 // programming error! 201 logger.Panicf("internal error, please report: attempted to add invalid warning: %v", err) 202 return 203 } 204 s.warnings[w.message] = &w 205 } 206 s.warnings[w.message].lastAdded = t 207 } 208 209 type byLastAdded []*Warning 210 211 func (a byLastAdded) Len() int { return len(a) } 212 func (a byLastAdded) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 213 func (a byLastAdded) Less(i, j int) bool { return a[i].lastAdded.Before(a[j].lastAdded) } 214 215 // AllWarnings returns all the warnings in the system, whether they're 216 // due to be shown or not. They'll be sorted by lastAdded. 217 func (s *State) AllWarnings() []*Warning { 218 s.reading() 219 220 all := s.flattenWarnings() 221 sort.Sort(byLastAdded(all)) 222 223 return all 224 } 225 226 // OkayWarnings marks warnings that were showable at the given time as shown. 227 func (s *State) OkayWarnings(t time.Time) int { 228 t = t.UTC() 229 s.writing() 230 231 n := 0 232 for _, w := range s.warnings { 233 if w.ShowAfter(t) { 234 w.lastShown = t 235 n++ 236 } 237 } 238 239 return n 240 } 241 242 // PendingWarnings returns the list of warnings to show the user, sorted by 243 // lastAdded, and a timestamp than can be used to refer to these warnings. 244 // 245 // Warnings to show to the user are those that have not been shown before, 246 // or that have been shown earlier than repeatAfter ago. 247 func (s *State) PendingWarnings() ([]*Warning, time.Time) { 248 s.reading() 249 now := time.Now().UTC() 250 251 var toShow []*Warning 252 for _, w := range s.warnings { 253 if !w.ShowAfter(now) { 254 continue 255 } 256 toShow = append(toShow, w) 257 } 258 259 sort.Sort(byLastAdded(toShow)) 260 return toShow, now 261 } 262 263 // WarningsSummary returns the number of warnings that are ready to be 264 // shown to the user, and the timestamp of the most recently added 265 // warning (useful for silencing the warning alerts, and OKing the 266 // returned warnings). 267 func (s *State) WarningsSummary() (int, time.Time) { 268 s.reading() 269 now := time.Now().UTC() 270 var last time.Time 271 272 var n int 273 for _, w := range s.warnings { 274 if w.ShowAfter(now) { 275 n++ 276 if w.lastAdded.After(last) { 277 last = w.lastAdded 278 } 279 } 280 } 281 282 return n, last 283 } 284 285 // UnshowAllWarnings clears the lastShown timestamp from all the 286 // warnings. For use in debugging. 287 func (s *State) UnshowAllWarnings() { 288 s.writing() 289 for _, w := range s.warnings { 290 w.lastShown = time.Time{} 291 } 292 }