github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/cmd/snap/error.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017-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  	"bytes"
    24  	"errors"
    25  	"fmt"
    26  	"go/doc"
    27  	"os"
    28  	"os/user"
    29  	"strings"
    30  	"text/tabwriter"
    31  
    32  	"golang.org/x/crypto/ssh/terminal"
    33  
    34  	"github.com/snapcore/snapd/client"
    35  	"github.com/snapcore/snapd/i18n"
    36  	"github.com/snapcore/snapd/logger"
    37  	"github.com/snapcore/snapd/osutil"
    38  	"github.com/snapcore/snapd/snap/channel"
    39  	"github.com/snapcore/snapd/strutil"
    40  )
    41  
    42  var errorPrefix = i18n.G("error: %v\n")
    43  
    44  var termSize = termSizeImpl
    45  
    46  func termSizeImpl() (width, height int) {
    47  	if f, ok := Stdout.(*os.File); ok {
    48  		width, height, _ = terminal.GetSize(int(f.Fd()))
    49  	}
    50  
    51  	if width <= 0 {
    52  		width = int(osutil.GetenvInt64("COLUMNS"))
    53  	}
    54  
    55  	if height <= 0 {
    56  		height = int(osutil.GetenvInt64("LINES"))
    57  	}
    58  
    59  	if width < 40 {
    60  		width = 80
    61  	}
    62  
    63  	if height < 15 {
    64  		height = 25
    65  	}
    66  
    67  	return width, height
    68  }
    69  
    70  func fill(para string, indent int) string {
    71  	width, _ := termSize()
    72  
    73  	if width > 100 {
    74  		width = 100
    75  	}
    76  
    77  	// some terminals aren't happy about writing in the last
    78  	// column (they'll add line for you). We could check terminfo
    79  	// for "sam" (semi_auto_right_margin), but that's a lot of
    80  	// work just for this.
    81  	width--
    82  
    83  	var buf bytes.Buffer
    84  	indentStr := strings.Repeat(" ", indent)
    85  	doc.ToText(&buf, para, indentStr, indentStr, width-indent)
    86  
    87  	return strings.TrimSpace(buf.String())
    88  }
    89  
    90  func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (string, error) {
    91  	// do this here instead of in the caller for more DRY
    92  	err, ok := e.(*client.Error)
    93  	if !ok {
    94  		return "", e
    95  	}
    96  	// retryable errors are just passed through
    97  	if client.IsRetryable(err) {
    98  		return "", err
    99  	}
   100  
   101  	// ensure the "real" error is available if we ask for it
   102  	logger.Debugf("error: %s", err)
   103  
   104  	// FIXME: using err.Message in user-facing messaging is not
   105  	// l10n-friendly, and probably means we're missing ad-hoc messaging.
   106  	isError := true
   107  	usesSnapName := true
   108  	var msg string
   109  	switch err.Kind {
   110  	case client.ErrorKindNotSnap:
   111  		msg = i18n.G(`%q does not contain an unpacked snap.
   112  
   113  Try 'snapcraft prime' in your project directory, then 'snap try' again.`)
   114  		if snapName == "" || snapName == "./" {
   115  			errValStr, ok := err.Value.(string)
   116  			if ok && errValStr != "" {
   117  				snapName = errValStr
   118  			}
   119  		}
   120  	case client.ErrorKindSnapNotFound:
   121  		msg = i18n.G("snap %q not found")
   122  		if snapName == "" {
   123  			errValStr, ok := err.Value.(string)
   124  			if ok && errValStr != "" {
   125  				snapName = errValStr
   126  			}
   127  		}
   128  	case client.ErrorKindSnapChannelNotAvailable,
   129  		client.ErrorKindSnapArchitectureNotAvailable:
   130  		values, ok := err.Value.(map[string]interface{})
   131  		if ok {
   132  			candName, _ := values["snap-name"].(string)
   133  			if candName != "" {
   134  				snapName = candName
   135  			}
   136  			action, _ := values["action"].(string)
   137  			arch, _ := values["architecture"].(string)
   138  			channel, _ := values["channel"].(string)
   139  			releases, _ := values["releases"].([]interface{})
   140  			if snapName != "" && action != "" && arch != "" && channel != "" && len(releases) != 0 {
   141  				usesSnapName = false
   142  				msg = snapRevisionNotAvailableMessage(err.Kind, snapName, action, arch, channel, releases)
   143  				break
   144  			}
   145  		}
   146  		fallthrough
   147  	case client.ErrorKindSnapRevisionNotAvailable:
   148  		if snapName == "" {
   149  			errValStr, ok := err.Value.(string)
   150  			if ok && errValStr != "" {
   151  				snapName = errValStr
   152  			}
   153  		}
   154  
   155  		usesSnapName = false
   156  		// TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name).
   157  		msg = fmt.Sprintf(i18n.G(`snap %[1]q not available as specified (see 'snap info %[1]s')`), snapName)
   158  
   159  		if opts != nil {
   160  			if opts.Revision != "" {
   161  				// TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %s is whatever the user used for --revision=
   162  				msg = fmt.Sprintf(i18n.G(`snap %[1]q revision %s not available (see 'snap info %[1]s')`), snapName, opts.Revision)
   163  			} else if opts.Channel != "" {
   164  				// (note --revision overrides --channel)
   165  
   166  				// TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %q is whatever foo the user used for --channel=foo
   167  				msg = fmt.Sprintf(i18n.G(`snap %[1]q not available on channel %q (see 'snap info %[1]s')`), snapName, opts.Channel)
   168  			}
   169  		}
   170  	case client.ErrorKindSnapAlreadyInstalled:
   171  		isError = false
   172  		msg = i18n.G(`snap %q is already installed, see 'snap help refresh'`)
   173  	case client.ErrorKindSnapNeedsDevMode:
   174  		if opts != nil && opts.Dangerous {
   175  			msg = i18n.G("snap %q requires devmode or confinement override")
   176  			break
   177  		}
   178  		msg = i18n.G(`
   179  The publisher of snap %q has indicated that they do not consider this revision
   180  to be of production quality and that it is only meant for development or testing
   181  at this point. As a consequence this snap will not refresh automatically and may
   182  perform arbitrary system changes outside of the security sandbox snaps are
   183  generally confined to, which may put your system at risk.
   184  
   185  If you understand and want to proceed repeat the command including --devmode;
   186  if instead you want to install the snap forcing it into strict confinement
   187  repeat the command including --jailmode.`)
   188  	case client.ErrorKindSnapNeedsClassic:
   189  		msg = i18n.G(`
   190  This revision of snap %q was published using classic confinement and thus may
   191  perform arbitrary system changes outside of the security sandbox that snaps are
   192  usually confined to, which may put your system at risk.
   193  
   194  If you understand and want to proceed repeat the command including --classic.
   195  `)
   196  	case client.ErrorKindSnapNotClassic:
   197  		msg = i18n.G(`snap %q is not compatible with --classic`)
   198  	case client.ErrorKindLoginRequired:
   199  		usesSnapName = false
   200  		u, _ := user.Current()
   201  		if u != nil && u.Username == "root" {
   202  			// TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”)
   203  			msg = fmt.Sprintf(i18n.G(`%s (see 'snap help login')`), err.Message)
   204  		} else {
   205  			// TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”)
   206  			msg = fmt.Sprintf(i18n.G(`%s (try with sudo)`), err.Message)
   207  		}
   208  	case client.ErrorKindSnapLocal:
   209  		msg = i18n.G("local snap %q is unknown to the store, use --amend to proceed anyway")
   210  	case client.ErrorKindSnapNoUpdateAvailable:
   211  		isError = false
   212  		msg = i18n.G("snap %q has no updates available")
   213  	case client.ErrorKindSnapNotInstalled:
   214  		isError = false
   215  		usesSnapName = false
   216  		msg = err.Message
   217  	case client.ErrorKindNetworkTimeout:
   218  		isError = true
   219  		usesSnapName = false
   220  		msg = i18n.G("unable to contact snap store")
   221  	case client.ErrorKindSystemRestart:
   222  		isError = false
   223  		usesSnapName = false
   224  		msg = i18n.G("snapd is about to reboot the system")
   225  		values, ok := err.Value.(map[string]interface{})
   226  		if ok {
   227  			op, ok := values["op"].(string)
   228  			if ok {
   229  				switch op {
   230  				case "halt":
   231  					msg = i18n.G("snapd is about to halt the system")
   232  				case "poweroff":
   233  					msg = i18n.G("snapd is about to power off the system")
   234  				}
   235  			}
   236  		}
   237  	case client.ErrorKindInsufficientDiskSpace:
   238  		// this error carries multiple snap names
   239  		usesSnapName = false
   240  		values, ok := err.Value.(map[string]interface{})
   241  		if ok {
   242  			changeKind, _ := values["change-kind"].(string)
   243  			snaps, _ := values["snap-names"].([]interface{})
   244  			snapNames := make([]string, len(snaps))
   245  			for i, v := range snaps {
   246  				snapNames[i] = fmt.Sprint(v)
   247  			}
   248  			names := strutil.Quoted(snapNames)
   249  			switch changeKind {
   250  			case "remove":
   251  				msg = fmt.Sprintf(i18n.G("cannot remove %s due to low disk space for automatic snapshot, use --purge to avoid creating a snapshot"), names)
   252  			case "install":
   253  				msg = fmt.Sprintf(i18n.G("cannot install %s due to low disk space"), names)
   254  			case "refresh":
   255  				msg = fmt.Sprintf(i18n.G("cannot refresh %s due to low disk space"), names)
   256  			default:
   257  				msg = err.Error()
   258  			}
   259  			break
   260  		}
   261  		fallthrough
   262  	default:
   263  		usesSnapName = false
   264  		msg = err.Message
   265  	}
   266  
   267  	if usesSnapName {
   268  		msg = fmt.Sprintf(msg, snapName)
   269  	}
   270  	// 3 is the %v\n, which will be present in any locale
   271  	msg = fill(msg, len(errorPrefix)-3)
   272  	if isError {
   273  		return "", errors.New(msg)
   274  	}
   275  
   276  	return msg, nil
   277  }
   278  
   279  func snapRevisionNotAvailableMessage(kind client.ErrorKind, snapName, action, arch, snapChannel string, releases []interface{}) string {
   280  	// releases contains all available (arch x channel)
   281  	// as reported by the store through the daemon
   282  	req, err := channel.Parse(snapChannel, arch)
   283  	if err != nil {
   284  		// XXX: this is no longer possible (should be caught before hitting the store), unless the state itself has an invalid channel
   285  		// TRANSLATORS: %q is the invalid request channel, %s is the snap name
   286  		msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), snapChannel, snapName)
   287  		return msg
   288  	}
   289  	avail := make([]*channel.Channel, 0, len(releases))
   290  	for _, v := range releases {
   291  		rel, _ := v.(map[string]interface{})
   292  		relCh, _ := rel["channel"].(string)
   293  		relArch, _ := rel["architecture"].(string)
   294  		if relArch == "" {
   295  			logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v)
   296  			continue
   297  		}
   298  		a, err := channel.Parse(relCh, relArch)
   299  		if err != nil {
   300  			logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v)
   301  			continue
   302  		}
   303  		avail = append(avail, &a)
   304  	}
   305  
   306  	matches := map[string][]*channel.Channel{}
   307  	for _, a := range avail {
   308  		m := req.Match(a)
   309  		matchRepr := m.String()
   310  		if matchRepr != "" {
   311  			matches[matchRepr] = append(matches[matchRepr], a)
   312  		}
   313  	}
   314  
   315  	// no release is for this architecture
   316  	if kind == client.ErrorKindSnapArchitectureNotAvailable {
   317  		// TODO: add "Get more information..." hints once snap info
   318  		// support showing multiple/all archs
   319  
   320  		// there are matching track+risk releases for other archs
   321  		if hits := matches["track:risk"]; len(hits) != 0 {
   322  			archs := strings.Join(archsForChannels(hits), ", ")
   323  			// TRANSLATORS: %q is for the snap name, %v is the requested channel, first %s is the system architecture short name, second %s is a comma separated list of available arch short names
   324  			msg := fmt.Sprintf(i18n.G("snap %q is not available on %v for this architecture (%s) but exists on other architectures (%s)."), snapName, req, arch, archs)
   325  			return msg
   326  		}
   327  
   328  		// not even that, generic error
   329  		archs := strings.Join(archsForChannels(avail), ", ")
   330  		// TRANSLATORS: %q is for the snap name, first %s is the system architecture short name, second %s is a comma separated list of available arch short names
   331  		msg := fmt.Sprintf(i18n.G("snap %q is not available on this architecture (%s) but exists on other architectures (%s)."), snapName, arch, archs)
   332  		return msg
   333  	}
   334  
   335  	// a branch was requested
   336  	if req.Branch != "" {
   337  		// there are matching arch+track+risk, give main track info
   338  		if len(matches["architecture:track:risk"]) != 0 {
   339  			trackRisk := channel.Channel{Track: req.Track, Risk: req.Risk}
   340  			trackRisk = trackRisk.Clean()
   341  
   342  			// TRANSLATORS: %q is for the snap name, first %s is the full requested channel
   343  			msg := fmt.Sprintf(i18n.G("requested a non-existing branch on %s for snap %q: %s"), trackRisk.Full(), snapName, req.Branch)
   344  			return msg
   345  		}
   346  
   347  		msg := fmt.Sprintf(i18n.G("requested a non-existing branch for snap %q: %s"), snapName, req.Full())
   348  		return msg
   349  	}
   350  
   351  	// TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint
   352  	preRelWarn := i18n.G("Please be mindful pre-release channels may include features not completely tested or implemented.")
   353  	// TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint
   354  	trackWarn := i18n.G("Please be mindful that different tracks may include different features.")
   355  	// TRANSLATORS: %s is for the snap name, will be concatenated after at the end of other error messages, possibly after a blank line
   356  	moreInfoHint := fmt.Sprintf(i18n.G("Get more information with 'snap info %s'."), snapName)
   357  
   358  	// there are matching arch+track releases => give hint and instructions
   359  	// about pre-release channels
   360  	if hits := matches["architecture:track"]; len(hits) != 0 {
   361  		// TRANSLATORS: %q is for the snap name, %v is the requested channel
   362  		msg := fmt.Sprintf(i18n.G("snap %q is not available on %v but is available to install on the following channels:\n"), snapName, req)
   363  		msg += installTable(snapName, action, hits, false)
   364  		msg += "\n"
   365  		if req.Risk == "stable" {
   366  			msg += "\n" + preRelWarn
   367  		}
   368  		msg += "\n" + moreInfoHint
   369  		return msg
   370  	}
   371  
   372  	// there are matching arch+risk releases => give hints and instructions
   373  	// about these other tracks
   374  	if hits := matches["architecture:risk"]; len(hits) != 0 {
   375  		// TRANSLATORS: %q is for the snap name, %s is the full requested channel
   376  		msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but is available to install on the following tracks:\n"), snapName, req.Full())
   377  		msg += installTable(snapName, action, hits, true)
   378  		msg += "\n\n" + trackWarn
   379  		msg += "\n" + moreInfoHint
   380  		return msg
   381  	}
   382  
   383  	// generic error
   384  	// TRANSLATORS: %q is for the snap name, %s is the full requested channel
   385  	msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but other tracks exist.\n"), snapName, req.Full())
   386  	msg += "\n\n" + trackWarn
   387  	msg += "\n" + moreInfoHint
   388  	return msg
   389  }
   390  
   391  func installTable(snapName, action string, avail []*channel.Channel, full bool) string {
   392  	b := &bytes.Buffer{}
   393  	w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0)
   394  	first := true
   395  	for _, a := range avail {
   396  		if first {
   397  			first = false
   398  		} else {
   399  			fmt.Fprint(w, "\n")
   400  		}
   401  		var ch string
   402  		if full {
   403  			ch = a.Full()
   404  		} else {
   405  			ch = a.String()
   406  		}
   407  		chOption := channelOption(a)
   408  		fmt.Fprintf(w, "%s\tsnap %s %s %s", ch, action, chOption, snapName)
   409  	}
   410  	w.Flush()
   411  	tbl := b.String()
   412  	// indent to drive fill/ToText to keep the tabulations intact
   413  	lines := strings.SplitAfter(tbl, "\n")
   414  	for i := range lines {
   415  		lines[i] = "  " + lines[i]
   416  	}
   417  	return strings.Join(lines, "")
   418  }
   419  
   420  func channelOption(c *channel.Channel) string {
   421  	if c.Branch == "" {
   422  		if c.Track == "" {
   423  			return fmt.Sprintf("--%s", c.Risk)
   424  		}
   425  		if c.Risk == "stable" {
   426  			return fmt.Sprintf("--channel=%s", c.Track)
   427  		}
   428  	}
   429  	return fmt.Sprintf("--channel=%s", c)
   430  }
   431  
   432  func archsForChannels(cs []*channel.Channel) []string {
   433  	archs := []string{}
   434  	for _, c := range cs {
   435  		if !strutil.ListContains(archs, c.Architecture) {
   436  			archs = append(archs, c.Architecture)
   437  		}
   438  	}
   439  	return archs
   440  }