mecha10_cli/services/process/
spawn.rs1use super::ProcessService;
8use crate::paths;
9use anyhow::{Context, Result};
10use mecha10_cli_core::utils::strip_node_scope;
11use std::collections::HashMap;
12use std::path::Path;
13use std::process::{Command, Stdio};
14
15impl ProcessService {
16 fn spawn_tracked(
24 &mut self,
25 name: &str,
26 binary_path: &str,
27 args: &[&str],
28 stdio: impl Fn() -> Stdio,
29 working_dir: Option<&Path>,
30 ) -> Result<u32> {
31 let mut command = Command::new(binary_path);
32 command.args(args).stdout(stdio()).stderr(stdio());
33 if let Some(dir) = working_dir {
34 command.current_dir(dir);
35 }
36
37 let child = command
38 .spawn()
39 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
40
41 let pid = child.id();
42 self.manager.track(name.to_string(), child);
43
44 Ok(pid)
45 }
46
47 pub fn spawn_node(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
59 self.spawn_tracked(name, binary_path, args, Stdio::inherit, None)
60 }
61
62 pub fn spawn_with_output(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
74 self.spawn_tracked(name, binary_path, args, Stdio::piped, None)
75 }
76
77 pub fn spawn_with_env(
86 &mut self,
87 name: &str,
88 binary_path: &str,
89 args: &[&str],
90 env: HashMap<String, String>,
91 ) -> Result<u32> {
92 let disk_logging_enabled = self.disk_logging_enabled();
93
94 tracing::debug!("🔧 spawn_with_env for '{}': received {} env vars", name, env.len());
95 if !env.is_empty() {
96 for (key, value) in &env {
97 if key == "ROBOT_API_KEY" {
98 tracing::debug!(" {} = <redacted>", key);
99 } else {
100 tracing::debug!(" {} = {}", key, value);
101 }
102 }
103 } else {
104 tracing::warn!("⚠️ No environment variables to inject!");
105 }
106
107 let mut cmd = Command::new(binary_path);
108 cmd.args(args).envs(&env);
109
110 cmd.env("MECHA10_DISK_LOGGING_ENABLED", disk_logging_enabled.to_string());
111
112 if disk_logging_enabled {
113 let logs_dir = std::path::PathBuf::from(paths::project::LOGS_DIR);
114 if !logs_dir.exists() {
115 std::fs::create_dir_all(&logs_dir)?;
116 }
117
118 let log_name = strip_node_scope(name);
119 let log_file_path = logs_dir.join(format!("{}.log", log_name));
120 let log_file = std::fs::OpenOptions::new()
121 .create(true)
122 .append(true)
123 .open(&log_file_path)
124 .with_context(|| format!("Failed to create log file: {}", log_file_path.display()))?;
125
126 let log_file_stderr = log_file.try_clone().context("Failed to clone log file handle")?;
127
128 cmd.stdout(log_file).stderr(log_file_stderr);
129 } else {
130 tracing::debug!(
131 "📴 Disk logging disabled via project config; discarding stdout/stderr for '{}'",
132 name
133 );
134 cmd.stdout(Stdio::null()).stderr(Stdio::null());
135 }
136
137 #[cfg(unix)]
138 {
139 use std::os::unix::process::CommandExt;
140 cmd.process_group(0);
141 }
142
143 let child = cmd
144 .spawn()
145 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
146
147 let pid = child.id();
148 self.manager.track(name.to_string(), child);
149
150 Ok(pid)
151 }
152
153 pub fn spawn_in_dir(
162 &mut self,
163 name: &str,
164 binary_path: &str,
165 args: &[&str],
166 working_dir: impl AsRef<Path>,
167 ) -> Result<u32> {
168 self.spawn_tracked(name, binary_path, args, Stdio::inherit, Some(working_dir.as_ref()))
169 }
170
171 pub fn spawn_nodes(&mut self, nodes: Vec<(&str, &str, Vec<&str>)>) -> Result<HashMap<String, u32>> {
181 let mut pids = HashMap::new();
182
183 for (name, binary_path, args) in nodes {
184 match self.spawn_node(name, binary_path, &args) {
185 Ok(pid) => {
186 pids.insert(name.to_string(), pid);
187 }
188 Err(e) => {
189 eprintln!("Failed to spawn {}: {}", name, e);
190 }
191 }
192 }
193
194 Ok(pids)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn spawn_node_tracks_the_process_under_the_given_name() {
204 let mut service = ProcessService::new();
205
206 let pid = service.spawn_node("test_process", "sleep", &["1"]).unwrap();
207
208 assert!(pid > 0);
209 assert_eq!(service.count(), 1);
210
211 service.cleanup();
212 }
213
214 #[test]
215 fn spawn_with_output_tracks_the_process_under_the_given_name() {
216 let mut service = ProcessService::new();
217
218 let pid = service.spawn_with_output("test_output", "sleep", &["1"]).unwrap();
219
220 assert!(pid > 0);
221 assert_eq!(service.count(), 1);
222
223 service.cleanup();
224 }
225
226 #[test]
227 fn spawn_in_dir_tracks_the_process_and_runs_it_in_the_given_directory() {
228 let mut service = ProcessService::new();
229
230 let pid = service.spawn_in_dir("test_dir", "sleep", &["1"], "/tmp").unwrap();
231
232 assert!(pid > 0);
233 assert_eq!(service.count(), 1);
234
235 service.cleanup();
236 }
237
238 #[test]
239 fn spawn_node_and_spawn_with_output_use_different_stdio_modes() {
240 let mut inherited = ProcessService::new();
241 let inherited_pid = inherited
242 .spawn_node("echo_inherited", "sh", &["-c", "echo hi"])
243 .unwrap();
244 assert!(inherited_pid > 0);
245 inherited.cleanup();
246
247 let mut piped = ProcessService::new();
248 let piped_pid = piped.spawn_with_output("echo_piped", "sh", &["-c", "echo hi"]).unwrap();
249 assert!(piped_pid > 0);
250 piped.cleanup();
251 }
252
253 #[test]
254 fn spawn_nodes_returns_a_pid_per_successfully_spawned_node() {
255 let mut service = ProcessService::new();
256
257 let pids = service
258 .spawn_nodes(vec![("node1", "sleep", vec!["1"]), ("node2", "sleep", vec!["1"])])
259 .unwrap();
260
261 assert_eq!(pids.len(), 2);
262 assert!(pids.contains_key("node1"));
263 assert!(pids.contains_key("node2"));
264
265 service.cleanup();
266 }
267}