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:
1 parent
114c6fc530
commit
10ffa393ca
3 files changed
+54
-7
No files matched your search
+5
-1
@@ -38,7 +38,11 @@ func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if err := agent.New(cfg).Run(ctx); err != nil {
|
||||
err := agent.New(cfg).Run(ctx)
|
||||
// Stopping (an update, a restart): hang the terminals up, so their shells
|
||||
// save their history instead of being killed.
|
||||
agent.CloseTerminals()
|
||||
if err != nil {
|
||||
log.Fatalf("agent: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
@@ -12,6 +15,27 @@ import (
|
||||
type PTY struct {
|
||||
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()
|
||||
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
|
||||
}
|
||||
_ = p.cmd.Wait()
|
||||
openPTYs.Delete(p)
|
||||
})
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user