fix(handoff): prevent race condition when killing pane processes

KillPaneProcesses was killing ALL processes in the pane, including the
gt handoff process itself. This created a race condition where the
process could be killed before RespawnPane executes, causing the pane
to close prematurely and requiring manual reattach.

Added KillPaneProcessesExcluding() function that excludes specified PIDs
from being killed. The handoff command now passes its own PID to avoid
the race condition.

Fixes: gt-85qd

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
diesel
2026-01-23 15:39:00 -08:00
committed by John Ogle
parent 4c5f0f4e11
commit 4d83d79da9

View File

@@ -447,40 +447,57 @@ func (t *Tmux) KillPaneProcessesExcluding(pane string, excludePIDs []string) err
return fmt.Errorf("pane PID is empty")
}
// Get all descendant PIDs recursively (returns deepest-first order)
descendants := getAllDescendants(pid)
// Collect PIDs to kill (excluding specified ones)
toKill := make(map[string]bool)
// Filter out excluded PIDs
var filtered []string
for _, dpid := range descendants {
if !exclude[dpid] {
filtered = append(filtered, dpid)
// First, collect process group members (catches reparented processes)
pgid := getProcessGroupID(pid)
if pgid != "" && pgid != "0" && pgid != "1" {
for _, member := range getProcessGroupMembers(pgid) {
if !exclude[member] {
toKill[member] = true
}
}
}
// Send SIGTERM to all non-excluded descendants (deepest first to avoid orphaning)
for _, dpid := range filtered {
// Also walk the process tree for any descendants that might have called setsid()
descendants := getAllDescendants(pid)
for _, dpid := range descendants {
if !exclude[dpid] {
toKill[dpid] = true
}
}
// Convert to slice for iteration
var killList []string
for dpid := range toKill {
killList = append(killList, dpid)
}
// Send SIGTERM to all non-excluded processes
for _, dpid := range killList {
_ = exec.Command("kill", "-TERM", dpid).Run()
}
// Wait for graceful shutdown
time.Sleep(100 * time.Millisecond)
// Wait for graceful shutdown (2s gives processes time to clean up)
time.Sleep(processKillGracePeriod)
// Send SIGKILL to any remaining non-excluded descendants
for _, dpid := range filtered {
// Send SIGKILL to any remaining non-excluded processes
for _, dpid := range killList {
_ = exec.Command("kill", "-KILL", dpid).Run()
}
// Kill the pane process itself only if not excluded
if !exclude[pid] {
_ = exec.Command("kill", "-TERM", pid).Run()
time.Sleep(100 * time.Millisecond)
time.Sleep(processKillGracePeriod)
_ = exec.Command("kill", "-KILL", pid).Run()
}
return nil
}
// KillServer terminates the entire tmux server and all sessions.
func (t *Tmux) KillServer() error {
_, err := t.run("kill-server")