systemprompt_models/
subprocess.rs1use std::process::Command;
35use std::sync::OnceLock;
36use std::sync::mpsc::{Sender, channel};
37
38pub const SUBPROCESS_MARKER_ENV: &str = "SYSTEMPROMPT_SUBPROCESS";
39pub const AGENT_NAME_ENV: &str = "AGENT_NAME";
40pub const MCP_SERVICE_ID_ENV: &str = "MCP_SERVICE_ID";
41
42type SpawnReply = Sender<std::io::Result<u32>>;
43
44pub fn spawn_supervised(cmd: Command) -> std::io::Result<u32> {
45 let sender = spawner()
46 .as_ref()
47 .map_err(|e| std::io::Error::other(e.clone()))?;
48
49 let (reply_tx, reply_rx) = channel();
50 sender
51 .send((cmd, reply_tx))
52 .map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?;
53 reply_rx
54 .recv()
55 .map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?
56}
57
58fn spawner() -> &'static Result<Sender<(Command, SpawnReply)>, String> {
59 static SPAWNER: OnceLock<Result<Sender<(Command, SpawnReply)>, String>> = OnceLock::new();
60 SPAWNER.get_or_init(|| {
61 let (tx, rx) = channel::<(Command, SpawnReply)>();
62 std::thread::Builder::new()
63 .name("subprocess-spawner".to_owned())
64 .spawn(move || {
65 while let Ok((mut cmd, reply)) = rx.recv() {
66 let outcome = spawn_on_this_thread(&mut cmd);
67 if reply.send(outcome).is_err() {
68 tracing::warn!(
69 "Spawn requester vanished before collecting the child pid; the child \
70 is unregistered and will only be cleaned up by its parent-death signal"
71 );
72 }
73 }
74 })
75 .map(|_handle| tx)
76 .map_err(|e| format!("could not start the subprocess spawner thread: {e}"))
77 })
78}
79
80fn spawn_on_this_thread(cmd: &mut Command) -> std::io::Result<u32> {
81 #[cfg(target_os = "linux")]
82 arm_parent_death_signal(cmd);
83
84 let child = cmd.spawn()?;
85 let pid = child.id();
86 #[expect(
87 clippy::mem_forget,
88 reason = "detached child: skip Child's drop-time wait so it keeps running after this \
89 returns; reaping is the caller's business via is_zombie"
90 )]
91 std::mem::forget(child);
92 Ok(pid)
93}
94
95#[cfg(target_os = "linux")]
96#[expect(
97 unsafe_code,
98 reason = "std::os::unix::process::CommandExt::pre_exec is an unsafe fn; there is no safe way \
99 to run code in the forked child before exec, and the parent-death signal must be \
100 armed there to cover children that never opt in"
101)]
102fn arm_parent_death_signal(cmd: &mut Command) {
103 use std::os::unix::process::CommandExt;
104
105 let supervisor = std::process::id();
106
107 unsafe {
111 cmd.pre_exec(move || {
112 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 {
113 return Err(std::io::Error::last_os_error());
114 }
115 if libc::getppid() != supervisor as libc::pid_t {
120 libc::_exit(0);
121 }
122 Ok(())
123 });
124 }
125}
126
127#[must_use]
128pub fn signalable_pid(pid: u32) -> Option<i32> {
129 if pid == 0 {
130 return None;
131 }
132 i32::try_from(pid).ok()
133}
134
135#[must_use]
136pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
137 let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
138 let expected_name = format!("{name_key}={service_name}");
139
140 let mut has_marker = false;
141 let mut has_name = false;
142 for entry in environ.split(|&b| b == 0) {
143 if entry == marker.as_bytes() {
144 has_marker = true;
145 } else if entry == expected_name.as_bytes() {
146 has_name = true;
147 }
148 }
149
150 has_marker && has_name
151}
152
153#[cfg(target_os = "linux")]
154#[must_use]
155pub fn live_pid_is_subprocess(pid: u32, name_key: &str, service_name: &str) -> bool {
156 match std::fs::read(format!("/proc/{pid}/environ")) {
157 Ok(environ) => environ_identifies_child(&environ, name_key, service_name),
158 Err(e) => {
159 tracing::warn!(pid, error = %e, "Could not read process environ to verify child identity");
160 false
161 },
162 }
163}
164
165#[cfg(not(target_os = "linux"))]
166#[must_use]
167pub fn live_pid_is_subprocess(pid: u32, _name_key: &str, service_name: &str) -> bool {
168 tracing::warn!(
169 pid,
170 service = %service_name,
171 "Child identity cannot be verified on this platform (no /proc), so this process will \
172 not be signalled; supervision is Linux-only and the child must be stopped by hand"
173 );
174 false
175}
176
177#[cfg(target_os = "linux")]
178#[must_use]
179pub fn is_zombie(pid: u32) -> bool {
180 let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
181 return false;
182 };
183 let Some((_, after_comm)) = stat.rsplit_once(')') else {
186 return false;
187 };
188 after_comm.split_whitespace().next() == Some("Z")
189}
190
191#[cfg(not(target_os = "linux"))]
192#[must_use]
193pub fn is_zombie(_pid: u32) -> bool {
194 false
195}