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