use std::process::Command;
use std::sync::OnceLock;
use std::sync::mpsc::{Sender, channel};
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::{is_zombie, live_pid_is_subprocess};
#[cfg(target_os = "macos")]
mod darwin;
#[cfg(target_os = "macos")]
pub use darwin::{is_zombie, live_pid_is_subprocess};
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
mod unsupported;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub use unsupported::{is_zombie, live_pid_is_subprocess};
pub const SUBPROCESS_MARKER_ENV: &str = "SYSTEMPROMPT_SUBPROCESS";
pub const AGENT_NAME_ENV: &str = "AGENT_NAME";
pub const MCP_SERVICE_ID_ENV: &str = "MCP_SERVICE_ID";
type SpawnReply = Sender<std::io::Result<u32>>;
pub fn spawn_supervised(cmd: Command) -> std::io::Result<u32> {
let sender = spawner()
.as_ref()
.map_err(|e| std::io::Error::other(e.clone()))?;
let (reply_tx, reply_rx) = channel();
sender
.send((cmd, reply_tx))
.map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?;
reply_rx
.recv()
.map_err(|disconnected| std::io::Error::other(disconnected.to_string()))?
}
fn spawner() -> &'static Result<Sender<(Command, SpawnReply)>, String> {
static SPAWNER: OnceLock<Result<Sender<(Command, SpawnReply)>, String>> = OnceLock::new();
SPAWNER.get_or_init(|| {
let (tx, rx) = channel::<(Command, SpawnReply)>();
std::thread::Builder::new()
.name("subprocess-spawner".to_owned())
.spawn(move || {
while let Ok((mut cmd, reply)) = rx.recv() {
let outcome = spawn_on_this_thread(&mut cmd);
if reply.send(outcome).is_err() {
tracing::warn!(
"Spawn requester vanished before collecting the child pid; the child \
is unregistered and will only be cleaned up by its parent-death signal"
);
}
}
})
.map(|_handle| tx)
.map_err(|e| format!("could not start the subprocess spawner thread: {e}"))
})
}
fn spawn_on_this_thread(cmd: &mut Command) -> std::io::Result<u32> {
#[cfg(target_os = "linux")]
linux::arm_parent_death_signal(cmd);
let child = cmd.spawn()?;
let pid = child.id();
#[expect(
clippy::mem_forget,
reason = "detached child: skip Child's drop-time wait so it keeps running after this \
returns; reaping is the caller's business via is_zombie"
)]
std::mem::forget(child);
Ok(pid)
}
#[cfg(unix)]
pub fn place_in_own_process_group(command: &mut Command) {
use std::os::unix::process::CommandExt;
command.process_group(0);
}
#[cfg(windows)]
pub fn place_in_own_process_group(command: &mut Command) {
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
command.creation_flags(CREATE_NEW_PROCESS_GROUP);
}
#[must_use]
pub const fn identity_verification_supported() -> bool {
cfg!(any(target_os = "linux", target_os = "macos"))
}
#[must_use]
pub fn signalable_pid(pid: u32) -> Option<i32> {
if pid == 0 {
return None;
}
i32::try_from(pid).ok()
}
#[must_use]
pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
let expected_name = format!("{name_key}={service_name}");
let mut has_marker = false;
let mut has_name = false;
for entry in environ.split(|&b| b == 0) {
if entry == marker.as_bytes() {
has_marker = true;
} else if entry == expected_name.as_bytes() {
has_name = true;
}
}
has_marker && has_name
}
#[must_use]
pub fn environ_from_procargs2(blob: &[u8]) -> Option<&[u8]> {
const ARGC_LEN: usize = size_of::<i32>();
let argc_bytes: [u8; ARGC_LEN] = blob.get(..ARGC_LEN)?.try_into().ok()?;
let argc = usize::try_from(i32::from_ne_bytes(argc_bytes)).ok()?;
let mut rest = blob.get(ARGC_LEN..)?;
let exec_path_end = rest.iter().position(|&b| b == 0)?;
rest = rest.get(exec_path_end + 1..)?;
let argv_start = rest.iter().position(|&b| b != 0)?;
rest = rest.get(argv_start..)?;
for _ in 0..argc {
let entry_end = rest.iter().position(|&b| b == 0)?;
rest = rest.get(entry_end + 1..)?;
}
Some(rest)
}