Skip to content

Commit 156fdb7

Browse files
authored
[cloud] Improve cloud shell error messages (#548)
## Summary This shows better error messages when cloud shell fails. A possibly common case is when a nix package doesn't exist or doesn't build correctly. Ideally we don't end the shell and let the user fix it, but in the meantime this shows a more relevant error message. To implement this efficiently and extensibly, this uses an io.Writer to collect pre-defined ssh session errors without explicitly storing the output. ## How was it tested? * Added a bad package to my devbox.json and did `devbox cloud shell` * To test generic failures did `devbox cloud shell`, turned off my wifi and waited for a timeout <img width="1200" alt="image" src="https://user-images.githubusercontent.com/544948/215221477-63380ba9-8fd1-440b-9056-32e585d2131d.png">
1 parent 35ca46d commit 156fdb7

File tree

5 files changed

+82
-5
lines changed

5 files changed

+82
-5
lines changed

devbox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717
"nixpkgs": {
1818
"commit": "3954218cf613eba8e0dcefa9abe337d26bc48fd0"
1919
}
20-
}
20+
}

internal/boxcli/usererr/usererr.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ func WithUserMessage(source error, msg string, args ...any) error {
5959
}
6060
}
6161

62+
func WithLoggedUserMessage(source error, msg string, args ...any) error {
63+
if source == nil || HasUserMessage(source) {
64+
return source
65+
}
66+
return &combined{
67+
logged: true,
68+
source: source,
69+
userMessage: fmt.Sprintf(msg, args...),
70+
}
71+
}
72+
6273
func HasUserMessage(err error) bool {
6374
c := &combined{}
6475
return errors.As(err, &c) // note double pointer

internal/cloud/cloud.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ func shell(username, hostname, projectDir string, shellStartTime time.Time) erro
360360
ShellStartTime: telemetry.UnixTimestampFromTime(shellStartTime),
361361
Username: username,
362362
}
363-
return client.Shell()
363+
sessionErrors := newSSHSessionErrors()
364+
return cloudShellErrorHandler(client.Shell(sessionErrors), sessionErrors)
364365
}
365366

366367
// relativeProjectPathInVM refers to the project path relative to the user's

internal/cloud/errors.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package cloud
2+
3+
import (
4+
"io"
5+
"strings"
6+
7+
"go.jetpack.io/devbox/internal/boxcli/usererr"
8+
)
9+
10+
const errApplyNixDerivationString = "Error: apply Nix derivation:"
11+
12+
var sshSessionErrorStrings = []string{
13+
errApplyNixDerivationString,
14+
}
15+
16+
// sshSessionErrors is a helper struct to collect errors from ssh sessions.
17+
// For performance and privacy it doesn't actually keep any content from the
18+
// sessions, but instead just keeps track of which errors were encountered.
19+
type sshSessionErrors struct {
20+
errors map[string]bool
21+
}
22+
23+
var _ io.Writer = (*sshSessionErrors)(nil)
24+
25+
func newSSHSessionErrors() *sshSessionErrors {
26+
return &sshSessionErrors{
27+
errors: make(map[string]bool),
28+
}
29+
}
30+
31+
func (s *sshSessionErrors) Write(p []byte) (n int, err error) {
32+
for _, errorString := range sshSessionErrorStrings {
33+
if strings.Contains(string(p), errorString) {
34+
s.errors[errorString] = true
35+
}
36+
}
37+
return len(p), nil
38+
}
39+
40+
// cloudShellErrorHandler is a helper function to handle ssh errors that
41+
// may contain nix errors in them. For now being cautious and logging them
42+
// to Sentry even though they may be due to user action.
43+
func cloudShellErrorHandler(err error, sessionErrors *sshSessionErrors) error {
44+
if err == nil {
45+
return nil
46+
}
47+
48+
// This usually on initial setup when running start_devbox_shell.sh
49+
if found := sessionErrors.errors[errApplyNixDerivationString]; found {
50+
return usererr.WithLoggedUserMessage(
51+
err,
52+
"Failed to apply Nix derivation. This can happen if your devbox (nix) "+
53+
"packages don't exist or failed to build. Please check your "+
54+
"devbox.json and try again",
55+
)
56+
}
57+
58+
// This can happen due to connection issues or any other unforeseen errors
59+
return usererr.WithLoggedUserMessage(
60+
err,
61+
"Your cloud shell terminated unexpectedly. Please check your connection "+
62+
"and devbox.json and try again",
63+
)
64+
}

internal/cloud/openssh/client.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package openssh
66
import (
77
"bytes"
88
"fmt"
9+
"io"
910
"io/fs"
1011
"net"
1112
"os"
@@ -23,7 +24,7 @@ type Client struct {
2324
Username string
2425
}
2526

26-
func (c *Client) Shell() error {
27+
func (c *Client) Shell(w io.Writer) error {
2728

2829
cmd := c.cmd("-t")
2930
remoteCmd := fmt.Sprintf(
@@ -33,8 +34,8 @@ func (c *Client) Shell() error {
3334
)
3435
cmd.Args = append(cmd.Args, remoteCmd)
3536
cmd.Stdin = os.Stdin
36-
cmd.Stdout = os.Stdout
37-
cmd.Stderr = os.Stderr
37+
cmd.Stdout = io.MultiWriter(os.Stdout, w)
38+
cmd.Stderr = io.MultiWriter(os.Stderr, w)
3839
return logCmdRun(cmd)
3940
}
4041

0 commit comments

Comments
 (0)