blob: c832caa7b5e26b1c559b041793c71f9c1e99f619 [file] [log] [blame]
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -07001//go:build linux
2
3package claudetool
4
5import (
6 "log/slog"
7 "os"
8 "syscall"
9 "time"
10
11 "golang.org/x/sys/unix"
12)
13
14// reapZombies attempts to reap zombie child processes from the specified
15// process group that may have been left behind after a process group cleanup.
16// This is important when running as PID 1 (init process) since no other process
17// will reap zombies.
18//
19// This function reaps zombies until the process group is empty or no more
20// zombies are available.
21func reapZombies(pgid int) {
22 if os.Getpid() != 1 {
23 return // not running as init (e.g. -unsafe), no need to reap
24 }
25 // Quick exit for the common case.
26 if !processGroupHasProcesses(pgid) {
27 return // no processes in the group, nothing to reap
28 }
29
30 // Reap in the background.
31 go func() {
32 maxAttempts := 1000 // shouldn't ever hit this, so be generous, this isn't particularly expensive
33
34 for range maxAttempts {
35 if !processGroupHasProcesses(pgid) {
36 return
37 }
38
39 var wstatus unix.WaitStatus
40 pid, err := unix.Wait4(-pgid, &wstatus, unix.WNOHANG, nil)
41
42 switch err {
43 case syscall.EINTR:
44 // interrupted, retry
45 continue
46 case syscall.ECHILD:
47 // no children, therefore no zombies
48 return
49 case nil:
50 // fall through to handle pid
51 default:
52 slog.Debug("unexpected error in reapZombies", "error", err, "pgid", pgid)
53 return
54 }
55
56 if pid == 0 {
57 // No zombies available right now, wait and check again
58 // There's no great rush, so give it some time.
59 time.Sleep(100 * time.Millisecond)
60 continue
61 }
62
63 slog.Debug("reaped zombie process", "pid", pid, "pgid", pgid, "status", wstatus)
64 }
65 }()
66}