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(
19 &mut self,
20 name: &str,
21 binary_path: &str,
22 args: &[&str],
23 stdio: impl Fn() -> Stdio,
24 working_dir: Option<&Path>,
25 ) -> Result<u32> {
26 let mut command = Command::new(binary_path);
27 command.args(args).stdout(stdio()).stderr(stdio());
28 if let Some(dir) = working_dir {
29 command.current_dir(dir);
30 }
31
32 let child = command
33 .spawn()
34 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
35
36 let pid = child.id();
37 self.manager.track(name.to_string(), child);
38
39 Ok(pid)
40 }
41
42 pub fn spawn_node(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
45 self.spawn_tracked(name, binary_path, args, Stdio::inherit, None)
46 }
47
48 pub fn spawn_with_output(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
50 self.spawn_tracked(name, binary_path, args, Stdio::piped, None)
51 }
52
53 pub fn spawn_with_env(
55 &mut self,
56 name: &str,
57 binary_path: &str,
58 args: &[&str],
59 env: HashMap<String, String>,
60 ) -> Result<u32> {
61 let disk_logging_enabled = self.disk_logging_enabled();
62
63 tracing::debug!("🔧 spawn_with_env for '{}': received {} env vars", name, env.len());
64 if !env.is_empty() {
65 for (key, value) in &env {
66 if key == "ROBOT_API_KEY" {
67 tracing::debug!(" {} = <redacted>", key);
68 } else {
69 tracing::debug!(" {} = {}", key, value);
70 }
71 }
72 } else {
73 tracing::warn!("⚠️ No environment variables to inject!");
74 }
75
76 let mut cmd = Command::new(binary_path);
77 cmd.args(args).envs(&env);
78
79 cmd.env("MECHA10_DISK_LOGGING_ENABLED", disk_logging_enabled.to_string());
80
81 if disk_logging_enabled {
82 let logs_dir = std::path::PathBuf::from(paths::project::LOGS_DIR);
83 if !logs_dir.exists() {
84 std::fs::create_dir_all(&logs_dir)?;
85 }
86
87 let log_name = strip_node_scope(name);
88 let log_file_path = logs_dir.join(format!("{}.log", log_name));
89 let log_file = std::fs::OpenOptions::new()
90 .create(true)
91 .append(true)
92 .open(&log_file_path)
93 .with_context(|| format!("Failed to create log file: {}", log_file_path.display()))?;
94
95 let log_file_stderr = log_file.try_clone().context("Failed to clone log file handle")?;
96
97 cmd.stdout(log_file).stderr(log_file_stderr);
98 } else {
99 tracing::debug!(
100 "📴 Disk logging disabled via project config; discarding stdout/stderr for '{}'",
101 name
102 );
103 cmd.stdout(Stdio::null()).stderr(Stdio::null());
104 }
105
106 #[cfg(unix)]
107 {
108 use std::os::unix::process::CommandExt;
109 cmd.process_group(0);
110 }
111
112 let child = cmd
113 .spawn()
114 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
115
116 let pid = child.id();
117 self.manager.track(name.to_string(), child);
118
119 Ok(pid)
120 }
121
122 pub fn spawn_in_dir(
124 &mut self,
125 name: &str,
126 binary_path: &str,
127 args: &[&str],
128 working_dir: impl AsRef<Path>,
129 ) -> Result<u32> {
130 self.spawn_tracked(name, binary_path, args, Stdio::inherit, Some(working_dir.as_ref()))
131 }
132
133 pub fn spawn_nodes(&mut self, nodes: Vec<(&str, &str, Vec<&str>)>) -> Result<HashMap<String, u32>> {
136 let mut pids = HashMap::new();
137
138 for (name, binary_path, args) in nodes {
139 match self.spawn_node(name, binary_path, &args) {
140 Ok(pid) => {
141 pids.insert(name.to_string(), pid);
142 }
143 Err(e) => {
144 eprintln!("Failed to spawn {}: {}", name, e);
145 }
146 }
147 }
148
149 Ok(pids)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn spawn_node_tracks_the_process_under_the_given_name() {
159 let mut service = ProcessService::new();
160
161 let pid = service.spawn_node("test_process", "sleep", &["1"]).unwrap();
162
163 assert!(pid > 0);
164 assert_eq!(service.count(), 1);
165
166 service.cleanup();
167 }
168
169 #[test]
170 fn spawn_with_output_tracks_the_process_under_the_given_name() {
171 let mut service = ProcessService::new();
172
173 let pid = service.spawn_with_output("test_output", "sleep", &["1"]).unwrap();
174
175 assert!(pid > 0);
176 assert_eq!(service.count(), 1);
177
178 service.cleanup();
179 }
180
181 #[test]
182 fn spawn_in_dir_tracks_the_process_and_runs_it_in_the_given_directory() {
183 let mut service = ProcessService::new();
184
185 let pid = service.spawn_in_dir("test_dir", "sleep", &["1"], "/tmp").unwrap();
186
187 assert!(pid > 0);
188 assert_eq!(service.count(), 1);
189
190 service.cleanup();
191 }
192
193 #[test]
194 fn spawn_node_and_spawn_with_output_use_different_stdio_modes() {
195 let mut inherited = ProcessService::new();
196 let inherited_pid = inherited
197 .spawn_node("echo_inherited", "sh", &["-c", "echo hi"])
198 .unwrap();
199 assert!(inherited_pid > 0);
200 inherited.cleanup();
201
202 let mut piped = ProcessService::new();
203 let piped_pid = piped.spawn_with_output("echo_piped", "sh", &["-c", "echo hi"]).unwrap();
204 assert!(piped_pid > 0);
205 piped.cleanup();
206 }
207
208 #[test]
209 fn spawn_nodes_returns_a_pid_per_successfully_spawned_node() {
210 let mut service = ProcessService::new();
211
212 let pids = service
213 .spawn_nodes(vec![("node1", "sleep", vec!["1"]), ("node2", "sleep", vec!["1"])])
214 .unwrap();
215
216 assert_eq!(pids.len(), 2);
217 assert!(pids.contains_key("node1"));
218 assert!(pids.contains_key("node2"));
219
220 service.cleanup();
221 }
222}