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 }