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
66pub const DEPLOYMENT_HOST_ENV: &str = "SYSTEMPROMPT_DEPLOYMENT_HOST";
71
72const FLY_HOST_ENV: &str = "FLY_APP_NAME";
75
76pub fn deployment_host(lookup: impl Fn(&str) -> Option<String>) -> Option<String> {
80 [DEPLOYMENT_HOST_ENV, FLY_HOST_ENV].iter().find_map(|name| {
81 lookup(name)
82 .map(|value| value.trim().to_owned())
83 .filter(|value| !value.is_empty())
84 })
85}
86
87pub fn is_deployment_host(lookup: impl Fn(&str) -> Option<String>) -> bool {
92 deployment_host(lookup).is_some()
93}
94
95pub fn inherited_parent_env(lookup: impl Fn(&str) -> Option<String>) -> Vec<(String, String)> {
101 let mut env: Vec<(String, String)> = [
102 DEPLOYMENT_HOST_ENV,
103 FLY_HOST_ENV,
104 "HOSTNAME",
105 "PATH",
106 "HOME",
107 ]
108 .iter()
109 .filter_map(|name| lookup(name).map(|value| ((*name).to_owned(), value)))
110 .collect();
111
112 if let Some(entry) = crate::net::trusted_hosts_env_entry(&lookup) {
113 env.push(entry);
114 }
115
116 env
117}
118
119type SpawnReply = Sender<std::io::Result<u32>>;
120
121pub fn spawn_supervised(cmd: Command) -> std::io::Result<u32> {
122 let sender = spawner()
123 .as_ref()
124 .map_err(|e| std::io::Error::other(e.clone()))?;
125
126 let (reply_tx, reply_rx) = channel();
127 sender
128 .send((cmd, reply_tx))
129 .map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?;
130 reply_rx
131 .recv()
132 .map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?
133}
134
135fn spawner() -> &'static Result<Sender<(Command, SpawnReply)>, String> {
136 static SPAWNER: OnceLock<Result<Sender<(Command, SpawnReply)>, String>> = OnceLock::new();
137 SPAWNER.get_or_init(|| {
138 let (tx, rx) = channel::<(Command, SpawnReply)>();
139 std::thread::Builder::new()
140 .name("subprocess-spawner".to_owned())
141 .spawn(move || {
142 while let Ok((mut cmd, reply)) = rx.recv() {
143 let outcome = spawn_on_this_thread(&mut cmd);
144 if reply.send(outcome).is_err() {
145 tracing::warn!(
146 "Spawn requester vanished before collecting the child pid; the child \
147 is unregistered and will only be cleaned up by its parent-death signal"
148 );
149 }
150 }
151 })
152 .map(|_handle| tx)
153 .map_err(|e| format!("could not start the subprocess spawner thread: {e}"))
154 })
155}
156
157fn spawn_on_this_thread(cmd: &mut Command) -> std::io::Result<u32> {
158 #[cfg(target_os = "linux")]
159 linux::arm_parent_death_signal(cmd);
160
161 let child = cmd.spawn()?;
162 let pid = child.id();
163 #[expect(
164 clippy::mem_forget,
165 reason = "detached child: skip Child's drop-time wait so it keeps running after this \
166 returns; reaping is the caller's business via is_zombie"
167 )]
168 std::mem::forget(child);
169 Ok(pid)
170}
171
172#[cfg(unix)]
176pub fn place_in_own_process_group(command: &mut Command) {
177 use std::os::unix::process::CommandExt;
178 command.process_group(0);
179}
180
181#[cfg(windows)]
182pub fn place_in_own_process_group(command: &mut Command) {
183 use std::os::windows::process::CommandExt;
184 const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
185 command.creation_flags(CREATE_NEW_PROCESS_GROUP);
186}
187
188#[must_use]
189pub const fn identity_verification_supported() -> bool {
190 cfg!(any(target_os = "linux", target_os = "macos"))
191}
192
193#[must_use]
194pub fn signalable_pid(pid: u32) -> Option<i32> {
195 if pid == 0 {
196 return None;
197 }
198 i32::try_from(pid).ok()
199}
200
201#[must_use]
202pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
203 let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
204 let expected_name = format!("{name_key}={service_name}");
205
206 let mut has_marker = false;
207 let mut has_name = false;
208 for entry in environ.split(|&b| b == 0) {
209 if entry == marker.as_bytes() {
210 has_marker = true;
211 } else if entry == expected_name.as_bytes() {
212 has_name = true;
213 }
214 }
215
216 has_marker && has_name
217}
218
219#[must_use]
227pub fn environ_from_procargs2(blob: &[u8]) -> Option<&[u8]> {
228 const ARGC_LEN: usize = size_of::<i32>();
229
230 let argc_bytes: [u8; ARGC_LEN] = blob.get(..ARGC_LEN)?.try_into().ok()?;
231 let argc = usize::try_from(i32::from_ne_bytes(argc_bytes)).ok()?;
232
233 let mut rest = blob.get(ARGC_LEN..)?;
234 let exec_path_end = rest.iter().position(|&b| b == 0)?;
235 rest = rest.get(exec_path_end + 1..)?;
236
237 let argv_start = rest.iter().position(|&b| b != 0)?;
238 rest = rest.get(argv_start..)?;
239
240 for _ in 0..argc {
241 let entry_end = rest.iter().position(|&b| b == 0)?;
242 rest = rest.get(entry_end + 1..)?;
243 }
244
245 Some(rest)
246}