ur_taking_me_with_you/lib.rs
1//! Ensure child processes die when their parent dies.
2//!
3//! Child processes normally continue running even after their parent exits (on Unix they get
4//! reparented to init/PID 1). This crate provides mechanisms to ensure child processes are
5//! terminated when their parent dies, even if the parent is killed with SIGKILL.
6//!
7//! ## Platform Support
8//!
9//! - **Linux**: Uses `prctl(PR_SET_PDEATHSIG, SIGKILL)` - the child receives SIGKILL when its
10//! parent thread dies
11//! - **macOS**: Uses a pipe-based approach - the child monitors a pipe from the parent and exits
12//! when the pipe closes (indicating parent death)
13//! - **Windows**: Uses job objects with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` - all processes in
14//! the job are terminated when the last handle closes
15//!
16//! ## Usage
17//!
18//! ### For the child process (call early in main):
19//!
20//! ```rust
21//! ur_taking_me_with_you::die_with_parent();
22//! ```
23//!
24//! ### For spawning children with std::process::Command:
25//!
26//! ```rust,no_run
27//! use std::process::Command;
28//!
29//! let mut cmd = Command::new("my-plugin");
30//! cmd.arg("--foo");
31//!
32//! let child = ur_taking_me_with_you::spawn_dying_with_parent(cmd)
33//! .expect("failed to spawn");
34//! ```
35
36// This crate requires unsafe for platform-specific FFI (libc calls for pipe/process management)
37#![allow(unsafe_code)]
38
39#[cfg(target_os = "linux")]
40mod linux;
41
42#[cfg(target_os = "macos")]
43mod macos;
44
45#[cfg(target_os = "windows")]
46mod windows;
47
48#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
49mod unsupported;
50
51use std::io;
52use std::process::{Child, Command};
53
54/// Configure the current process to die when its parent dies.
55///
56/// This should be called early in the child process's main function.
57///
58/// # Platform Behavior
59///
60/// - **Linux**: Calls `prctl(PR_SET_PDEATHSIG, SIGKILL)`. The process will receive
61/// SIGKILL when its parent thread terminates.
62/// - **macOS**: Checks for a death-watch pipe passed via environment variable and
63/// starts a watchdog thread if present. Use `spawn_dying_with_parent` to set this up.
64/// - **Windows**: Creates a job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` and
65/// assigns the current process to it.
66/// - **Other platforms**: No-op with a warning.
67///
68/// # Example
69///
70/// ```no_run
71/// ur_taking_me_with_you::die_with_parent();
72/// // ... rest of plugin code
73/// ```
74pub fn die_with_parent() {
75 #[cfg(target_os = "linux")]
76 linux::die_with_parent();
77
78 #[cfg(target_os = "macos")]
79 macos::die_with_parent();
80
81 #[cfg(target_os = "windows")]
82 windows::die_with_parent();
83
84 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
85 unsupported::die_with_parent();
86}
87
88/// Spawn a child process that will die when this (parent) process dies.
89///
90/// This wraps `Command::spawn()` with platform-specific setup to ensure the child
91/// is terminated when the parent exits, even if the parent is killed with SIGKILL.
92///
93/// # Platform Behavior
94///
95/// - **Linux**: Uses `pre_exec` to call `prctl(PR_SET_PDEATHSIG, SIGKILL)` in the
96/// child before exec.
97/// - **macOS**: Creates a pipe and passes the read end to the child via environment.
98/// The child must call `die_with_parent()` to start the watchdog thread.
99/// - **Windows**: Creates a job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` and
100/// assigns the child process to it. Note: small race window between spawn and assignment.
101///
102/// # Example
103///
104/// ```no_run
105/// use std::process::Command;
106///
107/// let mut cmd = Command::new("my-plugin");
108/// cmd.arg("--config").arg("/path/to/config");
109///
110/// let child = ur_taking_me_with_you::spawn_dying_with_parent(cmd)
111/// .expect("failed to spawn plugin");
112/// ```
113pub fn spawn_dying_with_parent(command: Command) -> io::Result<Child> {
114 #[cfg(target_os = "linux")]
115 return linux::spawn_dying_with_parent(command);
116
117 #[cfg(target_os = "macos")]
118 return macos::spawn_dying_with_parent(command);
119
120 #[cfg(target_os = "windows")]
121 return windows::spawn_dying_with_parent(command);
122
123 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
124 return unsupported::spawn_dying_with_parent(command);
125}
126
127/// Environment variable name used on macOS to pass the death-watch pipe FD.
128#[cfg(target_os = "macos")]
129pub const DEATH_PIPE_ENV: &str = "UR_TAKING_ME_WITH_YOU_FD";
130
131/// Spawn a child process (async) that will die when this (parent) process dies.
132///
133/// Same as `spawn_dying_with_parent` but takes a `tokio::process::Command` and
134/// returns a `tokio::process::Child` with async `wait()`.
135#[cfg(feature = "tokio")]
136pub fn spawn_dying_with_parent_async(
137 command: tokio::process::Command,
138) -> io::Result<tokio::process::Child> {
139 #[cfg(target_os = "linux")]
140 return linux::spawn_dying_with_parent_async(command);
141
142 #[cfg(target_os = "macos")]
143 return macos::spawn_dying_with_parent_async(command);
144
145 #[cfg(target_os = "windows")]
146 return windows::spawn_dying_with_parent_async(command);
147
148 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
149 return unsupported::spawn_dying_with_parent_async(command);
150}