diff --git a/sshd/run_unix.go b/sshd/run_unix.go index be1a537..81fdf59 100644 --- a/sshd/run_unix.go +++ b/sshd/run_unix.go @@ -25,6 +25,14 @@ func run(ch ssh.Channel, cmd string, p *ptyReq, win <-chan [2]uint32) int { if shell == "" { shell = "/bin/sh" } + // Without this the client sees only a session that ends at once. The container image Bamboo is + // published in contains no shell at all, so say so, and what to do about it. + if _, err := exec.LookPath(shell); err != nil { + fmt.Fprintf(ch.Stderr(), "bamboo: there is no shell to run (%v).\r\n"+ + "The Bamboo container image has none, and a shell in a container is not a shell on the machine. "+ + "Run Bamboo directly on the machine (for example with its systemd unit) to get one, or set SHELL to a shell that exists.\r\n", err) + return 127 + } var c *exec.Cmd if cmd == "" { c = exec.Command(shell, "-l") diff --git a/sshd/run_unix_test.go b/sshd/run_unix_test.go index 9ff8d4f..ec43519 100644 --- a/sshd/run_unix_test.go +++ b/sshd/run_unix_test.go @@ -82,3 +82,32 @@ func TestShell(t *testing.T) { t.Fatalf("got %q, %v", out.String(), err) } } + +// A machine with no shell (the published container image is one) must say so, not just hang up. +func TestNoShellIsExplained(t *testing.T) { + t.Setenv("SHELL", "/nonexistent/shell") + for name, usePty := range map[string]bool{"exec": false, "pty shell": true} { + s, done := session(t) + var stderr, stdout strings.Builder + s.Stderr, s.Stdout = &stderr, &stdout + var err error + if usePty { + if err = s.RequestPty("xterm", 24, 80, ssh.TerminalModes{}); err == nil { + err = s.Shell() + if err == nil { + err = s.Wait() + } + } + } else { + err = s.Run("true") + } + done() + var ee *ssh.ExitError + if !errors.As(err, &ee) || ee.ExitStatus() != 127 { + t.Errorf("%s: want exit status 127, got %v", name, err) + } + if !strings.Contains(stderr.String()+stdout.String(), "no shell to run") { + t.Errorf("%s: no explanation (stderr %q, stdout %q)", name, stderr.String(), stdout.String()) + } + } +}