github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/cmd/snap/error.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2018 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 "bytes" 24 "errors" 25 "fmt" 26 "go/doc" 27 "os" 28 "os/user" 29 "strings" 30 "text/tabwriter" 31 32 "golang.org/x/crypto/ssh/terminal" 33 34 "github.com/snapcore/snapd/client" 35 "github.com/snapcore/snapd/i18n" 36 "github.com/snapcore/snapd/logger" 37 "github.com/snapcore/snapd/osutil" 38 "github.com/snapcore/snapd/snap/channel" 39 "github.com/snapcore/snapd/strutil" 40 ) 41 42 var errorPrefix = i18n.G("error: %v\n") 43 44 var termSize = termSizeImpl 45 46 func termSizeImpl() (width, height int) { 47 if f, ok := Stdout.(*os.File); ok { 48 width, height, _ = terminal.GetSize(int(f.Fd())) 49 } 50 51 if width <= 0 { 52 width = int(osutil.GetenvInt64("COLUMNS")) 53 } 54 55 if height <= 0 { 56 height = int(osutil.GetenvInt64("LINES")) 57 } 58 59 if width < 40 { 60 width = 80 61 } 62 63 if height < 15 { 64 height = 25 65 } 66 67 return width, height 68 } 69 70 func fill(para string, indent int) string { 71 width, _ := termSize() 72 73 if width > 100 { 74 width = 100 75 } 76 77 // some terminals aren't happy about writing in the last 78 // column (they'll add line for you). We could check terminfo 79 // for "sam" (semi_auto_right_margin), but that's a lot of 80 // work just for this. 81 width-- 82 83 var buf bytes.Buffer 84 indentStr := strings.Repeat(" ", indent) 85 doc.ToText(&buf, para, indentStr, indentStr, width-indent) 86 87 return strings.TrimSpace(buf.String()) 88 } 89 90 func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (string, error) { 91 // do this here instead of in the caller for more DRY 92 err, ok := e.(*client.Error) 93 if !ok { 94 return "", e 95 } 96 // retryable errors are just passed through 97 if client.IsRetryable(err) { 98 return "", err 99 } 100 101 // ensure the "real" error is available if we ask for it 102 logger.Debugf("error: %s", err) 103 104 // FIXME: using err.Message in user-facing messaging is not 105 // l10n-friendly, and probably means we're missing ad-hoc messaging. 106 isError := true 107 usesSnapName := true 108 var msg string 109 switch err.Kind { 110 case client.ErrorKindNotSnap: 111 msg = i18n.G(`%q does not contain an unpacked snap. 112 113 Try 'snapcraft prime' in your project directory, then 'snap try' again.`) 114 if snapName == "" || snapName == "./" { 115 errValStr, ok := err.Value.(string) 116 if ok && errValStr != "" { 117 snapName = errValStr 118 } 119 } 120 case client.ErrorKindSnapNotFound: 121 msg = i18n.G("snap %q not found") 122 if snapName == "" { 123 errValStr, ok := err.Value.(string) 124 if ok && errValStr != "" { 125 snapName = errValStr 126 } 127 } 128 case client.ErrorKindSnapChannelNotAvailable, 129 client.ErrorKindSnapArchitectureNotAvailable: 130 values, ok := err.Value.(map[string]interface{}) 131 if ok { 132 candName, _ := values["snap-name"].(string) 133 if candName != "" { 134 snapName = candName 135 } 136 action, _ := values["action"].(string) 137 arch, _ := values["architecture"].(string) 138 channel, _ := values["channel"].(string) 139 releases, _ := values["releases"].([]interface{}) 140 if snapName != "" && action != "" && arch != "" && channel != "" && len(releases) != 0 { 141 usesSnapName = false 142 msg = snapRevisionNotAvailableMessage(err.Kind, snapName, action, arch, channel, releases) 143 break 144 } 145 } 146 fallthrough 147 case client.ErrorKindSnapRevisionNotAvailable: 148 if snapName == "" { 149 errValStr, ok := err.Value.(string) 150 if ok && errValStr != "" { 151 snapName = errValStr 152 } 153 } 154 155 usesSnapName = false 156 // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name). 157 msg = fmt.Sprintf(i18n.G(`snap %[1]q not available as specified (see 'snap info %[1]s')`), snapName) 158 159 if opts != nil { 160 if opts.Revision != "" { 161 // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %s is whatever the user used for --revision= 162 msg = fmt.Sprintf(i18n.G(`snap %[1]q revision %s not available (see 'snap info %[1]s')`), snapName, opts.Revision) 163 } else if opts.Channel != "" { 164 // (note --revision overrides --channel) 165 166 // TRANSLATORS: %[1]q and %[1]s refer to the same thing (a snap name); %q is whatever foo the user used for --channel=foo 167 msg = fmt.Sprintf(i18n.G(`snap %[1]q not available on channel %q (see 'snap info %[1]s')`), snapName, opts.Channel) 168 } 169 } 170 case client.ErrorKindSnapAlreadyInstalled: 171 isError = false 172 msg = i18n.G(`snap %q is already installed, see 'snap help refresh'`) 173 case client.ErrorKindSnapNeedsDevMode: 174 if opts != nil && opts.Dangerous { 175 msg = i18n.G("snap %q requires devmode or confinement override") 176 break 177 } 178 msg = i18n.G(` 179 The publisher of snap %q has indicated that they do not consider this revision 180 to be of production quality and that it is only meant for development or testing 181 at this point. As a consequence this snap will not refresh automatically and may 182 perform arbitrary system changes outside of the security sandbox snaps are 183 generally confined to, which may put your system at risk. 184 185 If you understand and want to proceed repeat the command including --devmode; 186 if instead you want to install the snap forcing it into strict confinement 187 repeat the command including --jailmode.`) 188 case client.ErrorKindSnapNeedsClassic: 189 msg = i18n.G(` 190 This revision of snap %q was published using classic confinement and thus may 191 perform arbitrary system changes outside of the security sandbox that snaps are 192 usually confined to, which may put your system at risk. 193 194 If you understand and want to proceed repeat the command including --classic. 195 `) 196 case client.ErrorKindSnapNotClassic: 197 msg = i18n.G(`snap %q is not compatible with --classic`) 198 case client.ErrorKindLoginRequired: 199 usesSnapName = false 200 u, _ := user.Current() 201 if u != nil && u.Username == "root" { 202 // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) 203 msg = fmt.Sprintf(i18n.G(`%s (see 'snap help login')`), err.Message) 204 } else { 205 // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) 206 msg = fmt.Sprintf(i18n.G(`%s (try with sudo)`), err.Message) 207 } 208 case client.ErrorKindSnapLocal: 209 msg = i18n.G("local snap %q is unknown to the store, use --amend to proceed anyway") 210 case client.ErrorKindSnapNoUpdateAvailable: 211 isError = false 212 msg = i18n.G("snap %q has no updates available") 213 case client.ErrorKindSnapNotInstalled: 214 isError = false 215 usesSnapName = false 216 msg = err.Message 217 case client.ErrorKindNetworkTimeout: 218 isError = true 219 usesSnapName = false 220 msg = i18n.G("unable to contact snap store") 221 case client.ErrorKindSystemRestart: 222 isError = false 223 usesSnapName = false 224 msg = i18n.G("snapd is about to reboot the system") 225 values, ok := err.Value.(map[string]interface{}) 226 if ok { 227 op, ok := values["op"].(string) 228 if ok { 229 switch op { 230 case "halt": 231 msg = i18n.G("snapd is about to halt the system") 232 case "poweroff": 233 msg = i18n.G("snapd is about to power off the system") 234 } 235 } 236 } 237 case client.ErrorKindInsufficientDiskSpace: 238 // this error carries multiple snap names 239 usesSnapName = false 240 values, ok := err.Value.(map[string]interface{}) 241 if ok { 242 changeKind, _ := values["change-kind"].(string) 243 snaps, _ := values["snap-names"].([]interface{}) 244 snapNames := make([]string, len(snaps)) 245 for i, v := range snaps { 246 snapNames[i] = fmt.Sprint(v) 247 } 248 names := strutil.Quoted(snapNames) 249 switch changeKind { 250 case "remove": 251 msg = fmt.Sprintf(i18n.G("cannot remove %s due to low disk space for automatic snapshot, use --purge to avoid creating a snapshot"), names) 252 case "install": 253 msg = fmt.Sprintf(i18n.G("cannot install %s due to low disk space"), names) 254 case "refresh": 255 msg = fmt.Sprintf(i18n.G("cannot refresh %s due to low disk space"), names) 256 default: 257 msg = err.Error() 258 } 259 break 260 } 261 fallthrough 262 default: 263 usesSnapName = false 264 msg = err.Message 265 } 266 267 if usesSnapName { 268 msg = fmt.Sprintf(msg, snapName) 269 } 270 // 3 is the %v\n, which will be present in any locale 271 msg = fill(msg, len(errorPrefix)-3) 272 if isError { 273 return "", errors.New(msg) 274 } 275 276 return msg, nil 277 } 278 279 func snapRevisionNotAvailableMessage(kind client.ErrorKind, snapName, action, arch, snapChannel string, releases []interface{}) string { 280 // releases contains all available (arch x channel) 281 // as reported by the store through the daemon 282 req, err := channel.Parse(snapChannel, arch) 283 if err != nil { 284 // XXX: this is no longer possible (should be caught before hitting the store), unless the state itself has an invalid channel 285 // TRANSLATORS: %q is the invalid request channel, %s is the snap name 286 msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), snapChannel, snapName) 287 return msg 288 } 289 avail := make([]*channel.Channel, 0, len(releases)) 290 for _, v := range releases { 291 rel, _ := v.(map[string]interface{}) 292 relCh, _ := rel["channel"].(string) 293 relArch, _ := rel["architecture"].(string) 294 if relArch == "" { 295 logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v) 296 continue 297 } 298 a, err := channel.Parse(relCh, relArch) 299 if err != nil { 300 logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v) 301 continue 302 } 303 avail = append(avail, &a) 304 } 305 306 matches := map[string][]*channel.Channel{} 307 for _, a := range avail { 308 m := req.Match(a) 309 matchRepr := m.String() 310 if matchRepr != "" { 311 matches[matchRepr] = append(matches[matchRepr], a) 312 } 313 } 314 315 // no release is for this architecture 316 if kind == client.ErrorKindSnapArchitectureNotAvailable { 317 // TODO: add "Get more information..." hints once snap info 318 // support showing multiple/all archs 319 320 // there are matching track+risk releases for other archs 321 if hits := matches["track:risk"]; len(hits) != 0 { 322 archs := strings.Join(archsForChannels(hits), ", ") 323 // TRANSLATORS: %q is for the snap name, %v is the requested channel, first %s is the system architecture short name, second %s is a comma separated list of available arch short names 324 msg := fmt.Sprintf(i18n.G("snap %q is not available on %v for this architecture (%s) but exists on other architectures (%s)."), snapName, req, arch, archs) 325 return msg 326 } 327 328 // not even that, generic error 329 archs := strings.Join(archsForChannels(avail), ", ") 330 // TRANSLATORS: %q is for the snap name, first %s is the system architecture short name, second %s is a comma separated list of available arch short names 331 msg := fmt.Sprintf(i18n.G("snap %q is not available on this architecture (%s) but exists on other architectures (%s)."), snapName, arch, archs) 332 return msg 333 } 334 335 // a branch was requested 336 if req.Branch != "" { 337 // there are matching arch+track+risk, give main track info 338 if len(matches["architecture:track:risk"]) != 0 { 339 trackRisk := channel.Channel{Track: req.Track, Risk: req.Risk} 340 trackRisk = trackRisk.Clean() 341 342 // TRANSLATORS: %q is for the snap name, first %s is the full requested channel 343 msg := fmt.Sprintf(i18n.G("requested a non-existing branch on %s for snap %q: %s"), trackRisk.Full(), snapName, req.Branch) 344 return msg 345 } 346 347 msg := fmt.Sprintf(i18n.G("requested a non-existing branch for snap %q: %s"), snapName, req.Full()) 348 return msg 349 } 350 351 // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint 352 preRelWarn := i18n.G("Please be mindful pre-release channels may include features not completely tested or implemented.") 353 // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint 354 trackWarn := i18n.G("Please be mindful that different tracks may include different features.") 355 // TRANSLATORS: %s is for the snap name, will be concatenated after at the end of other error messages, possibly after a blank line 356 moreInfoHint := fmt.Sprintf(i18n.G("Get more information with 'snap info %s'."), snapName) 357 358 // there are matching arch+track releases => give hint and instructions 359 // about pre-release channels 360 if hits := matches["architecture:track"]; len(hits) != 0 { 361 // TRANSLATORS: %q is for the snap name, %v is the requested channel 362 msg := fmt.Sprintf(i18n.G("snap %q is not available on %v but is available to install on the following channels:\n"), snapName, req) 363 msg += installTable(snapName, action, hits, false) 364 msg += "\n" 365 if req.Risk == "stable" { 366 msg += "\n" + preRelWarn 367 } 368 msg += "\n" + moreInfoHint 369 return msg 370 } 371 372 // there are matching arch+risk releases => give hints and instructions 373 // about these other tracks 374 if hits := matches["architecture:risk"]; len(hits) != 0 { 375 // TRANSLATORS: %q is for the snap name, %s is the full requested channel 376 msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but is available to install on the following tracks:\n"), snapName, req.Full()) 377 msg += installTable(snapName, action, hits, true) 378 msg += "\n\n" + trackWarn 379 msg += "\n" + moreInfoHint 380 return msg 381 } 382 383 // generic error 384 // TRANSLATORS: %q is for the snap name, %s is the full requested channel 385 msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but other tracks exist.\n"), snapName, req.Full()) 386 msg += "\n\n" + trackWarn 387 msg += "\n" + moreInfoHint 388 return msg 389 } 390 391 func installTable(snapName, action string, avail []*channel.Channel, full bool) string { 392 b := &bytes.Buffer{} 393 w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0) 394 first := true 395 for _, a := range avail { 396 if first { 397 first = false 398 } else { 399 fmt.Fprint(w, "\n") 400 } 401 var ch string 402 if full { 403 ch = a.Full() 404 } else { 405 ch = a.String() 406 } 407 chOption := channelOption(a) 408 fmt.Fprintf(w, "%s\tsnap %s %s %s", ch, action, chOption, snapName) 409 } 410 w.Flush() 411 tbl := b.String() 412 // indent to drive fill/ToText to keep the tabulations intact 413 lines := strings.SplitAfter(tbl, "\n") 414 for i := range lines { 415 lines[i] = " " + lines[i] 416 } 417 return strings.Join(lines, "") 418 } 419 420 func channelOption(c *channel.Channel) string { 421 if c.Branch == "" { 422 if c.Track == "" { 423 return fmt.Sprintf("--%s", c.Risk) 424 } 425 if c.Risk == "stable" { 426 return fmt.Sprintf("--channel=%s", c.Track) 427 } 428 } 429 return fmt.Sprintf("--channel=%s", c) 430 } 431 432 func archsForChannels(cs []*channel.Channel) []string { 433 archs := []string{} 434 for _, c := range cs { 435 if !strutil.ListContains(archs, c.Architecture) { 436 archs = append(archs, c.Architecture) 437 } 438 } 439 return archs 440 }