Fix exit not closing session

This commit is contained in:
Owen
2026-05-21 20:38:38 -07:00
parent 9640ada8b7
commit f217ddf6a7
2 changed files with 40 additions and 3 deletions
+30 -3
View File
@@ -1,9 +1,11 @@
package nativessh package nativessh
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"sync"
"github.com/creack/pty" "github.com/creack/pty"
) )
@@ -11,8 +13,10 @@ import (
// PTYSession is a running shell process attached to a PTY. // PTYSession is a running shell process attached to a PTY.
// It implements io.ReadWriteCloser so it can be bridged to any transport. // It implements io.ReadWriteCloser so it can be bridged to any transport.
type PTYSession struct { type PTYSession struct {
ptmx *os.File ptmx *os.File
cmd *exec.Cmd cmd *exec.Cmd
waitOnce sync.Once
exitCode int
} }
// findShell returns the path to the best available interactive shell by // findShell returns the path to the best available interactive shell by
@@ -54,9 +58,32 @@ func (p *PTYSession) Resize(cols, rows uint16) error {
return pty.Setsize(p.ptmx, &pty.Winsize{Cols: cols, Rows: rows}) return pty.Setsize(p.ptmx, &pty.Winsize{Cols: cols, Rows: rows})
} }
// wait waits for the child process to exit exactly once and records its exit
// code. Safe to call concurrently or multiple times.
func (p *PTYSession) wait() {
p.waitOnce.Do(func() {
err := p.cmd.Wait()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
p.exitCode = exitErr.ExitCode()
return
}
p.exitCode = 1
}
})
}
// ExitCode waits for the shell process to exit and returns its exit code.
// It is safe to call before or after Close.
func (p *PTYSession) ExitCode() int {
p.wait()
return p.exitCode
}
// Close closes the PTY and waits for the child process to exit. // Close closes the PTY and waits for the child process to exit.
func (p *PTYSession) Close() error { func (p *PTYSession) Close() error {
err := p.ptmx.Close() err := p.ptmx.Close()
_ = p.cmd.Wait() p.wait()
return err return err
} }
+10
View File
@@ -177,8 +177,18 @@ func (s *Server) handleSession(ch ssh.Channel, requests <-chan *ssh.Request) {
// PTY output → SSH channel. // PTY output → SSH channel.
go func() { go func() {
_, _ = io.Copy(ch, sess) _, _ = io.Copy(ch, sess)
// Notify the client of the shell's exit status so it can
// disconnect cleanly instead of requiring a manual disconnect.
exitCode := sess.ExitCode()
exitStatusPayload := ssh.Marshal(struct{ Status uint32 }{uint32(exitCode)})
_, _ = ch.SendRequest("exit-status", false, exitStatusPayload)
_ = ch.CloseWrite() _ = ch.CloseWrite()
sess.Close() //nolint:errcheck sess.Close() //nolint:errcheck
// Close the channel so the ssh library closes the requests
// channel, which unblocks the for-range loop in handleSession
// and allows the deferred ch.Close() to run. Without this,
// handleSession blocks forever waiting for requests to drain.
_ = ch.Close()
}() }()
// SSH channel input → PTY stdin. // SSH channel input → PTY stdin.
go func() { go func() {