mecha10-cli 0.6.3

Mecha10 CLI tool
Documentation
//! Plain process spawning primitives for `ProcessService`
//!
//! Covers spawning with inherited/piped output, custom env, and a specific
//! working directory. Higher-level node spawning (via `mecha10-node-runner`
//! or standalone mode) lives in [`super::spawn_node`].

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 {
    /// Spawn a node process
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    ///
    /// # Errors
    ///
    /// Returns an error if the process cannot be spawned
    pub fn spawn_node(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
        let child = Command::new(binary_path)
            .args(args)
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn a process with output capture
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    ///
    /// # Errors
    ///
    /// Returns an error if the process cannot be spawned
    pub fn spawn_with_output(&mut self, name: &str, binary_path: &str, args: &[&str]) -> Result<u32> {
        let child = Command::new(binary_path)
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn a process with custom environment variables
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    /// * `env` - Environment variables to set
    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();

        // Debug: Log environment being passed to process
        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);

        // Propagate the disk-logging setting to the child (e.g. mecha10-node-runner),
        // which inherits its own stdout/stderr descendants may write logs through.
        cmd.env("MECHA10_DISK_LOGGING_ENABLED", disk_logging_enabled.to_string());

        if disk_logging_enabled {
            // Create logs directory if it doesn't exist
            let logs_dir = std::path::PathBuf::from(paths::project::LOGS_DIR);
            if !logs_dir.exists() {
                std::fs::create_dir_all(&logs_dir)?;
            }

            // Create log file for this process
            // Use short node name for log file (e.g., "@mecha10/listener" -> "listener.log")
            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()))?;

            // Clone for stderr
            let log_file_stderr = log_file.try_clone().context("Failed to clone log file handle")?;

            cmd.stdout(log_file) // Redirect stdout to log file
                .stderr(log_file_stderr); // Redirect stderr to log file
        } else {
            // Disk logging disabled via project config
            // (`environments.<env>.logging.disk_logging_enabled: false`): don't open or
            // write to any file on disk. Remote/network log shipping (handled
            // independently by the node/control-plane) is unaffected by this setting.
            tracing::debug!(
                "📴 Disk logging disabled via project config; discarding stdout/stderr for '{}'",
                name
            );
            cmd.stdout(Stdio::null()).stderr(Stdio::null());
        }

        // On Unix: Create new process group to prevent terminal signals from reaching child processes
        // This ensures Ctrl+C in the terminal only affects the main CLI process, not node-runner
        #[cfg(unix)]
        {
            use std::os::unix::process::CommandExt;
            cmd.process_group(0); // 0 = create new process group with same ID as child PID
        }

        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)
    }

    /// Spawn a process in a specific working directory
    ///
    /// # Arguments
    ///
    /// * `name` - Name to identify the process
    /// * `binary_path` - Path to the binary to execute
    /// * `args` - Command-line arguments
    /// * `working_dir` - Working directory for the process
    pub fn spawn_in_dir(
        &mut self,
        name: &str,
        binary_path: &str,
        args: &[&str],
        working_dir: impl AsRef<Path>,
    ) -> Result<u32> {
        let child = Command::new(binary_path)
            .args(args)
            .current_dir(working_dir)
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .spawn()
            .with_context(|| format!("Failed to spawn process: {}", binary_path))?;

        let pid = child.id();
        self.manager.track(name.to_string(), child);

        Ok(pid)
    }

    /// Spawn multiple node processes from a list
    ///
    /// # Arguments
    ///
    /// * `nodes` - Vec of (name, binary_path, args) tuples
    ///
    /// # Returns
    ///
    /// HashMap of node names to PIDs
    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)
    }
}