Skip to main content

agentmux_srv/bootstrap/
watchers.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Split out of the single-file bootstrap module unchanged (SPEC_LARGE_FILE_MODULE_ANALYSIS_2026_09_30.md §4.3).
5
6/// Start a ppid polling watchdog on Linux/macOS.
7/// If the parent process dies, getppid() changes (reparented to init/launchd).
8/// This is safer than PR_SET_PDEATHSIG which tracks the parent *thread*, not process,
9/// and can fire spuriously with async runtimes like Tokio.
10#[cfg(any(target_os = "linux", target_os = "macos"))]
11fn start_ppid_watchdog() {
12    let original_ppid = unsafe { libc::getppid() };
13    std::thread::spawn(move || {
14        loop {
15            std::thread::sleep(std::time::Duration::from_secs(2));
16            let current_ppid = unsafe { libc::getppid() };
17            if current_ppid != original_ppid {
18                eprintln_lossy!(
19                    "parent process died (ppid changed {} -> {}), shutting down",
20                    original_ppid, current_ppid
21                );
22                std::process::exit(0);
23            }
24        }
25    });
26}
27
28/// Event-driven parent process watcher using kqueue (macOS) or pidfd (Linux).
29/// Monitors a specific PID and exits when that process terminates.
30/// Falls back to PPID polling on older Linux kernels without pidfd support.
31#[cfg(target_os = "macos")]
32fn start_parent_watcher(parent_pid: u32) {
33    std::thread::spawn(move || {
34        unsafe {
35            let kq = libc::kqueue();
36            if kq < 0 {
37                eprintln_lossy!(
38                    "kqueue() failed (errno={}), falling back to ppid watchdog",
39                    *libc::__error()
40                );
41                let _ = kq;
42                start_ppid_watchdog();
43                return;
44            }
45
46            // Register EVFILT_PROC + NOTE_EXIT on the parent PID.
47            let mut changelist: [libc::kevent; 1] = std::mem::zeroed();
48            changelist[0] = libc::kevent {
49                ident: parent_pid as usize,
50                filter: libc::EVFILT_PROC,
51                flags: libc::EV_ADD | libc::EV_ONESHOT,
52                fflags: libc::NOTE_EXIT,
53                data: 0,
54                udata: std::ptr::null_mut(),
55            };
56
57            let ret = libc::kevent(
58                kq,
59                changelist.as_ptr(),
60                1,
61                std::ptr::null_mut(),
62                0,
63                std::ptr::null(),
64            );
65
66            if ret < 0 {
67                let errno = *libc::__error();
68                libc::close(kq);
69                if errno == libc::ESRCH {
70                    // Parent already dead
71                    eprintln_lossy!(
72                        "parent process {} already exited (ESRCH during kqueue registration), shutting down",
73                        parent_pid
74                    );
75                    std::process::exit(0);
76                }
77                eprintln_lossy!(
78                    "kevent() registration failed (errno={}), falling back to ppid watchdog",
79                    errno
80                );
81                start_ppid_watchdog();
82                return;
83            }
84
85            eprintln_lossy!("kqueue EVFILT_PROC registered for parent pid {}", parent_pid);
86
87            // Race condition guard: check if the parent is still alive after registering.
88            // If it died between our registration and this check, we might miss the event.
89            if libc::kill(parent_pid as i32, 0) != 0 && *libc::__error() == libc::ESRCH {
90                libc::close(kq);
91                eprintln_lossy!(
92                    "parent process {} already exited (post-registration check), shutting down",
93                    parent_pid
94                );
95                std::process::exit(0);
96            }
97
98            // Block until the parent exits.
99            let mut eventlist: [libc::kevent; 1] = std::mem::zeroed();
100            let n = libc::kevent(
101                kq,
102                std::ptr::null(),
103                0,
104                eventlist.as_mut_ptr(),
105                1,
106                std::ptr::null(),
107            );
108            libc::close(kq);
109
110            if n > 0 {
111                eprintln_lossy!(
112                    "parent process {} exited (kqueue EVFILT_PROC), shutting down",
113                    parent_pid
114                );
115            } else {
116                eprintln_lossy!(
117                    "kevent() wait returned {} (errno={}), shutting down",
118                    n,
119                    *libc::__error()
120                );
121            }
122            std::process::exit(0);
123        }
124    });
125}
126
127/// Event-driven parent process watcher using pidfd_open (Linux 5.3+).
128/// Falls back to PPID polling on older kernels without pidfd support.
129#[cfg(target_os = "linux")]
130fn start_parent_watcher(parent_pid: u32) {
131    std::thread::spawn(move || {
132        unsafe {
133            // Try pidfd_open (syscall 434 on x86_64, 434 on aarch64)
134            let pidfd = libc::syscall(libc::SYS_pidfd_open, parent_pid as libc::c_int, 0 as libc::c_int);
135
136            if pidfd < 0 {
137                let errno = *libc::__errno_location();
138                if errno == libc::ESRCH {
139                    // Parent already dead
140                    eprintln_lossy!(
141                        "parent process {} already exited (ESRCH from pidfd_open), shutting down",
142                        parent_pid
143                    );
144                    std::process::exit(0);
145                }
146                // ENOSYS means kernel doesn't support pidfd_open — fall back
147                eprintln_lossy!(
148                    "pidfd_open() failed (errno={}), falling back to ppid watchdog",
149                    errno
150                );
151                start_ppid_watchdog();
152                return;
153            }
154
155            let pidfd = pidfd as libc::c_int;
156
157            // Race condition guard: verify parent is still alive
158            if libc::kill(parent_pid as i32, 0) != 0 && *libc::__errno_location() == libc::ESRCH {
159                libc::close(pidfd);
160                eprintln_lossy!(
161                    "parent process {} already exited (post-pidfd check), shutting down",
162                    parent_pid
163                );
164                std::process::exit(0);
165            }
166
167            // poll() on the pidfd — blocks until the process exits
168            let mut pfd = libc::pollfd {
169                fd: pidfd,
170                events: libc::POLLIN,
171                revents: 0,
172            };
173
174            let ret = libc::poll(&mut pfd, 1, -1); // infinite timeout
175            libc::close(pidfd);
176
177            if ret > 0 {
178                eprintln_lossy!(
179                    "parent process {} exited (pidfd poll), shutting down",
180                    parent_pid
181                );
182            } else {
183                eprintln_lossy!(
184                    "poll() on pidfd returned {} (errno={}), shutting down",
185                    ret,
186                    *libc::__errno_location()
187                );
188            }
189            std::process::exit(0);
190        }
191    });
192}
193
194/// Step -1: Crash monitor branch — must be checked before any other initialization.
195/// The monitor process runs a blocking minidumper::Server and exits when the
196/// main process disconnects. It does not run any backend logic.
197///
198/// Returns `true` if this process was the crash-monitor branch (in which case
199/// `main()` must return immediately without doing anything else).
200pub fn maybe_run_crash_monitor() -> bool {
201    #[cfg(windows)]
202    {
203        if std::env::args().any(|a| a == "--crash-monitor") {
204            crate::crash_monitor::run_monitor();
205            return true;
206        }
207    }
208    false
209}
210
211/// Step 0: Start parent process watcher BEFORE tokio runtime does real work (Linux/macOS only).
212/// On Windows, the frontend uses a Job Object with KILL_ON_JOB_CLOSE instead.
213/// Uses getppid() to get the parent PID, then kqueue/pidfd to watch it (event-driven,
214/// zero CPU). Falls back to PPID polling if kqueue/pidfd setup fails or parent is init/launchd.
215pub fn install_process_watchers() {
216    #[cfg(any(target_os = "linux", target_os = "macos"))]
217    {
218        let ppid = unsafe { libc::getppid() } as u32;
219        if ppid <= 1 {
220            // Parent is init/launchd — can't meaningfully watch it, use polling fallback
221            start_ppid_watchdog();
222        } else {
223            start_parent_watcher(ppid);
224        }
225    }
226}
227
228/// Step 0b: Attach out-of-process crash dump handler (Windows only).
229/// Spawns self with --crash-monitor and installs a VEH handler.
230/// The returned guard must stay alive — dropping it uninstalls the VEH handler.
231/// Non-fatal: if the monitor fails to start, the process continues normally
232/// and WER LocalDumps still captures __fastfail crashes independently.
233#[cfg(windows)]
234pub fn install_crash_guard() -> Option<crate::crash_monitor::CrashHandlerGuard> {
235    crate::crash_monitor::spawn_and_attach()
236}