fix(tmux): kill process group to prevent orphaned processes
KillSession was leaving orphaned Claude/node processes because pgrep -P only finds direct children. Processes that reparent to init (PID 1) were missed. Changes: - Kill entire process group first using kill -TERM/-KILL -<pgid> - Add getProcessGroupID() and getProcessGroupMembers() helpers - Update KillSessionWithProcesses, KillSessionWithProcessesExcluding, and KillPaneProcesses to use process group killing - Fix EnsureSessionFresh to use KillSessionWithProcesses instead of basic KillSession Fixes: gt-w1dcvq Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package tmux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func hasTmux() bool {
|
||||
@@ -705,6 +708,97 @@ func TestKillSessionWithProcessesExcluding_NonexistentSession(t *testing.T) {
|
||||
_ = err
|
||||
}
|
||||
|
||||
func TestGetProcessGroupID(t *testing.T) {
|
||||
// Test with current process
|
||||
pid := fmt.Sprintf("%d", os.Getpid())
|
||||
pgid := getProcessGroupID(pid)
|
||||
|
||||
if pgid == "" {
|
||||
t.Error("expected non-empty PGID for current process")
|
||||
}
|
||||
|
||||
// PGID should not be 0 or 1 for a normal process
|
||||
if pgid == "0" || pgid == "1" {
|
||||
t.Errorf("unexpected PGID %q for current process", pgid)
|
||||
}
|
||||
|
||||
// Test with nonexistent PID
|
||||
pgid = getProcessGroupID("999999999")
|
||||
if pgid != "" {
|
||||
t.Errorf("expected empty PGID for nonexistent process, got %q", pgid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProcessGroupMembers(t *testing.T) {
|
||||
// Get current process's PGID
|
||||
pid := fmt.Sprintf("%d", os.Getpid())
|
||||
pgid := getProcessGroupID(pid)
|
||||
if pgid == "" {
|
||||
t.Skip("could not get PGID for current process")
|
||||
}
|
||||
|
||||
members := getProcessGroupMembers(pgid)
|
||||
|
||||
// Current process should be in the list
|
||||
found := false
|
||||
for _, m := range members {
|
||||
if m == pid {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("current process %s not found in process group %s members: %v", pid, pgid, members)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKillSessionWithProcesses_KillsProcessGroup(t *testing.T) {
|
||||
if !hasTmux() {
|
||||
t.Skip("tmux not installed")
|
||||
}
|
||||
|
||||
tm := NewTmux()
|
||||
sessionName := "gt-test-killpg-" + t.Name()
|
||||
|
||||
// Clean up any existing session
|
||||
_ = tm.KillSession(sessionName)
|
||||
|
||||
// Create session that spawns a child process
|
||||
// The child will stay in the same process group as the shell
|
||||
cmd := `sleep 300 & sleep 300`
|
||||
if err := tm.NewSessionWithCommand(sessionName, "", cmd); err != nil {
|
||||
t.Fatalf("NewSessionWithCommand: %v", err)
|
||||
}
|
||||
|
||||
// Give processes time to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Verify session exists
|
||||
has, err := tm.HasSession(sessionName)
|
||||
if err != nil {
|
||||
t.Fatalf("HasSession: %v", err)
|
||||
}
|
||||
if !has {
|
||||
t.Fatal("expected session to exist after creation")
|
||||
}
|
||||
|
||||
// Kill with processes (should kill the entire process group)
|
||||
if err := tm.KillSessionWithProcesses(sessionName); err != nil {
|
||||
t.Fatalf("KillSessionWithProcesses: %v", err)
|
||||
}
|
||||
|
||||
// Verify session is gone
|
||||
has, err = tm.HasSession(sessionName)
|
||||
if err != nil {
|
||||
t.Fatalf("HasSession after kill: %v", err)
|
||||
}
|
||||
if has {
|
||||
t.Error("expected session to not exist after KillSessionWithProcesses")
|
||||
_ = tm.KillSession(sessionName) // cleanup
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionSet(t *testing.T) {
|
||||
if !hasTmux() {
|
||||
t.Skip("tmux not installed")
|
||||
|
||||
Reference in New Issue
Block a user