github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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  }