github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/cmd/snap/main.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"os"
    27  	"path/filepath"
    28  	"runtime"
    29  	"strings"
    30  	"unicode"
    31  	"unicode/utf8"
    32  
    33  	"github.com/jessevdk/go-flags"
    34  	"golang.org/x/crypto/ssh/terminal"
    35  	"golang.org/x/xerrors"
    36  
    37  	"github.com/snapcore/snapd/client"
    38  	"github.com/snapcore/snapd/dirs"
    39  	"github.com/snapcore/snapd/i18n"
    40  	"github.com/snapcore/snapd/logger"
    41  	"github.com/snapcore/snapd/osutil"
    42  	"github.com/snapcore/snapd/release"
    43  	"github.com/snapcore/snapd/snap"
    44  	"github.com/snapcore/snapd/snap/squashfs"
    45  	"github.com/snapcore/snapd/snapdenv"
    46  	"github.com/snapcore/snapd/snapdtool"
    47  )
    48  
    49  func init() {
    50  	// set User-Agent for when 'snap' talks to the store directly (snap download etc...)
    51  	snapdenv.SetUserAgentFromVersion(snapdtool.Version, nil, "snap")
    52  
    53  	if osutil.GetenvBool("SNAPD_DEBUG") || snapdenv.Testing() {
    54  		// in tests or when debugging, enforce the "tidy" lint checks
    55  		noticef = logger.Panicf
    56  	}
    57  
    58  	// plug/slot sanitization not used by snap commands (except for snap pack
    59  	// which re-sets it), make it no-op.
    60  	snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {}
    61  }
    62  
    63  var (
    64  	// Standard streams, redirected for testing.
    65  	Stdin  io.Reader = os.Stdin
    66  	Stdout io.Writer = os.Stdout
    67  	Stderr io.Writer = os.Stderr
    68  	// overridden for testing
    69  	ReadPassword = terminal.ReadPassword
    70  	// set to logger.Panicf in testing
    71  	noticef = logger.Noticef
    72  )
    73  
    74  type options struct {
    75  	Version func() `long:"version"`
    76  }
    77  
    78  type argDesc struct {
    79  	name string
    80  	desc string
    81  }
    82  
    83  var optionsData options
    84  
    85  // ErrExtraArgs is returned  if extra arguments to a command are found
    86  var ErrExtraArgs = fmt.Errorf(i18n.G("too many arguments for command"))
    87  
    88  // cmdInfo holds information needed to call parser.AddCommand(...).
    89  type cmdInfo struct {
    90  	name, shortHelp, longHelp string
    91  	builder                   func() flags.Commander
    92  	hidden                    bool
    93  	// completeHidden set to true forces completion even of
    94  	// a hidden command
    95  	completeHidden bool
    96  	optDescs       map[string]string
    97  	argDescs       []argDesc
    98  	alias          string
    99  	extra          func(*flags.Command)
   100  }
   101  
   102  // commands holds information about all non-debug commands.
   103  var commands []*cmdInfo
   104  
   105  // debugCommands holds information about all debug commands.
   106  var debugCommands []*cmdInfo
   107  
   108  // routineCommands holds information about all internal commands.
   109  var routineCommands []*cmdInfo
   110  
   111  // addCommand replaces parser.addCommand() in a way that is compatible with
   112  // re-constructing a pristine parser.
   113  func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
   114  	info := &cmdInfo{
   115  		name:      name,
   116  		shortHelp: shortHelp,
   117  		longHelp:  longHelp,
   118  		builder:   builder,
   119  		optDescs:  optDescs,
   120  		argDescs:  argDescs,
   121  	}
   122  	commands = append(commands, info)
   123  	return info
   124  }
   125  
   126  // addDebugCommand replaces parser.addCommand() in a way that is
   127  // compatible with re-constructing a pristine parser. It is meant for
   128  // adding debug commands.
   129  func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
   130  	info := &cmdInfo{
   131  		name:      name,
   132  		shortHelp: shortHelp,
   133  		longHelp:  longHelp,
   134  		builder:   builder,
   135  		optDescs:  optDescs,
   136  		argDescs:  argDescs,
   137  	}
   138  	debugCommands = append(debugCommands, info)
   139  	return info
   140  }
   141  
   142  // addRoutineCommand replaces parser.addCommand() in a way that is
   143  // compatible with re-constructing a pristine parser. It is meant for
   144  // adding "snap routine" commands.
   145  func addRoutineCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
   146  	info := &cmdInfo{
   147  		name:      name,
   148  		shortHelp: shortHelp,
   149  		longHelp:  longHelp,
   150  		builder:   builder,
   151  		optDescs:  optDescs,
   152  		argDescs:  argDescs,
   153  	}
   154  	routineCommands = append(routineCommands, info)
   155  	return info
   156  }
   157  
   158  type parserSetter interface {
   159  	setParser(*flags.Parser)
   160  }
   161  
   162  func lintDesc(cmdName, optName, desc, origDesc string) {
   163  	if len(optName) == 0 {
   164  		logger.Panicf("option on %q has no name", cmdName)
   165  	}
   166  	if len(origDesc) != 0 {
   167  		logger.Panicf("description of %s's %q of %q set from tag (=> no i18n)", cmdName, optName, origDesc)
   168  	}
   169  	if len(desc) > 0 {
   170  		// decode the first rune instead of converting all of desc into []rune
   171  		r, _ := utf8.DecodeRuneInString(desc)
   172  		// note IsLower != !IsUpper for runes with no upper/lower.
   173  		if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") && !strings.HasPrefix(desc, cmdName) {
   174  			noticef("description of %s's %q is lowercase in locale %q: %q", cmdName, optName, i18n.CurrentLocale(), desc)
   175  		}
   176  	}
   177  }
   178  
   179  func lintArg(cmdName, optName, desc, origDesc string) {
   180  	lintDesc(cmdName, optName, desc, origDesc)
   181  	if len(optName) > 0 && optName[0] == '<' && optName[len(optName)-1] == '>' {
   182  		return
   183  	}
   184  	if len(optName) > 0 && optName[0] == '<' && strings.HasSuffix(optName, ">s") {
   185  		// see comment in fixupArg about the >s case
   186  		return
   187  	}
   188  	noticef("argument %q's %q should begin with < and end with >", cmdName, optName)
   189  }
   190  
   191  func fixupArg(optName string) string {
   192  	// Due to misunderstanding some localized versions of option name are
   193  	// literally "<option>s" instead of "<option>". While translators can
   194  	// improve this over time we can be smarter and avoid silly messages
   195  	// logged whenever "snap" command is used.
   196  	//
   197  	// See: https://bugs.launchpad.net/snapd/+bug/1806761
   198  	if strings.HasSuffix(optName, ">s") {
   199  		return optName[:len(optName)-1]
   200  	}
   201  	return optName
   202  }
   203  
   204  type clientSetter interface {
   205  	setClient(*client.Client)
   206  }
   207  
   208  type clientMixin struct {
   209  	client *client.Client
   210  }
   211  
   212  func (ch *clientMixin) setClient(cli *client.Client) {
   213  	ch.client = cli
   214  }
   215  
   216  func firstNonOptionIsRun() bool {
   217  	if len(os.Args) < 2 {
   218  		return false
   219  	}
   220  	for _, arg := range os.Args[1:] {
   221  		if len(arg) == 0 || arg[0] == '-' {
   222  			continue
   223  		}
   224  		return arg == "run"
   225  	}
   226  	return false
   227  }
   228  
   229  // noCompletion marks command descriptions of commands that should not
   230  // be completed
   231  var noCompletion = make(map[string]bool)
   232  
   233  func markForNoCompletion(ci *cmdInfo) {
   234  	if ci.hidden && !ci.completeHidden {
   235  		if ci.shortHelp == "" {
   236  			logger.Panicf("%q missing short help", ci.name)
   237  		}
   238  		noCompletion[ci.shortHelp] = true
   239  	}
   240  }
   241  
   242  // completionHandler filters out unwanted completions based on
   243  // the noCompletion map before dumping them to stdout.
   244  func completionHandler(comps []flags.Completion) {
   245  	for _, comp := range comps {
   246  		if noCompletion[comp.Description] {
   247  			continue
   248  		}
   249  		fmt.Fprintln(Stdout, comp.Item)
   250  	}
   251  }
   252  
   253  func registerCommands(cli *client.Client, parser *flags.Parser, baseCmd *flags.Command, commands []*cmdInfo, checkUnique func(*cmdInfo)) {
   254  	for _, c := range commands {
   255  		checkUnique(c)
   256  		markForNoCompletion(c)
   257  
   258  		obj := c.builder()
   259  		if x, ok := obj.(clientSetter); ok {
   260  			x.setClient(cli)
   261  		}
   262  		if x, ok := obj.(parserSetter); ok {
   263  			x.setParser(parser)
   264  		}
   265  
   266  		cmd, err := baseCmd.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj)
   267  		if err != nil {
   268  			logger.Panicf("cannot add command %q: %v", c.name, err)
   269  		}
   270  		cmd.Hidden = c.hidden
   271  		if c.alias != "" {
   272  			cmd.Aliases = append(cmd.Aliases, c.alias)
   273  		}
   274  
   275  		opts := cmd.Options()
   276  		if c.optDescs != nil && len(opts) != len(c.optDescs) {
   277  			logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs))
   278  		}
   279  		for _, opt := range opts {
   280  			name := opt.LongName
   281  			if name == "" {
   282  				name = string(opt.ShortName)
   283  			}
   284  			desc, ok := c.optDescs[name]
   285  			if !(c.optDescs == nil || ok) {
   286  				logger.Panicf("%s missing description for %s", c.name, name)
   287  			}
   288  			lintDesc(c.name, name, desc, opt.Description)
   289  			if desc != "" {
   290  				opt.Description = desc
   291  			}
   292  		}
   293  
   294  		args := cmd.Args()
   295  		if c.argDescs != nil && len(args) != len(c.argDescs) {
   296  			logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs))
   297  		}
   298  		for i, arg := range args {
   299  			name, desc := arg.Name, ""
   300  			if c.argDescs != nil {
   301  				name = c.argDescs[i].name
   302  				desc = c.argDescs[i].desc
   303  			}
   304  			lintArg(c.name, name, desc, arg.Description)
   305  			name = fixupArg(name)
   306  			arg.Name = name
   307  			arg.Description = desc
   308  		}
   309  		if c.extra != nil {
   310  			c.extra(cmd)
   311  		}
   312  	}
   313  }
   314  
   315  // Parser creates and populates a fresh parser.
   316  // Since commands have local state a fresh parser is required to isolate tests
   317  // from each other.
   318  func Parser(cli *client.Client) *flags.Parser {
   319  	optionsData.Version = func() {
   320  		printVersions(cli)
   321  		panic(&exitStatus{0})
   322  	}
   323  	flagopts := flags.Options(flags.PassDoubleDash)
   324  	if firstNonOptionIsRun() {
   325  		flagopts |= flags.PassAfterNonOption
   326  	}
   327  	parser := flags.NewParser(&optionsData, flagopts)
   328  	parser.CompletionHandler = completionHandler
   329  	parser.ShortDescription = i18n.G("Tool to interact with snaps")
   330  	parser.LongDescription = longSnapDescription
   331  	// hide the unhelpful "[OPTIONS]" from help output
   332  	parser.Usage = ""
   333  	if version := parser.FindOptionByLongName("version"); version != nil {
   334  		version.Description = i18n.G("Print the version and exit")
   335  		version.Hidden = true
   336  	}
   337  	// add --help like what go-flags would do for us, but hidden
   338  	addHelp(parser)
   339  
   340  	seen := make(map[string]bool, len(commands)+len(debugCommands)+len(routineCommands))
   341  	checkUnique := func(ci *cmdInfo, kind string) {
   342  		if seen[ci.shortHelp] && ci.shortHelp != "Internal" && ci.shortHelp != "Deprecated (hidden)" {
   343  			logger.Panicf(`%scommand %q has an already employed description != "Internal"|"Deprecated (hidden)": %s`, kind, ci.name, ci.shortHelp)
   344  		}
   345  		seen[ci.shortHelp] = true
   346  	}
   347  
   348  	// Add all regular commands
   349  	registerCommands(cli, parser, parser.Command, commands, func(ci *cmdInfo) {
   350  		checkUnique(ci, "")
   351  	})
   352  	// Add the debug command
   353  	debugCommand, err := parser.AddCommand("debug", shortDebugHelp, longDebugHelp, &cmdDebug{})
   354  	if err != nil {
   355  		logger.Panicf("cannot add command %q: %v", "debug", err)
   356  	}
   357  	// Add all the sub-commands of the debug command
   358  	registerCommands(cli, parser, debugCommand, debugCommands, func(ci *cmdInfo) {
   359  		checkUnique(ci, "debug ")
   360  	})
   361  	// Add the internal command
   362  	routineCommand, err := parser.AddCommand("routine", shortRoutineHelp, longRoutineHelp, &cmdRoutine{})
   363  	routineCommand.Hidden = true
   364  	if err != nil {
   365  		logger.Panicf("cannot add command %q: %v", "internal", err)
   366  	}
   367  	// Add all the sub-commands of the routine command
   368  	registerCommands(cli, parser, routineCommand, routineCommands, func(ci *cmdInfo) {
   369  		checkUnique(ci, "routine ")
   370  	})
   371  	return parser
   372  }
   373  
   374  var isStdinTTY = terminal.IsTerminal(0)
   375  
   376  // ClientConfig is the configuration of the Client used by all commands.
   377  var ClientConfig = client.Config{
   378  	// we need the powerful snapd socket
   379  	Socket: dirs.SnapdSocket,
   380  	// Allow interactivity if we have a terminal
   381  	Interactive: isStdinTTY,
   382  }
   383  
   384  // Client returns a new client using ClientConfig as configuration.
   385  // commands should (in general) not use this, and instead use clientMixin.
   386  func mkClient() *client.Client {
   387  	cfg := &ClientConfig
   388  	// Set client user-agent when talking to the snapd daemon to the
   389  	// same value as when talking to the store.
   390  	cfg.UserAgent = snapdenv.UserAgent()
   391  
   392  	cli := client.New(cfg)
   393  	goos := runtime.GOOS
   394  	if release.OnWSL {
   395  		goos = "Windows Subsystem for Linux"
   396  	}
   397  	if goos != "linux" {
   398  		cli.Hijack(func(*http.Request) (*http.Response, error) {
   399  			fmt.Fprintf(Stderr, i18n.G(`Interacting with snapd is not yet supported on %s.
   400  This command has been left available for documentation purposes only.
   401  `), goos)
   402  			os.Exit(1)
   403  			panic("execution continued past call to exit")
   404  		})
   405  	}
   406  	return cli
   407  }
   408  
   409  func init() {
   410  	err := logger.SimpleSetup()
   411  	if err != nil {
   412  		fmt.Fprintf(Stderr, i18n.G("WARNING: failed to activate logging: %v\n"), err)
   413  	}
   414  }
   415  
   416  func resolveApp(snapApp string) (string, error) {
   417  	target, err := os.Readlink(filepath.Join(dirs.SnapBinariesDir, snapApp))
   418  	if err != nil {
   419  		return "", err
   420  	}
   421  	if filepath.Base(target) == target { // alias pointing to an app command in /snap/bin
   422  		return target, nil
   423  	}
   424  	return snapApp, nil
   425  }
   426  
   427  // exitCodeFromError takes an error and returns specific exit codes
   428  // for some errors. Otherwise the generic exit code 1 is returned.
   429  func exitCodeFromError(err error) int {
   430  	var mksquashfsError squashfs.MksquashfsError
   431  	var cmdlineFlagsError *flags.Error
   432  	var unknownCmdError unknownCommandError
   433  
   434  	switch {
   435  	case err == nil:
   436  		return 0
   437  	case client.IsRetryable(err):
   438  		return 10
   439  	case xerrors.As(err, &mksquashfsError):
   440  		return 20
   441  	case xerrors.As(err, &cmdlineFlagsError) || xerrors.As(err, &unknownCmdError):
   442  		// EX_USAGE, see sysexit.h
   443  		return 64
   444  	default:
   445  		return 1
   446  	}
   447  }
   448  
   449  func main() {
   450  	snapdtool.ExecInSnapdOrCoreSnap()
   451  
   452  	// check for magic symlink to /usr/bin/snap:
   453  	// 1. symlink from command-not-found to /usr/bin/snap: run c-n-f
   454  	if os.Args[0] == filepath.Join(dirs.GlobalRootDir, "/usr/lib/command-not-found") {
   455  		cmd := &cmdAdviseSnap{
   456  			Command: true,
   457  			Format:  "pretty",
   458  		}
   459  		// the bash.bashrc handler runs:
   460  		//    /usr/lib/command-not-found -- "$1"
   461  		// so skip over any "--"
   462  		for _, arg := range os.Args[1:] {
   463  			if arg != "--" {
   464  				cmd.Positionals.CommandOrPkg = arg
   465  				break
   466  			}
   467  		}
   468  		if err := cmd.Execute(nil); err != nil {
   469  			fmt.Fprintln(Stderr, err)
   470  		}
   471  		return
   472  	}
   473  
   474  	// 2. symlink from /snap/bin/$foo to /usr/bin/snap: run snapApp
   475  	snapApp := filepath.Base(os.Args[0])
   476  	if osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, snapApp)) {
   477  		var err error
   478  		snapApp, err = resolveApp(snapApp)
   479  		if err != nil {
   480  			fmt.Fprintf(Stderr, i18n.G("cannot resolve snap app %q: %v"), snapApp, err)
   481  			os.Exit(46)
   482  		}
   483  		cmd := &cmdRun{}
   484  		cmd.client = mkClient()
   485  		os.Args[0] = snapApp
   486  		// this will call syscall.Exec() so it does not return
   487  		// *unless* there is an error, i.e. we setup a wrong
   488  		// symlink (or syscall.Exec() fails for strange reasons)
   489  		err = cmd.Execute(os.Args)
   490  		fmt.Fprintf(Stderr, i18n.G("internal error, please report: running %q failed: %v\n"), snapApp, err)
   491  		os.Exit(46)
   492  	}
   493  
   494  	defer func() {
   495  		if v := recover(); v != nil {
   496  			if e, ok := v.(*exitStatus); ok {
   497  				os.Exit(e.code)
   498  			}
   499  			panic(v)
   500  		}
   501  	}()
   502  
   503  	// no magic /o\
   504  	if err := run(); err != nil {
   505  		fmt.Fprintf(Stderr, errorPrefix, err)
   506  		os.Exit(exitCodeFromError(err))
   507  	}
   508  }
   509  
   510  type exitStatus struct {
   511  	code int
   512  }
   513  
   514  func (e *exitStatus) Error() string {
   515  	return fmt.Sprintf("internal error: exitStatus{%d} being handled as normal error", e.code)
   516  }
   517  
   518  var wrongDashes = string([]rune{
   519  	0x2010, // hyphen
   520  	0x2011, // non-breaking hyphen
   521  	0x2012, // figure dash
   522  	0x2013, // en dash
   523  	0x2014, // em dash
   524  	0x2015, // horizontal bar
   525  	0xfe58, // small em dash
   526  	0x2015, // figure dash
   527  	0x2e3a, // two-em dash
   528  	0x2e3b, // three-em dash
   529  })
   530  
   531  type unknownCommandError struct {
   532  	msg string
   533  }
   534  
   535  func (e unknownCommandError) Error() string {
   536  	return e.msg
   537  }
   538  
   539  func run() error {
   540  	cli := mkClient()
   541  	parser := Parser(cli)
   542  	xtra, err := parser.Parse()
   543  	if err != nil {
   544  		if e, ok := err.(*flags.Error); ok {
   545  			switch e.Type {
   546  			case flags.ErrCommandRequired:
   547  				printShortHelp()
   548  				return nil
   549  			case flags.ErrHelp:
   550  				parser.WriteHelp(Stdout)
   551  				return nil
   552  			case flags.ErrUnknownCommand:
   553  				sub := os.Args[1]
   554  				sug := "snap help"
   555  				if len(xtra) > 0 {
   556  					sub = xtra[0]
   557  					if x := parser.Command.Active; x != nil && x.Name != "help" {
   558  						sug = "snap help " + x.Name
   559  					}
   560  				}
   561  				// TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help <cmd>'
   562  				return unknownCommandError{fmt.Sprintf(i18n.G("unknown command %q, see '%s'."), sub, sug)}
   563  			}
   564  		}
   565  
   566  		msg, err := errorToCmdMessage("", err, nil)
   567  
   568  		if cmdline := strings.Join(os.Args, " "); strings.ContainsAny(cmdline, wrongDashes) {
   569  			// TRANSLATORS: the %+q is the commandline (+q means quoted, with any non-ascii character called out). Please keep the lines to at most 80 characters.
   570  			fmt.Fprintf(Stderr, i18n.G(`Your command included some characters that look like dashes but are not:
   571      %+q
   572  in some situations you might find that when copying from an online source such
   573  as a blog you need to replace “typographic” dashes and quotes with their ASCII
   574  equivalent.  Dashes in particular are homoglyphs on most terminals and in most
   575  fixed-width fonts, so it can be hard to tell.
   576  
   577  `), cmdline)
   578  		}
   579  
   580  		if err != nil {
   581  			return err
   582  		}
   583  
   584  		fmt.Fprintln(Stderr, msg)
   585  		return nil
   586  	}
   587  
   588  	maybePresentWarnings(cli.WarningsSummary())
   589  
   590  	return nil
   591  }