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}