Skip to main content

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}