Files
Bamboo/sshd/run_unix.go
T
VeillaxandClaude Sonnet 5.5 79bd4d8646
CI / test (push) Successful in 3m34s
Release / release (push) Successful in 5m4s
ssh: say why when there is no shell to run
The published container image has no shell, so a session used to end at once with
nothing printed for a command. It now explains that, and what to do about it,
and exits with status 127.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 02:04:28 -06:00

96 lines
2.3 KiB
Go

//go:build !windows
package sshd
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"github.com/creack/pty"
"golang.org/x/crypto/ssh"
)
const (
ptySupported = true
runSupported = true
)
// run starts the user's shell (or, for exec, runs cmd through it) and returns its exit code.
// With a pty request it gets a terminal, otherwise plain pipes.
func run(ch ssh.Channel, cmd string, p *ptyReq, win <-chan [2]uint32) int {
shell := os.Getenv("SHELL")
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")
} else {
c = exec.Command(shell, "-c", cmd)
}
c.Env = os.Environ()
if p != nil {
term := p.term
if term == "" {
term = "xterm"
}
c.Env = append(c.Env, "TERM="+term)
return runPty(ch, c, p, win)
}
in, err := c.StdinPipe()
if err != nil {
fmt.Fprintf(ch.Stderr(), "bamboo: %v\r\n", err)
return 255
}
go func() { io.Copy(in, ch); in.Close() }()
c.Stdout = ch
c.Stderr = ch.Stderr()
return exitCode(c.Run(), c)
}
func runPty(ch ssh.Channel, c *exec.Cmd, p *ptyReq, win <-chan [2]uint32) int {
f, err := pty.StartWithSize(c, &pty.Winsize{Cols: uint16(p.cols), Rows: uint16(p.rows)})
if err != nil {
fmt.Fprintf(ch.Stderr(), "bamboo: %v\r\n", err)
return 255
}
defer f.Close()
done := make(chan struct{})
defer close(done)
go func() {
for {
select {
case w := <-win:
pty.Setsize(f, &pty.Winsize{Cols: uint16(w[0]), Rows: uint16(w[1])})
case <-done:
return
}
}
}()
go func() { io.Copy(f, ch) }()
io.Copy(ch, f) // ends with an error when the shell exits and the terminal closes
return exitCode(c.Wait(), c)
}
func exitCode(err error, c *exec.Cmd) int {
if err == nil {
return 0
}
var ee *exec.ExitError
if errors.As(err, &ee) && ee.ExitCode() >= 0 {
return ee.ExitCode()
}
return 255
}