github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap/main.go (about)

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