running_process/broker/lifecycle/
process_tree.rs1use std::{io, time::Duration};
8
9use crate::platform::process::{OwnerDeathCleanup, OwnerDeathCleanupError, OwnerDeathCleanupStage};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ProcessTreeCleanup {
15 LinuxParentDeathSignal,
17 WindowsKillOnJobClose,
19 WindowsAlreadyInJob,
21 MacosKqueueSupervisorContract,
23 UnsupportedNoop,
25}
26
27pub const MACOS_SUPERVISOR_KILL_DEADLINE: Duration = Duration::from_secs(5);
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct MacosSupervisorContract {
33 pub watch_pid: MacosSupervisorWatchPid,
35 pub kqueue_filter: MacosKqueueFilter,
37 pub kqueue_note: MacosKqueueNote,
39 pub registration_barrier: MacosSupervisorRegistrationBarrier,
41 pub race_guard: MacosSupervisorRaceGuard,
43 pub exit_action: MacosSupervisorExitAction,
45 pub kill_deadline: Duration,
47}
48
49impl MacosSupervisorContract {
50 pub const fn phase5() -> Self {
52 Self {
53 watch_pid: MacosSupervisorWatchPid::BrokerParent,
54 kqueue_filter: MacosKqueueFilter::Process,
55 kqueue_note: MacosKqueueNote::Exit,
56 registration_barrier: MacosSupervisorRegistrationBarrier::BeforeBackendPipePublication,
57 race_guard: MacosSupervisorRaceGuard::RecheckBrokerAliveAfterRegistration,
58 exit_action: MacosSupervisorExitAction::SigkillBackend,
59 kill_deadline: MACOS_SUPERVISOR_KILL_DEADLINE,
60 }
61 }
62
63 pub const fn kqueue_filter_name(&self) -> &'static str {
65 match self.kqueue_filter {
66 MacosKqueueFilter::Process => "EVFILT_PROC",
67 }
68 }
69
70 pub const fn kqueue_note_name(&self) -> &'static str {
72 match self.kqueue_note {
73 MacosKqueueNote::Exit => "NOTE_EXIT",
74 }
75 }
76
77 pub const fn termination_signal_name(&self) -> &'static str {
79 match self.exit_action {
80 MacosSupervisorExitAction::SigkillBackend => "SIGKILL",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum MacosSupervisorWatchPid {
88 BrokerParent,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum MacosKqueueFilter {
95 Process,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum MacosKqueueNote {
102 Exit,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum MacosSupervisorRegistrationBarrier {
109 BeforeBackendPipePublication,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum MacosSupervisorRaceGuard {
116 RecheckBrokerAliveAfterRegistration,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum MacosSupervisorExitAction {
123 SigkillBackend,
125}
126
127pub const fn macos_supervisor_contract() -> MacosSupervisorContract {
129 MacosSupervisorContract::phase5()
130}
131
132#[derive(Debug, thiserror::Error)]
134pub enum ProcessTreeError {
135 #[error("failed to install Linux parent-death signal: {0}")]
137 LinuxParentDeathSignal(io::Error),
138 #[error("failed to create Windows kill-on-close Job Object: {0}")]
140 WindowsJobCreate(io::Error),
141 #[error("failed to assign broker process to Windows Job Object: {0}")]
143 WindowsJobAssign(io::Error),
144}
145
146pub fn install_cleanup() -> Result<ProcessTreeCleanup, ProcessTreeError> {
157 crate::platform::process::install_owner_death_cleanup()
158 .map(from_facade)
159 .map_err(from_facade_error)
160}
161
162pub fn cleanup_target() -> ProcessTreeCleanup {
164 from_facade(crate::platform::process::owner_death_cleanup_target())
165}
166
167fn from_facade(cleanup: OwnerDeathCleanup) -> ProcessTreeCleanup {
175 match cleanup {
176 OwnerDeathCleanup::OwnerDeathSignal => ProcessTreeCleanup::LinuxParentDeathSignal,
177 OwnerDeathCleanup::KillOnOwnerHandleClose => ProcessTreeCleanup::WindowsKillOnJobClose,
178 OwnerDeathCleanup::AlreadyContained => ProcessTreeCleanup::WindowsAlreadyInJob,
179 OwnerDeathCleanup::SupervisorRequired => ProcessTreeCleanup::MacosKqueueSupervisorContract,
180 OwnerDeathCleanup::Unsupported => ProcessTreeCleanup::UnsupportedNoop,
181 }
182}
183
184fn from_facade_error(error: OwnerDeathCleanupError) -> ProcessTreeError {
191 match error.stage {
192 OwnerDeathCleanupStage::RequestSignal => {
193 ProcessTreeError::LinuxParentDeathSignal(error.source)
194 }
195 OwnerDeathCleanupStage::CreateContainer => ProcessTreeError::WindowsJobCreate(error.source),
196 OwnerDeathCleanupStage::JoinContainer => ProcessTreeError::WindowsJobAssign(error.source),
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
212 fn cleanup_target_model_states_phase_5_platform_contracts() {
213 for (reported, expected) in [
214 (
215 OwnerDeathCleanup::OwnerDeathSignal,
216 ProcessTreeCleanup::LinuxParentDeathSignal,
217 ),
218 (
219 OwnerDeathCleanup::KillOnOwnerHandleClose,
220 ProcessTreeCleanup::WindowsKillOnJobClose,
221 ),
222 (
223 OwnerDeathCleanup::AlreadyContained,
224 ProcessTreeCleanup::WindowsAlreadyInJob,
225 ),
226 (
227 OwnerDeathCleanup::SupervisorRequired,
228 ProcessTreeCleanup::MacosKqueueSupervisorContract,
229 ),
230 (
231 OwnerDeathCleanup::Unsupported,
232 ProcessTreeCleanup::UnsupportedNoop,
233 ),
234 ] {
235 assert_eq!(from_facade(reported), expected, "{reported:?}");
236 }
237 }
238
239 #[test]
245 fn a_failure_keeps_the_step_it_failed_at() {
246 use crate::platform::process::OwnerDeathCleanupStage;
247
248 let staged = |stage| OwnerDeathCleanupError {
249 stage,
250 source: io::Error::from_raw_os_error(5),
251 };
252 assert!(matches!(
253 from_facade_error(staged(OwnerDeathCleanupStage::RequestSignal)),
254 ProcessTreeError::LinuxParentDeathSignal(_)
255 ));
256 assert!(matches!(
257 from_facade_error(staged(OwnerDeathCleanupStage::CreateContainer)),
258 ProcessTreeError::WindowsJobCreate(_)
259 ));
260 assert!(matches!(
261 from_facade_error(staged(OwnerDeathCleanupStage::JoinContainer)),
262 ProcessTreeError::WindowsJobAssign(_)
263 ));
264 }
265
266 #[test]
274 fn cleanup_target_is_explicit_for_current_platform() {
275 let target = cleanup_target();
276 assert_ne!(
277 target,
278 ProcessTreeCleanup::UnsupportedNoop,
279 "every shipped host names a contract; UnsupportedNoop means one was not taught"
280 );
281 assert_eq!(
282 target,
283 install_cleanup().expect("installing must succeed where a target is claimed"),
284 "the target must be what installing actually reports"
285 );
286 }
287
288 #[test]
289 fn macos_supervisor_contract_pins_phase_5_cleanup_requirements() {
290 let contract = macos_supervisor_contract();
291
292 assert_eq!(contract.watch_pid, MacosSupervisorWatchPid::BrokerParent);
293 assert_eq!(contract.kqueue_filter_name(), "EVFILT_PROC");
294 assert_eq!(contract.kqueue_note_name(), "NOTE_EXIT");
295 assert_eq!(
296 contract.registration_barrier,
297 MacosSupervisorRegistrationBarrier::BeforeBackendPipePublication
298 );
299 assert_eq!(
300 contract.race_guard,
301 MacosSupervisorRaceGuard::RecheckBrokerAliveAfterRegistration
302 );
303 assert_eq!(contract.termination_signal_name(), "SIGKILL");
304 assert_eq!(contract.kill_deadline, Duration::from_secs(5));
305 }
306}