package app import ( "syscall" "testing" "time" ) // TestParentExitKillsDescendants verifies that when a parent process // exits, every still-live process that was spawned under it is signaled // and dies, recursively through the tree. func TestParentExitKillsDescendants(t *testing.T) { sess := NewSession(t.TempDir(), "test") // Use a tiny pause-then-trap shell so the children survive long // enough for the parent to die first; SIGTERM should terminate them. sleepArgv := []string{"sh", "-c", "trap 'exit 0' TERM; sleep 30"} parent, err := sess.Spawn(SpawnSpec{ Kind: KindTerminal, Argv: []string{"sh", "-c", "sleep 30"}, }, 80, 24) if err != nil { t.Fatalf("spawn parent: %v", err) } childA, err := sess.Spawn(SpawnSpec{ Kind: KindCommand, Argv: sleepArgv, ParentID: parent.ID, }, 80, 24) if err != nil { t.Fatalf("spawn childA: %v", err) } grandchild, err := sess.Spawn(SpawnSpec{ Kind: KindCommand, Argv: sleepArgv, ParentID: childA.ID, }, 80, 24) if err != nil { t.Fatalf("spawn grandchild: %v", err) } // Wait for everyone to be running. waitUntilLive(t, parent) waitUntilLive(t, childA) waitUntilLive(t, grandchild) // Kill the parent. Its reapChild should cascade to childA, whose // reapChild should in turn cascade to grandchild. if err := parent.signal(syscall.SIGTERM); err != nil { t.Fatalf("signal parent: %v", err) } waitUntilNotLive(t, parent) waitUntilNotLive(t, childA) waitUntilNotLive(t, grandchild) } func waitUntilLive(t *testing.T, c *Child) { t.Helper() deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if c.IsLive() { return } time.Sleep(20 * time.Millisecond) } t.Fatalf("child %s never went live", c.ID) } func waitUntilNotLive(t *testing.T, c *Child) { t.Helper() deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if !c.IsLive() { return } time.Sleep(20 * time.Millisecond) } t.Fatalf("child %s still live after parent died (status=%s)", c.ID, c.Status()) }