systemprompt_models/subprocess/
mod.rs1use std::process::Command;
44use std::sync::OnceLock;
45use std::sync::mpsc::{Sender, channel};
46
47#[cfg(target_os = "linux")]
48mod linux;
49#[cfg(target_os = "linux")]
50pub use linux::{is_zombie, live_pid_is_subprocess};
51
52#[cfg(target_os = "macos")]
53mod darwin;
54#[cfg(target_os = "macos")]
55pub use darwin::{is_zombie, live_pid_is_subprocess};
56
57#[cfg(not(any(target_os = "linux", target_os = "macos")))]
58mod unsupported;
59#[cfg(not(any(target_os = "linux", target_os = "macos")))]
60pub use unsupported::{is_zombie, live_pid_is_subprocess};
61
62pub const SUBPROCESS_MARKER_ENV: &str = "SYSTEMPROMPT_SUBPROCESS";
63pub const AGENT_NAME_ENV: &str = "AGENT_NAME";
64pub const MCP_SERVICE_ID_ENV: &str = "MCP_SERVICE_ID";
65
66type SpawnReply = Sender<std::io::Result<u32>>;
67
68pub fn spawn_supervised(cmd: Command) -> std::io::Result<u32> {
69 let sender = spawner()
70 .as_ref()
71 .map_err(|e| std::io::Error::other(e.clone()))?;
72
73 let (reply_tx, reply_rx) = channel();
74 sender
75 .send((cmd, reply_tx))
76 .map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?;
77 reply_rx
78 .recv()
79 .map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?
80}
81
82fn spawner() -> &'static Result<Sender<(Command, SpawnReply)>, String> {
83 static SPAWNER: OnceLock<Result<Sender<(Command, SpawnReply)>, String>> = OnceLock::new();
84 SPAWNER.get_or_init(|| {
85 let (tx, rx) = channel::<(Command, SpawnReply)>();
86 std::thread::Builder::new()
87 .name("subprocess-spawner".to_owned())
88 .spawn(move || {
89 while let Ok((mut cmd, reply)) = rx.recv() {
90 let outcome = spawn_on_this_thread(&mut cmd);
91 if reply.send(outcome).is_err() {
92 tracing::warn!(
93 "Spawn requester vanished before collecting the child pid; the child \
94 is unregistered and will only be cleaned up by its parent-death signal"
95 );
96 }
97 }
98 })
99 .map(|_handle| tx)
100 .map_err(|e| format!("could not start the subprocess spawner thread: {e}"))
101 })
102}
103
104fn spawn_on_this_thread(cmd: &mut Command) -> std::io::Result<u32> {
105 #[cfg(target_os = "linux")]
106 linux::arm_parent_death_signal(cmd);
107
108 let child = cmd.spawn()?;
109 let pid = child.id();
110 #[expect(
111 clippy::mem_forget,
112 reason = "detached child: skip Child's drop-time wait so it keeps running after this \
113 returns; reaping is the caller's business via is_zombie"
114 )]
115 std::mem::forget(child);
116 Ok(pid)
117}
118
119#[must_use]
120pub const fn identity_verification_supported() -> bool {
121 cfg!(any(target_os = "linux", target_os = "macos"))
122}
123
124#[must_use]
125pub fn signalable_pid(pid: u32) -> Option<i32> {
126 if pid == 0 {
127 return None;
128 }
129 i32::try_from(pid).ok()
130}
131
132#[must_use]
133pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
134 let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
135 let expected_name = format!("{name_key}={service_name}");
136
137 let mut has_marker = false;
138 let mut has_name = false;
139 for entry in environ.split(|&b| b == 0) {
140 if entry == marker.as_bytes() {
141 has_marker = true;
142 } else if entry == expected_name.as_bytes() {
143 has_name = true;
144 }
145 }
146
147 has_marker && has_name
148}
149
150#[must_use]
158pub fn environ_from_procargs2(blob: &[u8]) -> Option<&[u8]> {
159 const ARGC_LEN: usize = size_of::<i32>();
160
161 let argc_bytes: [u8; ARGC_LEN] = blob.get(..ARGC_LEN)?.try_into().ok()?;
162 let argc = usize::try_from(i32::from_ne_bytes(argc_bytes)).ok()?;
163
164 let mut rest = blob.get(ARGC_LEN..)?;
165 let exec_path_end = rest.iter().position(|&b| b == 0)?;
166 rest = rest.get(exec_path_end + 1..)?;
167
168 let argv_start = rest.iter().position(|&b| b != 0)?;
169 rest = rest.get(argv_start..)?;
170
171 for _ in 0..argc {
172 let entry_end = rest.iter().position(|&b| b == 0)?;
173 rest = rest.get(entry_end + 1..)?;
174 }
175
176 Some(rest)
177}