Skip to content

fix: improve exit codes for agent/agentssh and cli/ssh #10850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions agent/agentssh/agentssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,29 @@ func (s *Server) sessionHandler(session ssh.Session) {
err := s.sessionStart(logger, session, extraEnv)
var exitError *exec.ExitError
if xerrors.As(err, &exitError) {
logger.Info(ctx, "ssh session returned", slog.Error(exitError))
_ = session.Exit(exitError.ExitCode())
code := exitError.ExitCode()
if code == -1 {
// If we return -1 here, it will be transmitted as an
// uint32(4294967295). This exit code is nonsense, so
// instead we return 255 (same as OpenSSH). This is
// also the same exit code that the shell returns for
// -1.
//
// For signals, we could consider sending 128+signal
// instead (however, OpenSSH doesn't seem to do this).
code = 255
}
logger.Info(ctx, "ssh session returned",
slog.Error(exitError),
slog.F("process_exit_code", exitError.ExitCode()),
slog.F("exit_code", code),
)

// TODO(mafredri): For signal exit, there's also an "exit-signal"
// request (session.Exit sends "exit-status"), however, since it's
// not implemented on the session interface and not used by
// OpenSSH, we'll leave it for now.
_ = session.Exit(code)
return
}
if err != nil {
Expand Down
16 changes: 14 additions & 2 deletions agent/agentssh/agentssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,13 @@ func TestNewServer_Signal(t *testing.T) {
require.NoError(t, sc.Err())

err = sess.Wait()
require.Error(t, err)
exitErr := &ssh.ExitError{}
require.ErrorAs(t, err, &exitErr)
wantCode := 255
if runtime.GOOS == "windows" {
wantCode = 1
}
require.Equal(t, wantCode, exitErr.ExitStatus())
})
t.Run("PTY", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -300,7 +306,13 @@ func TestNewServer_Signal(t *testing.T) {
require.NoError(t, sc.Err())

err = sess.Wait()
require.Error(t, err)
exitErr := &ssh.ExitError{}
require.ErrorAs(t, err, &exitErr)
wantCode := 255
if runtime.GOOS == "windows" {
wantCode = 1
}
require.Equal(t, wantCode, exitErr.ExitStatus())
})
}

Expand Down
38 changes: 35 additions & 3 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,22 @@ func (r *RootCmd) RunMain(subcommands []*clibase.Cmd) {
}
err = cmd.Invoke().WithOS().Run()
if err != nil {
code := 1
var exitErr *exitError
if errors.As(err, &exitErr) {
code = exitErr.code
err = exitErr.err
}
if errors.Is(err, cliui.Canceled) {
//nolint:revive
os.Exit(1)
os.Exit(code)
}
f := prettyErrorFormatter{w: os.Stderr, verbose: r.verbose}
f.format(err)
if err != nil {
f.format(err)
}
//nolint:revive
os.Exit(1)
os.Exit(code)
}
}

Expand Down Expand Up @@ -953,6 +961,30 @@ func DumpHandler(ctx context.Context) {
}
}

type exitError struct {
code int
err error
}

var _ error = (*exitError)(nil)

func (e *exitError) Error() string {
if e.err != nil {
return fmt.Sprintf("exit code %d: %v", e.code, e.err)
}
return fmt.Sprintf("exit code %d", e.code)
}

func (e *exitError) Unwrap() error {
return e.err
}

// ExitError returns an error that will cause the CLI to exit with the given
// exit code. If err is non-nil, it will be wrapped by the returned error.
func ExitError(code int, err error) error {
return &exitError{code: code, err: err}
}

// IiConnectionErr is a convenience function for checking if the source of an
// error is due to a 'connection refused', 'no such host', etc.
func isConnectionError(err error) bool {
Expand Down
7 changes: 6 additions & 1 deletion cli/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,16 @@ func (r *RootCmd) ssh() *clibase.Cmd {

err = sshSession.Wait()
if err != nil {
if exitErr := (&gossh.ExitError{}); errors.As(err, &exitErr) {
// Clear the error since it's not useful beyond
// reporting status.
return ExitError(exitErr.ExitStatus(), nil)
}
// If the connection drops unexpectedly, we get an
// ExitMissingError but no other error details, so try to at
// least give the user a better message
if errors.Is(err, &gossh.ExitMissingError{}) {
return xerrors.New("SSH connection ended unexpectedly")
return ExitError(255, xerrors.New("SSH connection ended unexpectedly"))
}
return xerrors.Errorf("session ended: %w", err)
}
Expand Down