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 }