github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/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  }