fix: a closed terminal hangs its shell up instead of killing it, so the history is saved; idle shells end with their session, and on agent stop

This commit is contained in:
redtea committed 2026-10-14 19:50:50 +03:00
1 parent 114c6fc530
commit 10ffa393ca
3 files changed
+57 -10

No files matched your search

+4
View File
@@ -54,6 +54,10 @@ func (a *Agent) bridgeStream(parent context.Context, sid string, rw io.ReadWrite
ctx, cancel := context.WithCancel(parent)
defer cancel()
defer rw.Close()
// A session closed from the panel (a tab reloaded, a window shut) ends
// the stream at once, not on its next output: an idle shell would live
// on until the agent stopped.
go func() { <-ctx.Done(); rw.Close() }()
// Downstream: pull input frames and write them to the stream.
go func() {
+48 -9
View File
@@ -4,14 +4,38 @@ import (
"fmt"
"os"
"os/exec"
"sync"
"syscall"
"time"
"github.com/creack/pty"
)
// PTY is a pseudo-terminal running an interactive login shell.
type PTY struct {
cmd *exec.Cmd
f *os.File
cmd *exec.Cmd
f *os.File
exited chan struct{} // closed once the shell has exited
once sync.Once
}
// hangupGrace is how long a shell gets after the hangup to save its history
// and exit before it is killed.
const hangupGrace = 3 * time.Second
// openPTYs are the terminals running now, to close on shutdown.
var openPTYs sync.Map // *PTY -> struct{}
// CloseTerminals hangs up every open terminal and waits for them, so the
// shells save their history before the agent exits.
func CloseTerminals() {
var wg sync.WaitGroup
openPTYs.Range(func(k, _ any) bool {
wg.Add(1)
go func() { defer wg.Done(); k.(*PTY).Close() }()
return true
})
wg.Wait()
}
// StartPTY launches a login shell attached to a new PTY. With user empty (or
@@ -56,7 +80,10 @@ func StartPTY(user string) (*PTY, error) {
if err != nil {
return nil, err
}
return &PTY{cmd: cmd, f: f}, nil
p := &PTY{cmd: cmd, f: f, exited: make(chan struct{})}
go func() { _ = cmd.Wait(); close(p.exited) }()
openPTYs.Store(p, struct{}{})
return p, nil
}
func isDir(p string) bool {
@@ -78,12 +105,24 @@ func (p *PTY) Resize(cols, rows uint16) {
_ = pty.Setsize(p.f, &pty.Winsize{Rows: rows, Cols: cols})
}
// Close terminates the shell and releases the PTY.
// Close hangs the terminal up as a closed window would: the shell gets
// SIGHUP, saves its history and exits. One that does not within hangupGrace
// is killed. Safe to call more than once.
func (p *PTY) Close() error {
err := p.f.Close()
if p.cmd.Process != nil {
_ = p.cmd.Process.Kill()
}
_ = p.cmd.Wait()
var err error
p.once.Do(func() {
// Closing the master hangs up the session; the signal to the
// shell's process group makes sure it hears, through su as well.
err = p.f.Close()
pid := p.cmd.Process.Pid
_ = syscall.Kill(-pid, syscall.SIGHUP)
select {
case <-p.exited:
case <-time.After(hangupGrace):
_ = syscall.Kill(-pid, syscall.SIGKILL)
<-p.exited
}
openPTYs.Delete(p)
})
return err
}