use super::ProcessService;
use crate::paths;
use anyhow::{Context, Result};
use mecha10_cli_core::utils::strip_node_scope;
use std::collections::HashMap;
use std::path::Path;
use std::process::{Command, Stdio};
impl ProcessService {
fn spawn_tracked(
&mut self,
name: &str,
binary_path: &str,
args: &[&str],
stdio: impl Fn() -> Stdio,
working_dir: Option<&Path>,
) -> Result<u32> {
let mut command = Command::new(binary_path);
command.args(args).stdout(stdio()).stderr(stdio());
if let Some(dir) = working_dir {
command.current_dir(dir);
}
let child = command
.spawn()
.with_context(|| format!("Failed to spawn process: {}", binary_path))?;
let pid = child.id();
self.manager.track(name.to_string(), child);
Ok(pid)
}
pub fn spawn_node(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
self.spawn_tracked(name, binary_path, args, Stdio::inherit, None)
}
pub fn spawn_with_output(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
self.spawn_tracked(name, binary_path, args, Stdio::piped, None)
}
pub fn spawn_with_env(
&mut self,
name: &str,
binary_path: &str,
args: &[&str],
env: HashMap<String, String>,
) -> Result<u32> {
let disk_logging_enabled = self.disk_logging_enabled();
tracing::debug!("🔧 spawn_with_env for '{}': received {} env vars", name, env.len());
if !env.is_empty() {
for (key, value) in &env {
if key == "ROBOT_API_KEY" {
tracing::debug!(" {} = <redacted>", key);
} else {
tracing::debug!(" {} = {}", key, value);
}
}
} else {
tracing::warn!("⚠️ No environment variables to inject!");
}
let mut cmd = Command::new(binary_path);
cmd.args(args).envs(&env);
cmd.env("MECHA10_DISK_LOGGING_ENABLED", disk_logging_enabled.to_string());
if disk_logging_enabled {
let logs_dir = std::path::PathBuf::from(paths::project::LOGS_DIR);
if !logs_dir.exists() {
std::fs::create_dir_all(&logs_dir)?;
}
let log_name = strip_node_scope(name);
let log_file_path = logs_dir.join(format!("{}.log", log_name));
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
.with_context(|| format!("Failed to create log file: {}", log_file_path.display()))?;
let log_file_stderr = log_file.try_clone().context("Failed to clone log file handle")?;
cmd.stdout(log_file).stderr(log_file_stderr);
} else {
tracing::debug!(
"📴 Disk logging disabled via project config; discarding stdout/stderr for '{}'",
name
);
cmd.stdout(Stdio::null()).stderr(Stdio::null());
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
let child = cmd
.spawn()
.with_context(|| format!("Failed to spawn process: {}", binary_path))?;
let pid = child.id();
self.manager.track(name.to_string(), child);
Ok(pid)
}
pub fn spawn_in_dir(
&mut self,
name: &str,
binary_path: &str,
args: &[&str],
working_dir: impl AsRef<Path>,
) -> Result<u32> {
self.spawn_tracked(name, binary_path, args, Stdio::inherit, Some(working_dir.as_ref()))
}
pub fn spawn_nodes(&mut self, nodes: Vec<(&str, &str, Vec<&str>)>) -> Result<HashMap<String, u32>> {
let mut pids = HashMap::new();
for (name, binary_path, args) in nodes {
match self.spawn_node(name, binary_path, &args) {
Ok(pid) => {
pids.insert(name.to_string(), pid);
}
Err(e) => {
eprintln!("Failed to spawn {}: {}", name, e);
}
}
}
Ok(pids)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_node_tracks_the_process_under_the_given_name() {
let mut service = ProcessService::new();
let pid = service.spawn_node("test_process", "sleep", &["1"]).unwrap();
assert!(pid > 0);
assert_eq!(service.count(), 1);
service.cleanup();
}
#[test]
fn spawn_with_output_tracks_the_process_under_the_given_name() {
let mut service = ProcessService::new();
let pid = service.spawn_with_output("test_output", "sleep", &["1"]).unwrap();
assert!(pid > 0);
assert_eq!(service.count(), 1);
service.cleanup();
}
#[test]
fn spawn_in_dir_tracks_the_process_and_runs_it_in_the_given_directory() {
let mut service = ProcessService::new();
let pid = service.spawn_in_dir("test_dir", "sleep", &["1"], "/tmp").unwrap();
assert!(pid > 0);
assert_eq!(service.count(), 1);
service.cleanup();
}
#[test]
fn spawn_node_and_spawn_with_output_use_different_stdio_modes() {
let mut inherited = ProcessService::new();
let inherited_pid = inherited
.spawn_node("echo_inherited", "sh", &["-c", "echo hi"])
.unwrap();
assert!(inherited_pid > 0);
inherited.cleanup();
let mut piped = ProcessService::new();
let piped_pid = piped.spawn_with_output("echo_piped", "sh", &["-c", "echo hi"]).unwrap();
assert!(piped_pid > 0);
piped.cleanup();
}
#[test]
fn spawn_nodes_returns_a_pid_per_successfully_spawned_node() {
let mut service = ProcessService::new();
let pids = service
.spawn_nodes(vec![("node1", "sleep", vec!["1"]), ("node2", "sleep", vec!["1"])])
.unwrap();
assert_eq!(pids.len(), 2);
assert!(pids.contains_key("node1"));
assert!(pids.contains_key("node2"));
service.cleanup();
}
}