github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 case client.ErrorKindInsufficientDiskSpace: 226 // this error carries multiple snap names 227 usesSnapName = false 228 values, ok := err.Value.(map[string]interface{}) 229 if ok { 230 changeKind, _ := values["change-kind"].(string) 231 snaps, _ := values["snap-names"].([]interface{}) 232 snapNames := make([]string, len(snaps)) 233 for i, v := range snaps { 234 snapNames[i] = fmt.Sprint(v) 235 } 236 names := strutil.Quoted(snapNames) 237 switch changeKind { 238 case "remove": 239 msg = fmt.Sprintf(i18n.G("cannot remove %s due to low disk space for automatic snapshot, use --purge to avoid creating a snapshot"), names) 240 case "install": 241 msg = fmt.Sprintf(i18n.G("cannot install %s due to low disk space"), names) 242 case "refresh": 243 msg = fmt.Sprintf(i18n.G("cannot refresh %s due to low disk space"), names) 244 default: 245 msg = err.Error() 246 } 247 break 248 } 249 fallthrough 250 default: 251 usesSnapName = false 252 msg = err.Message 253 } 254 255 if usesSnapName { 256 msg = fmt.Sprintf(msg, snapName) 257 } 258 // 3 is the %v\n, which will be present in any locale 259 msg = fill(msg, len(errorPrefix)-3) 260 if isError { 261 return "", errors.New(msg) 262 } 263 264 return msg, nil 265 } 266 267 func snapRevisionNotAvailableMessage(kind client.ErrorKind, snapName, action, arch, snapChannel string, releases []interface{}) string { 268 // releases contains all available (arch x channel) 269 // as reported by the store through the daemon 270 req, err := channel.Parse(snapChannel, arch) 271 if err != nil { 272 // XXX: this is no longer possible (should be caught before hitting the store), unless the state itself has an invalid channel 273 // TRANSLATORS: %q is the invalid request channel, %s is the snap name 274 msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), snapChannel, snapName) 275 return msg 276 } 277 avail := make([]*channel.Channel, 0, len(releases)) 278 for _, v := range releases { 279 rel, _ := v.(map[string]interface{}) 280 relCh, _ := rel["channel"].(string) 281 relArch, _ := rel["architecture"].(string) 282 if relArch == "" { 283 logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v) 284 continue 285 } 286 a, err := channel.Parse(relCh, relArch) 287 if err != nil { 288 logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v) 289 continue 290 } 291 avail = append(avail, &a) 292 } 293 294 matches := map[string][]*channel.Channel{} 295 for _, a := range avail { 296 m := req.Match(a) 297 matchRepr := m.String() 298 if matchRepr != "" { 299 matches[matchRepr] = append(matches[matchRepr], a) 300 } 301 } 302 303 // no release is for this architecture 304 if kind == client.ErrorKindSnapArchitectureNotAvailable { 305 // TODO: add "Get more information..." hints once snap info 306 // support showing multiple/all archs 307 308 // there are matching track+risk releases for other archs 309 if hits := matches["track:risk"]; len(hits) != 0 { 310 archs := strings.Join(archsForChannels(hits), ", ") 311 // 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 312 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) 313 return msg 314 } 315 316 // not even that, generic error 317 archs := strings.Join(archsForChannels(avail), ", ") 318 // 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 319 msg := fmt.Sprintf(i18n.G("snap %q is not available on this architecture (%s) but exists on other architectures (%s)."), snapName, arch, archs) 320 return msg 321 } 322 323 // a branch was requested 324 if req.Branch != "" { 325 // there are matching arch+track+risk, give main track info 326 if len(matches["architecture:track:risk"]) != 0 { 327 trackRisk := channel.Channel{Track: req.Track, Risk: req.Risk} 328 trackRisk = trackRisk.Clean() 329 330 // TRANSLATORS: %q is for the snap name, first %s is the full requested channel 331 msg := fmt.Sprintf(i18n.G("requested a non-existing branch on %s for snap %q: %s"), trackRisk.Full(), snapName, req.Branch) 332 return msg 333 } 334 335 msg := fmt.Sprintf(i18n.G("requested a non-existing branch for snap %q: %s"), snapName, req.Full()) 336 return msg 337 } 338 339 // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint 340 preRelWarn := i18n.G("Please be mindful pre-release channels may include features not completely tested or implemented.") 341 // TRANSLATORS: can optionally be concatenated after a blank line at the end of other error messages, together with the "Get more information ..." hint 342 trackWarn := i18n.G("Please be mindful that different tracks may include different features.") 343 // TRANSLATORS: %s is for the snap name, will be concatenated after at the end of other error messages, possibly after a blank line 344 moreInfoHint := fmt.Sprintf(i18n.G("Get more information with 'snap info %s'."), snapName) 345 346 // there are matching arch+track releases => give hint and instructions 347 // about pre-release channels 348 if hits := matches["architecture:track"]; len(hits) != 0 { 349 // TRANSLATORS: %q is for the snap name, %v is the requested channel 350 msg := fmt.Sprintf(i18n.G("snap %q is not available on %v but is available to install on the following channels:\n"), snapName, req) 351 msg += installTable(snapName, action, hits, false) 352 msg += "\n" 353 if req.Risk == "stable" { 354 msg += "\n" + preRelWarn 355 } 356 msg += "\n" + moreInfoHint 357 return msg 358 } 359 360 // there are matching arch+risk releases => give hints and instructions 361 // about these other tracks 362 if hits := matches["architecture:risk"]; len(hits) != 0 { 363 // TRANSLATORS: %q is for the snap name, %s is the full requested channel 364 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()) 365 msg += installTable(snapName, action, hits, true) 366 msg += "\n\n" + trackWarn 367 msg += "\n" + moreInfoHint 368 return msg 369 } 370 371 // generic error 372 // TRANSLATORS: %q is for the snap name, %s is the full requested channel 373 msg := fmt.Sprintf(i18n.G("snap %q is not available on %s but other tracks exist.\n"), snapName, req.Full()) 374 msg += "\n\n" + trackWarn 375 msg += "\n" + moreInfoHint 376 return msg 377 } 378 379 func installTable(snapName, action string, avail []*channel.Channel, full bool) string { 380 b := &bytes.Buffer{} 381 w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0) 382 first := true 383 for _, a := range avail { 384 if first { 385 first = false 386 } else { 387 fmt.Fprint(w, "\n") 388 } 389 var ch string 390 if full { 391 ch = a.Full() 392 } else { 393 ch = a.String() 394 } 395 chOption := channelOption(a) 396 fmt.Fprintf(w, "%s\tsnap %s %s %s", ch, action, chOption, snapName) 397 } 398 w.Flush() 399 tbl := b.String() 400 // indent to drive fill/ToText to keep the tabulations intact 401 lines := strings.SplitAfter(tbl, "\n") 402 for i := range lines { 403 lines[i] = " " + lines[i] 404 } 405 return strings.Join(lines, "") 406 } 407 408 func channelOption(c *channel.Channel) string { 409 if c.Branch == "" { 410 if c.Track == "" { 411 return fmt.Sprintf("--%s", c.Risk) 412 } 413 if c.Risk == "stable" { 414 return fmt.Sprintf("--channel=%s", c.Track) 415 } 416 } 417 return fmt.Sprintf("--channel=%s", c) 418 } 419 420 func archsForChannels(cs []*channel.Channel) []string { 421 archs := []string{} 422 for _, c := range cs { 423 if !strutil.ListContains(archs, c.Architecture) { 424 archs = append(archs, c.Architecture) 425 } 426 } 427 return archs 428 }