github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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  	case client.ErrorKindInsufficientDiskSpace:
   226  		// this error carries multiple snap names
   227  		usesSnapName = false
   228  		values, ok := err.Value.(map[string]interface{})
   229  		if ok {
   230  			changeKind, _ := values["change-kind"].(string)
   231  			snaps, _ := values["snap-names"].([]interface{})
   232  			snapNames := make([]string, len(snaps))
   233  			for i, v := range snaps {
   234  				snapNames[i] = fmt.Sprint(v)
   235  			}
   236  			names := strutil.Quoted(snapNames)
   237  			switch changeKind {
   238  			case "remove":
   239  				msg = fmt.Sprintf(i18n.G("cannot remove %s due to low disk space for automatic snapshot, use --purge to avoid creating a snapshot"), names)
   240  			case "install":
   241  				msg = fmt.Sprintf(i18n.G("cannot install %s due to low disk space"), names)
   242  			case "refresh":
   243  				msg = fmt.Sprintf(i18n.G("cannot refresh %s due to low disk space"), names)
   244  			default:
   245  				msg = err.Error()
   246  			}
   247  			break
   248  		}
   249  		fallthrough
   250  	default:
   251  		usesSnapName = false
   252  		msg = err.Message
   253  	}
   254  
   255  	if usesSnapName {
   256  		msg = fmt.Sprintf(msg, snapName)
   257  	}
   258  	// 3 is the %v\n, which will be present in any locale
   259  	msg = fill(msg, len(errorPrefix)-3)
   260  	if isError {
   261  		return "", errors.New(msg)
   262  	}
   263  
   264  	return msg, nil
   265  }
   266  
   267  func snapRevisionNotAvailableMessage(kind client.ErrorKind, snapName, action, arch, snapChannel string, releases []interface{}) string {
   268  	// releases contains all available (arch x channel)
   269  	// as reported by the store through the daemon
   270  	req, err := channel.Parse(snapChannel, arch)
   271  	if err != nil {
   272  		// XXX: this is no longer possible (should be caught before hitting the store), unless the state itself has an invalid channel
   273  		// TRANSLATORS: %q is the invalid request channel, %s is the snap name
   274  		msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), snapChannel, snapName)
   275  		return msg
   276  	}
   277  	avail := make([]*channel.Channel, 0, len(releases))
   278  	for _, v := range releases {
   279  		rel, _ := v.(map[string]interface{})
   280  		relCh, _ := rel["channel"].(string)
   281  		relArch, _ := rel["architecture"].(string)
   282  		if relArch == "" {
   283  			logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v)
   284  			continue
   285  		}
   286  		a, err := channel.Parse(relCh, relArch)
   287  		if err != nil {
   288  			logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v)
   289  			continue
   290  		}
   291  		avail = append(avail, &a)
   292  	}
   293  
   294  	matches := map[string][]*channel.Channel{}
   295  	for _, a := range avail {
   296  		m := req.Match(a)
   297  		matchRepr := m.String()
   298  		if matchRepr != "" {
   299  			matches[matchRepr] = append(matches[matchRepr], a)
   300  		}
   301  	}
   302  
   303  	// no release is for this architecture
   304  	if kind == client.ErrorKindSnapArchitectureNotAvailable {
   305  		// TODO: add "Get more information..." hints once snap info
   306  		// support showing multiple/all archs
   307  
   308  		// there are matching track+risk releases for other archs
   309  		if hits := matches["track:risk"]; len(hits) != 0 {
   310  			archs := strings.Join(archsForChannels(hits), ", ")
   311  			// 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
   312  			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)
   313  			return msg
   314  		}
   315  
   316  		// not even that, generic error
   317  		archs := strings.Join(archsForChannels(avail), ", ")
   318  		// 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
   319  		msg := fmt.Sprintf(i18n.G("snap %q is not available on this architecture (%s) but exists on other architectures (%s)."), snapName, arch, archs)
   320  		return msg
   321  	}
   322  
   323  	// a branch was requested
   324  	if req.Branch != "" {
   325  		// there are matching arch+track+risk, give main track info
   326  		if len(matches["architecture:track:risk"]) != 0 {
   327  			trackRisk := channel.Channel{Track: req.Track, Risk: req.Risk}
   328  			trackRisk = trackRisk.Clean()
   329  
   330  			// TRANSLATORS: %q is for the snap name, first %s is the full requested channel
   331  			msg := fmt.Sprintf(i18n.G("requested a non-existing branch on %s for snap %q: %s"), trackRisk.Full(), snapName, req.Branch)
   332  			return msg
   333  		}
   334  
   335  		msg := fmt.Sprintf(i18n.G("requested a non-existing branch for snap %q: %s"), snapName, req.Full())
   336  		return msg
   337  	}
   338  
   339  	// TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint
   340  	preRelWarn := i18n.G("Please be mindful pre-release channels may include features not completely tested or implemented.")
   341  	// TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint
   342  	trackWarn := i18n.G("Please be mindful that different tracks may include different features.")
   343  	// TRANSLATORS: %s is for the snap name, will be concatenated after at the end of other error messages, possibly after a blank line
   344  	moreInfoHint := fmt.Sprintf(i18n.G("Get more information with 'snap info %s'."), snapName)
   345  
   346  	// there are matching arch+track releases => give hint and instructions
   347  	// about pre-release channels
   348  	if hits := matches["architecture:track"]; len(hits) != 0 {
   349  		// TRANSLATORS: %q is for the snap name, %v is the requested channel
   350  		msg := fmt.Sprintf(i18n.G("snap %q is not available on %v but is available to install on the following channels:\n"), snapName, req)
   351  		msg += installTable(snapName, action, hits, false)
   352  		msg += "\n"
   353  		if req.Risk == "stable" {
   354  			msg += "\n" + preRelWarn
   355  		}
   356  		msg += "\n" + moreInfoHint
   357  		return msg
   358  	}
   359  
   360  	// there are matching arch+risk releases => give hints and instructions
   361  	// about these other tracks
   362  	if hits := matches["architecture:risk"]; len(hits) != 0 {
   363  		// TRANSLATORS: %q is for the snap name, %s is the full requested channel
   364  		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())
   365  		msg += installTable(snapName, action, hits, true)
   366  		msg += "\n\n" + trackWarn
   367  		msg += "\n" + moreInfoHint
   368  		return msg
   369  	}
   370  
   371  	// generic error
   372  	// TRANSLATORS: %q is for the snap name, %s is the full requested channel
   373  	msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but other tracks exist.\n"), snapName, req.Full())
   374  	msg += "\n\n" + trackWarn
   375  	msg += "\n" + moreInfoHint
   376  	return msg
   377  }
   378  
   379  func installTable(snapName, action string, avail []*channel.Channel, full bool) string {
   380  	b := &bytes.Buffer{}
   381  	w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0)
   382  	first := true
   383  	for _, a := range avail {
   384  		if first {
   385  			first = false
   386  		} else {
   387  			fmt.Fprint(w, "\n")
   388  		}
   389  		var ch string
   390  		if full {
   391  			ch = a.Full()
   392  		} else {
   393  			ch = a.String()
   394  		}
   395  		chOption := channelOption(a)
   396  		fmt.Fprintf(w, "%s\tsnap %s %s %s", ch, action, chOption, snapName)
   397  	}
   398  	w.Flush()
   399  	tbl := b.String()
   400  	// indent to drive fill/ToText to keep the tabulations intact
   401  	lines := strings.SplitAfter(tbl, "\n")
   402  	for i := range lines {
   403  		lines[i] = "  " + lines[i]
   404  	}
   405  	return strings.Join(lines, "")
   406  }
   407  
   408  func channelOption(c *channel.Channel) string {
   409  	if c.Branch == "" {
   410  		if c.Track == "" {
   411  			return fmt.Sprintf("--%s", c.Risk)
   412  		}
   413  		if c.Risk == "stable" {
   414  			return fmt.Sprintf("--channel=%s", c.Track)
   415  		}
   416  	}
   417  	return fmt.Sprintf("--channel=%s", c)
   418  }
   419  
   420  func archsForChannels(cs []*channel.Channel) []string {
   421  	archs := []string{}
   422  	for _, c := range cs {
   423  		if !strutil.ListContains(archs, c.Architecture) {
   424  			archs = append(archs, c.Architecture)
   425  		}
   426  	}
   427  	return archs
   428  }