github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 }