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";
67
68const FLY_HOST_ENV: &str = "FLY_APP_NAME";
70
71pub fn deployment_host(lookup: impl Fn(&str) -> Option<String>) -> Option<String> {
72 [DEPLOYMENT_HOST_ENV, FLY_HOST_ENV].iter().find_map(|name| {
73 lookup(name)
74 .map(|value| value.trim().to_owned())
75 .filter(|value| !value.is_empty())
76 })
77}
78
79pub fn is_deployment_host(lookup: impl Fn(&str) -> Option<String>) -> bool {
80 deployment_host(lookup).is_some()
81}
82
83pub fn inherited_parent_env(lookup: impl Fn(&str) -> Option<String>) -> Vec<(String, String)> {
84 let mut env: Vec<(String, String)> = [
85 DEPLOYMENT_HOST_ENV,
86 FLY_HOST_ENV,
87 "HOSTNAME",
88 "PATH",
89 "HOME",
90 ]
91 .iter()
92 .filter_map(|name| lookup(name).map(|value| ((*name).to_owned(), value)))
93 .collect();
94
95 if let Some(entry) = crate::net::trusted_hosts_env_entry(&lookup) {
96 env.push(entry);
97 }
98
99 env
100}
101
102type SpawnReply = Sender<std::io::Result<std::process::Child>>;
103
104pub fn spawn_supervised(cmd: Command) -> std::io::Result<u32> {
105 let child = spawn_owned_supervised(cmd)?;
106 let pid = child.id();
107 drop(child);
108 Ok(pid)
109}
110
111pub fn spawn_owned_supervised(cmd: Command) -> std::io::Result<std::process::Child> {
112 let sender = spawner()
113 .as_ref()
114 .map_err(|e| std::io::Error::other(e.clone()))?;
115 let (reply_tx, reply_rx) = channel();
116 sender
117 .send((cmd, reply_tx))
118 .map_err(|error| std::io::Error::other(error.to_string()))?;
119 reply_rx
120 .recv()
121 .map_err(|error| std::io::Error::other(error.to_string()))?
122}
123
124fn spawner() -> &'static Result<Sender<(Command, SpawnReply)>, String> {
125 static SPAWNER: OnceLock<Result<Sender<(Command, SpawnReply)>, String>> = OnceLock::new();
126 SPAWNER.get_or_init(|| {
127 let (tx, rx) = channel::<(Command, SpawnReply)>();
128 std::thread::Builder::new()
129 .name("subprocess-spawner".to_owned())
130 .spawn(move || {
131 while let Ok((mut cmd, reply)) = rx.recv() {
132 let outcome = spawn_on_this_thread(&mut cmd);
133 if let Err(undelivered) = reply.send(outcome)
134 && let Ok(mut child) = undelivered.0
135 {
136 if let Err(error) = child.kill() {
137 tracing::warn!(error = %error, "Failed to stop unclaimed subprocess");
138 }
139 if let Err(error) = child.wait() {
140 tracing::warn!(error = %error, "Failed to reap unclaimed subprocess");
141 }
142 }
143 }
144 })
145 .map(|_handle| tx)
146 .map_err(|e| format!("could not start the subprocess spawner thread: {e}"))
147 })
148}
149
150fn spawn_on_this_thread(cmd: &mut Command) -> std::io::Result<std::process::Child> {
151 #[cfg(target_os = "linux")]
152 linux::arm_parent_death_signal(cmd);
153 cmd.spawn()
154}
155
156#[cfg(unix)]
159pub fn place_in_own_process_group(command: &mut Command) {
160 use std::os::unix::process::CommandExt;
161 command.process_group(0);
162}
163
164#[cfg(windows)]
165pub fn place_in_own_process_group(command: &mut Command) {
166 use std::os::windows::process::CommandExt;
167 const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
168 command.creation_flags(CREATE_NEW_PROCESS_GROUP);
169}
170
171#[must_use]
172pub const fn identity_verification_supported() -> bool {
173 cfg!(any(target_os = "linux", target_os = "macos"))
174}
175
176#[must_use]
177pub fn signalable_pid(pid: u32) -> Option<i32> {
178 if pid == 0 {
179 return None;
180 }
181 i32::try_from(pid).ok()
182}
183
184#[must_use]
185pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
186 let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
187 let expected_name = format!("{name_key}={service_name}");
188
189 let mut has_marker = false;
190 let mut has_name = false;
191 for entry in environ.split(|&b| b == 0) {
192 if entry == marker.as_bytes() {
193 has_marker = true;
194 } else if entry == expected_name.as_bytes() {
195 has_name = true;
196 }
197 }
198
199 has_marker && has_name
200}
201
202#[must_use]
206pub fn environ_from_procargs2(blob: &[u8]) -> Option<&[u8]> {
207 const ARGC_LEN: usize = size_of::<i32>();
208
209 let argc_bytes: [u8; ARGC_LEN] = blob.get(..ARGC_LEN)?.try_into().ok()?;
210 let argc = usize::try_from(i32::from_ne_bytes(argc_bytes)).ok()?;
211
212 let mut rest = blob.get(ARGC_LEN..)?;
213 let exec_path_end = rest.iter().position(|&b| b == 0)?;
214 rest = rest.get(exec_path_end + 1..)?;
215
216 let argv_start = rest.iter().position(|&b| b != 0)?;
217 rest = rest.get(argv_start..)?;
218
219 for _ in 0..argc {
220 let entry_end = rest.iter().position(|&b| b == 0)?;
221 rest = rest.get(entry_end + 1..)?;
222 }
223
224 Some(rest)
225}