mecha10_cli/services/process/spawn.rs
1//! Plain process spawning primitives for `ProcessService`
2//!
3//! Covers spawning with inherited/piped output, custom env, and a specific
4//! working directory. Higher-level node spawning (via `mecha10-node-runner`
5//! or standalone mode) lives in [`super::spawn_node`].
6
7use 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 /// Spawn a node process
17 ///
18 /// # Arguments
19 ///
20 /// * `name` - Name to identify the process
21 /// * `binary_path` - Path to the binary to execute
22 /// * `args` - Command-line arguments
23 ///
24 /// # Errors
25 ///
26 /// Returns an error if the process cannot be spawned
27 pub fn spawn_node(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
28 let child = Command::new(binary_path)
29 .args(args)
30 .stdout(Stdio::inherit())
31 .stderr(Stdio::inherit())
32 .spawn()
33 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
34
35 let pid = child.id();
36 self.manager.track(name.to_string(), child);
37
38 Ok(pid)
39 }
40
41 /// Spawn a process with output capture
42 ///
43 /// # Arguments
44 ///
45 /// * `name` - Name to identify the process
46 /// * `binary_path` - Path to the binary to execute
47 /// * `args` - Command-line arguments
48 ///
49 /// # Errors
50 ///
51 /// Returns an error if the process cannot be spawned
52 pub fn spawn_with_output(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
53 let child = Command::new(binary_path)
54 .args(args)
55 .stdout(Stdio::piped())
56 .stderr(Stdio::piped())
57 .spawn()
58 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
59
60 let pid = child.id();
61 self.manager.track(name.to_string(), child);
62
63 Ok(pid)
64 }
65
66 /// Spawn a process with custom environment variables
67 ///
68 /// # Arguments
69 ///
70 /// * `name` - Name to identify the process
71 /// * `binary_path` - Path to the binary to execute
72 /// * `args` - Command-line arguments
73 /// * `env` - Environment variables to set
74 pub fn spawn_with_env(
75 &mut self,
76 name: &str,
77 binary_path: &str,
78 args: &[&str],
79 env: HashMap<String, String>,
80 ) -> Result<u32> {
81 let disk_logging_enabled = self.disk_logging_enabled();
82
83 // Debug: Log environment being passed to process
84 tracing::debug!("🔧 spawn_with_env for '{}': received {} env vars", name, env.len());
85 if !env.is_empty() {
86 for (key, value) in &env {
87 if key == "ROBOT_API_KEY" {
88 tracing::debug!(" {} = <redacted>", key);
89 } else {
90 tracing::debug!(" {} = {}", key, value);
91 }
92 }
93 } else {
94 tracing::warn!("⚠️ No environment variables to inject!");
95 }
96
97 let mut cmd = Command::new(binary_path);
98 cmd.args(args).envs(&env);
99
100 // Propagate the disk-logging setting to the child (e.g. mecha10-node-runner),
101 // which inherits its own stdout/stderr descendants may write logs through.
102 cmd.env("MECHA10_DISK_LOGGING_ENABLED", disk_logging_enabled.to_string());
103
104 if disk_logging_enabled {
105 // Create logs directory if it doesn't exist
106 let logs_dir = std::path::PathBuf::from(paths::project::LOGS_DIR);
107 if !logs_dir.exists() {
108 std::fs::create_dir_all(&logs_dir)?;
109 }
110
111 // Create log file for this process
112 // Use short node name for log file (e.g., "@mecha10/listener" -> "listener.log")
113 let log_name = strip_node_scope(name);
114 let log_file_path = logs_dir.join(format!("{}.log", log_name));
115 let log_file = std::fs::OpenOptions::new()
116 .create(true)
117 .append(true)
118 .open(&log_file_path)
119 .with_context(|| format!("Failed to create log file: {}", log_file_path.display()))?;
120
121 // Clone for stderr
122 let log_file_stderr = log_file.try_clone().context("Failed to clone log file handle")?;
123
124 cmd.stdout(log_file) // Redirect stdout to log file
125 .stderr(log_file_stderr); // Redirect stderr to log file
126 } else {
127 // Disk logging disabled via project config
128 // (`environments.<env>.logging.disk_logging_enabled: false`): don't open or
129 // write to any file on disk. Remote/network log shipping (handled
130 // independently by the node/control-plane) is unaffected by this setting.
131 tracing::debug!(
132 "📴 Disk logging disabled via project config; discarding stdout/stderr for '{}'",
133 name
134 );
135 cmd.stdout(Stdio::null()).stderr(Stdio::null());
136 }
137
138 // On Unix: Create new process group to prevent terminal signals from reaching child processes
139 // This ensures Ctrl+C in the terminal only affects the main CLI process, not node-runner
140 #[cfg(unix)]
141 {
142 use std::os::unix::process::CommandExt;
143 cmd.process_group(0); // 0 = create new process group with same ID as child PID
144 }
145
146 let child = cmd
147 .spawn()
148 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
149
150 let pid = child.id();
151 self.manager.track(name.to_string(), child);
152
153 Ok(pid)
154 }
155
156 /// Spawn a process in a specific working directory
157 ///
158 /// # Arguments
159 ///
160 /// * `name` - Name to identify the process
161 /// * `binary_path` - Path to the binary to execute
162 /// * `args` - Command-line arguments
163 /// * `working_dir` - Working directory for the process
164 pub fn spawn_in_dir(
165 &mut self,
166 name: &str,
167 binary_path: &str,
168 args: &[&str],
169 working_dir: impl AsRef<Path>,
170 ) -> Result<u32> {
171 let child = Command::new(binary_path)
172 .args(args)
173 .current_dir(working_dir)
174 .stdout(Stdio::inherit())
175 .stderr(Stdio::inherit())
176 .spawn()
177 .with_context(|| format!("Failed to spawn process: {}", binary_path))?;
178
179 let pid = child.id();
180 self.manager.track(name.to_string(), child);
181
182 Ok(pid)
183 }
184
185 /// Spawn multiple node processes from a list
186 ///
187 /// # Arguments
188 ///
189 /// * `nodes` - Vec of (name, binary_path, args) tuples
190 ///
191 /// # Returns
192 ///
193 /// HashMap of node names to PIDs
194 pub fn spawn_nodes(&mut self, nodes: Vec<(&str, &str, Vec<&str>)>) -> Result<HashMap<String, u32>> {
195 let mut pids = HashMap::new();
196
197 for (name, binary_path, args) in nodes {
198 match self.spawn_node(name, binary_path, &args) {
199 Ok(pid) => {
200 pids.insert(name.to_string(), pid);
201 }
202 Err(e) => {
203 eprintln!("Failed to spawn {}: {}", name, e);
204 }
205 }
206 }
207
208 Ok(pids)
209 }
210}