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