systemprompt_loader/subprocess/
spawn.rs1use std::process::{Child, Command};
16use std::sync::mpsc::{Sender, channel};
17use std::sync::{Mutex, PoisonError};
18
19use systemprompt_identifiers::ServiceName;
20
21use super::ChildKind;
22
23type SpawnReply = Sender<std::io::Result<Child>>;
24type SpawnRequest = (Command, SpawnReply);
25
26static SPAWNER: Mutex<Option<Sender<SpawnRequest>>> = Mutex::new(None);
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ApiServerStamp {
31 Stamped,
32 Unverifiable,
33}
34
35pub fn mark_child(cmd: &mut Command, kind: ChildKind, service: &ServiceName) {
36 match kind {
37 ChildKind::Agent | ChildKind::Mcp => {
38 cmd.env(systemprompt_models::subprocess::SUBPROCESS_MARKER_ENV, "1");
39 },
40 ChildKind::Api => {},
41 }
42 cmd.env(kind.marker_env(), service.as_str());
43}
44
45#[cfg(unix)]
52pub fn stamp_api_server() -> std::io::Result<ApiServerStamp> {
53 use std::os::unix::process::CommandExt;
54
55 let service = super::api_server_service();
56 let Some(environ) = super::live_environ(std::process::id())
57 .filter(|environ| systemprompt_models::subprocess::environ_has_entries(environ))
58 else {
59 return Ok(ApiServerStamp::Unverifiable);
60 };
61 if ChildKind::Api.identifies(&environ, &service) {
62 return Ok(ApiServerStamp::Stamped);
63 }
64 let mut cmd = Command::new(std::env::current_exe()?);
65 cmd.args(std::env::args_os().skip(1));
66 mark_child(&mut cmd, ChildKind::Api, &service);
67 Err(cmd.exec())
68}
69
70#[cfg(not(unix))]
71pub const fn stamp_api_server() -> std::io::Result<ApiServerStamp> {
72 Ok(ApiServerStamp::Unverifiable)
73}
74
75pub fn spawn_supervised(cmd: Command) -> std::io::Result<u32> {
76 let child = spawn_owned_supervised(cmd)?;
77 let pid = child.id();
78 drop(child);
79 Ok(pid)
80}
81
82pub fn spawn_owned_supervised(cmd: Command) -> std::io::Result<Child> {
83 let sender = spawner()?;
84 let (reply_tx, reply_rx) = channel();
85 sender
86 .send((cmd, reply_tx))
87 .map_err(std::io::Error::other)?;
88 reply_rx.recv().map_err(std::io::Error::other)?
89}
90
91fn spawner() -> std::io::Result<Sender<SpawnRequest>> {
92 let mut slot = SPAWNER.lock().unwrap_or_else(PoisonError::into_inner);
93 if let Some(sender) = slot.as_ref() {
94 return Ok(sender.clone());
95 }
96 let sender = start_spawner_thread()?;
97 Ok(slot.insert(sender).clone())
98}
99
100fn start_spawner_thread() -> std::io::Result<Sender<SpawnRequest>> {
101 let (tx, rx) = channel::<SpawnRequest>();
102 std::thread::Builder::new()
103 .name("subprocess-spawner".to_owned())
104 .spawn(move || {
105 while let Ok((mut cmd, reply)) = rx.recv() {
106 let outcome = spawn_on_this_thread(&mut cmd);
107 if let Err(undelivered) = reply.send(outcome)
108 && let Ok(mut child) = undelivered.0
109 {
110 if let Err(error) = child.kill() {
111 tracing::warn!(error = %error, "Failed to stop unclaimed subprocess");
112 }
113 if let Err(error) = child.wait() {
114 tracing::warn!(error = %error, "Failed to reap unclaimed subprocess");
115 }
116 }
117 }
118 })
119 .map(|_handle| tx)
120}
121
122fn spawn_on_this_thread(cmd: &mut Command) -> std::io::Result<Child> {
123 #[cfg(target_os = "linux")]
124 super::linux::arm_parent_death_signal(cmd);
125 cmd.spawn()
126}
127
128#[cfg(unix)]
131pub fn place_in_own_process_group(command: &mut Command) {
132 use std::os::unix::process::CommandExt;
133 command.process_group(0);
134}
135
136#[cfg(windows)]
137pub fn place_in_own_process_group(command: &mut Command) {
138 use std::os::windows::process::CommandExt;
139 const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
140 command.creation_flags(CREATE_NEW_PROCESS_GROUP);
141}