Fix sshExecScript hang: explicit exit instead of relying on PTY stdin EOF
Root cause of the "script finishes, process never returns" hangs (ci runs #56 and #60, and a direct local reproduction): SSH channel EOF just tells the server "not sending more data right now" -- for a plain exec channel that maps onto closing the child's stdin pipe (clean, unconditional EOF on read()), but for a PTY-backed session the server is managing an interactive terminal, where a pause in input isn't supposed to mean "hang up". It doesn't reliably translate into an EOF condition the remote bash's read() ever sees, so bash just waits forever for a keystroke that never comes. This held true even after switching to Start()+StdinPipe()+explicit Close() (the fix that did work for uploadFile, which uses a plain non-PTY session) -- ruling out Run()'s implicit copying as the cause. Confirmed independently outside this codebase: golang/go#6088 (the exact same x/crypto/ssh symptom) and net-ssh/net-ssh#460 (different language, same symptom, and confirms removing request_pty fixes it) -- this is a general PTY/SSH-protocol interaction, not a library bug. Fix: append `exit $?` to the script fed to bash -s, so it terminates the instant the last command finishes, independent of whatever the PTY does or doesn't do with EOF. Verified against a real box with a script that prints its own completion timestamp then keeps running unless killed -- local process now returns within seconds of the remote script finishing instead of hanging indefinitely.
This commit is contained in:
parent
bf8e5f6f13
commit
c3e94b7484
1 changed files with 40 additions and 2 deletions
42
main.go
42
main.go
|
|
@ -467,6 +467,23 @@ func uploadFile(st *state, localPath, remotePath string) error {
|
|||
// `export` lines for env. Values are forwarded only over the SSH channel's
|
||||
// stdin — never as a command-line argument on either end — so they don't
|
||||
// show up in `ps` output on the box or in this process's argv.
|
||||
// sshExecScript pipes script to `bash -s` over a PTY (needed so the remote
|
||||
// side line-buffers instead of block-buffering -- see sshSession).
|
||||
//
|
||||
// The script ends with an explicit `exit $?` rather than relying on EOF to
|
||||
// end bash's read loop. Confirmed in production (ci runs #56 and #60,
|
||||
// plus a direct reproduction: a script that finishes and prints its own
|
||||
// "end" timestamp, after which the local process sits for 10+ more
|
||||
// minutes) that PTY stdin EOF is not a reliable "hang up" signal the way
|
||||
// it is on a plain pipe -- a PTY is fundamentally for interactive
|
||||
// sessions, where "stopped sending input" doesn't inherently mean
|
||||
// "terminate". This held true even after switching to
|
||||
// Start()+StdinPipe()+explicit Close() (the fix that *did* work for
|
||||
// uploadFile, which uses a plain, non-PTY session) -- ruling out
|
||||
// Run()'s implicit copying as the cause and pointing at the PTY itself.
|
||||
// An explicit `exit` sidesteps the ambiguity entirely: bash terminates on
|
||||
// its own the instant the script's last command finishes, independent of
|
||||
// whatever the PTY does or doesn't do with EOF.
|
||||
func sshExecScript(st *state, script string, env map[string]string) (int, error) {
|
||||
session, client, err := sshSession(st, true)
|
||||
if err != nil {
|
||||
|
|
@ -480,11 +497,32 @@ func sshExecScript(st *state, script string, env map[string]string) (int, error)
|
|||
fmt.Fprintf(&buf, "export %s=%s\n", k, shellQuote(v))
|
||||
}
|
||||
buf.WriteString(script)
|
||||
buf.WriteString("\nexit $?\n")
|
||||
|
||||
session.Stdin = strings.NewReader(buf.String())
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
session.Stdout = os.Stdout
|
||||
session.Stderr = os.Stderr
|
||||
return runSession(session, "bash -s")
|
||||
|
||||
if err := session.Start("bash -s"); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if _, err := stdin.Write([]byte(buf.String())); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if err := session.Wait(); err != nil {
|
||||
if exitErr, ok := err.(*ssh.ExitError); ok {
|
||||
return exitErr.ExitStatus(), nil
|
||||
}
|
||||
return -1, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func shellQuote(v string) string {
|
||||
|
|
|
|||
Loading…
Reference in a new issue