...
This commit is contained in:
		
							
								
								
									
										22
									
								
								packages/system/process/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/system/process/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
//! # SAL Process Package
 | 
			
		||||
//!
 | 
			
		||||
//! The `sal-process` package provides functionality for managing and interacting with
 | 
			
		||||
//! system processes across different platforms. It includes capabilities for:
 | 
			
		||||
//!
 | 
			
		||||
//! - Running commands and scripts
 | 
			
		||||
//! - Listing and filtering processes
 | 
			
		||||
//! - Killing processes
 | 
			
		||||
//! - Checking for command existence
 | 
			
		||||
//! - Screen session management
 | 
			
		||||
//!
 | 
			
		||||
//! This package is designed to work consistently across Windows, macOS, and Linux.
 | 
			
		||||
 | 
			
		||||
mod mgmt;
 | 
			
		||||
mod run;
 | 
			
		||||
mod screen;
 | 
			
		||||
 | 
			
		||||
pub mod rhai;
 | 
			
		||||
 | 
			
		||||
pub use mgmt::*;
 | 
			
		||||
pub use run::*;
 | 
			
		||||
pub use screen::{kill as kill_screen, new as new_screen};
 | 
			
		||||
							
								
								
									
										351
									
								
								packages/system/process/src/mgmt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								packages/system/process/src/mgmt.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,351 @@
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::process::Command;
 | 
			
		||||
 | 
			
		||||
/// Error type for process management operations
 | 
			
		||||
///
 | 
			
		||||
/// This enum represents various errors that can occur during process management
 | 
			
		||||
/// operations such as listing, finding, or killing processes.
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum ProcessError {
 | 
			
		||||
    /// An error occurred while executing a command
 | 
			
		||||
    CommandExecutionFailed(io::Error),
 | 
			
		||||
    /// A command executed successfully but returned an error
 | 
			
		||||
    CommandFailed(String),
 | 
			
		||||
    /// No process was found matching the specified pattern
 | 
			
		||||
    NoProcessFound(String),
 | 
			
		||||
    /// Multiple processes were found matching the specified pattern
 | 
			
		||||
    MultipleProcessesFound(String, usize),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Implement Display for ProcessError to provide human-readable error messages
 | 
			
		||||
impl fmt::Display for ProcessError {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            ProcessError::CommandExecutionFailed(e) => {
 | 
			
		||||
                write!(f, "Failed to execute command: {}", e)
 | 
			
		||||
            }
 | 
			
		||||
            ProcessError::CommandFailed(e) => write!(f, "{}", e),
 | 
			
		||||
            ProcessError::NoProcessFound(pattern) => {
 | 
			
		||||
                write!(f, "No processes found matching '{}'", pattern)
 | 
			
		||||
            }
 | 
			
		||||
            ProcessError::MultipleProcessesFound(pattern, count) => write!(
 | 
			
		||||
                f,
 | 
			
		||||
                "Multiple processes ({}) found matching '{}'",
 | 
			
		||||
                count, pattern
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implement Error trait for ProcessError
 | 
			
		||||
impl Error for ProcessError {
 | 
			
		||||
    fn source(&self) -> Option<&(dyn Error + 'static)> {
 | 
			
		||||
        match self {
 | 
			
		||||
            ProcessError::CommandExecutionFailed(e) => Some(e),
 | 
			
		||||
            _ => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Define a struct to represent process information
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct ProcessInfo {
 | 
			
		||||
    pub pid: i64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub memory: f64,
 | 
			
		||||
    pub cpu: f64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if a command exists in PATH.
 | 
			
		||||
 *
 | 
			
		||||
 * # Arguments
 | 
			
		||||
 *
 | 
			
		||||
 * * `cmd` - The command to check
 | 
			
		||||
 *
 | 
			
		||||
 * # Returns
 | 
			
		||||
 *
 | 
			
		||||
 * * `Option<String>` - The full path to the command if found, None otherwise
 | 
			
		||||
 *
 | 
			
		||||
 * # Examples
 | 
			
		||||
 *
 | 
			
		||||
 * ```
 | 
			
		||||
 * use sal_process::which;
 | 
			
		||||
 *
 | 
			
		||||
 * match which("git") {
 | 
			
		||||
 *     Some(path) => println!("Git is installed at: {}", path),
 | 
			
		||||
 *     None => println!("Git is not installed"),
 | 
			
		||||
 * }
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
pub fn which(cmd: &str) -> Option<String> {
 | 
			
		||||
    #[cfg(target_os = "windows")]
 | 
			
		||||
    let which_cmd = "where";
 | 
			
		||||
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    let which_cmd = "which";
 | 
			
		||||
 | 
			
		||||
    let output = Command::new(which_cmd).arg(cmd).output();
 | 
			
		||||
 | 
			
		||||
    match output {
 | 
			
		||||
        Ok(out) => {
 | 
			
		||||
            if out.status.success() {
 | 
			
		||||
                let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
 | 
			
		||||
                Some(path)
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(_) => None,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Kill processes matching a pattern.
 | 
			
		||||
 *
 | 
			
		||||
 * # Arguments
 | 
			
		||||
 *
 | 
			
		||||
 * * `pattern` - The pattern to match against process names
 | 
			
		||||
 *
 | 
			
		||||
 * # Returns
 | 
			
		||||
 *
 | 
			
		||||
 * * `Ok(String)` - A success message indicating processes were killed or none were found
 | 
			
		||||
 * * `Err(ProcessError)` - An error if the kill operation failed
 | 
			
		||||
 *
 | 
			
		||||
 * # Examples
 | 
			
		||||
 *
 | 
			
		||||
 * ```
 | 
			
		||||
 * // Kill all processes with "server" in their name
 | 
			
		||||
 * use sal_process::kill;
 | 
			
		||||
 *
 | 
			
		||||
 * fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
 *     let result = kill("server")?;
 | 
			
		||||
 *     println!("{}", result);
 | 
			
		||||
 *     Ok(())
 | 
			
		||||
 * }
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
pub fn kill(pattern: &str) -> Result<String, ProcessError> {
 | 
			
		||||
    // Platform specific implementation
 | 
			
		||||
    #[cfg(target_os = "windows")]
 | 
			
		||||
    {
 | 
			
		||||
        // On Windows, use taskkill with wildcard support
 | 
			
		||||
        let mut args = vec!["/F"]; // Force kill
 | 
			
		||||
 | 
			
		||||
        if pattern.contains('*') {
 | 
			
		||||
            // If it contains wildcards, use filter
 | 
			
		||||
            args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Otherwise use image name directly
 | 
			
		||||
            args.extend(&["/IM", pattern]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let output = Command::new("taskkill")
 | 
			
		||||
            .args(&args)
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(ProcessError::CommandExecutionFailed)?;
 | 
			
		||||
 | 
			
		||||
        if output.status.success() {
 | 
			
		||||
            Ok("Successfully killed processes".to_string())
 | 
			
		||||
        } else {
 | 
			
		||||
            let error = String::from_utf8_lossy(&output.stderr);
 | 
			
		||||
            if error.is_empty() {
 | 
			
		||||
                let stdout = String::from_utf8_lossy(&output.stdout);
 | 
			
		||||
                if stdout.contains("No tasks") {
 | 
			
		||||
                    Ok("No matching processes found".to_string())
 | 
			
		||||
                } else {
 | 
			
		||||
                    Err(ProcessError::CommandFailed(format!(
 | 
			
		||||
                        "Failed to kill processes: {}",
 | 
			
		||||
                        stdout
 | 
			
		||||
                    )))
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                Err(ProcessError::CommandFailed(format!(
 | 
			
		||||
                    "Failed to kill processes: {}",
 | 
			
		||||
                    error
 | 
			
		||||
                )))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    {
 | 
			
		||||
        // On Unix-like systems, use pkill which has built-in pattern matching
 | 
			
		||||
        let output = Command::new("pkill")
 | 
			
		||||
            .arg("-f") // Match against full process name/args
 | 
			
		||||
            .arg(pattern)
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(ProcessError::CommandExecutionFailed)?;
 | 
			
		||||
 | 
			
		||||
        // pkill returns 0 if processes were killed, 1 if none matched
 | 
			
		||||
        if output.status.success() {
 | 
			
		||||
            Ok("Successfully killed processes".to_string())
 | 
			
		||||
        } else if output.status.code() == Some(1) {
 | 
			
		||||
            Ok("No matching processes found".to_string())
 | 
			
		||||
        } else {
 | 
			
		||||
            let error = String::from_utf8_lossy(&output.stderr);
 | 
			
		||||
            Err(ProcessError::CommandFailed(format!(
 | 
			
		||||
                "Failed to kill processes: {}",
 | 
			
		||||
                error
 | 
			
		||||
            )))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * List processes matching a pattern (or all if pattern is empty).
 | 
			
		||||
 *
 | 
			
		||||
 * # Arguments
 | 
			
		||||
 *
 | 
			
		||||
 * * `pattern` - The pattern to match against process names (empty string for all processes)
 | 
			
		||||
 *
 | 
			
		||||
 * # Returns
 | 
			
		||||
 *
 | 
			
		||||
 * * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
 | 
			
		||||
 * * `Err(ProcessError)` - An error if the list operation failed
 | 
			
		||||
 *
 | 
			
		||||
 * # Examples
 | 
			
		||||
 *
 | 
			
		||||
 * ```
 | 
			
		||||
 * // List all processes
 | 
			
		||||
 * use sal_process::process_list;
 | 
			
		||||
 *
 | 
			
		||||
 * fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
 *     let processes = process_list("")?;
 | 
			
		||||
 *
 | 
			
		||||
 *     // List processes with "server" in their name
 | 
			
		||||
 *     let processes = process_list("server")?;
 | 
			
		||||
 *     for proc in processes {
 | 
			
		||||
 *         println!("PID: {}, Name: {}", proc.pid, proc.name);
 | 
			
		||||
 *     }
 | 
			
		||||
 *     Ok(())
 | 
			
		||||
 * }
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
 | 
			
		||||
    let mut processes = Vec::new();
 | 
			
		||||
 | 
			
		||||
    // Platform specific implementations
 | 
			
		||||
    #[cfg(target_os = "windows")]
 | 
			
		||||
    {
 | 
			
		||||
        // Windows implementation using wmic
 | 
			
		||||
        let output = Command::new("wmic")
 | 
			
		||||
            .args(&["process", "list", "brief"])
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(ProcessError::CommandExecutionFailed)?;
 | 
			
		||||
 | 
			
		||||
        if output.status.success() {
 | 
			
		||||
            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
 | 
			
		||||
 | 
			
		||||
            // Parse output (assuming format: Handle Name Priority)
 | 
			
		||||
            for line in stdout.lines().skip(1) {
 | 
			
		||||
                // Skip header
 | 
			
		||||
                let parts: Vec<&str> = line.trim().split_whitespace().collect();
 | 
			
		||||
                if parts.len() >= 2 {
 | 
			
		||||
                    let pid = parts[0].parse::<i64>().unwrap_or(0);
 | 
			
		||||
                    let name = parts[1].to_string();
 | 
			
		||||
 | 
			
		||||
                    // Filter by pattern if provided
 | 
			
		||||
                    if !pattern.is_empty() && !name.contains(pattern) {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    processes.push(ProcessInfo {
 | 
			
		||||
                        pid,
 | 
			
		||||
                        name,
 | 
			
		||||
                        memory: 0.0, // Placeholder
 | 
			
		||||
                        cpu: 0.0,    // Placeholder
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
 | 
			
		||||
            return Err(ProcessError::CommandFailed(format!(
 | 
			
		||||
                "Failed to list processes: {}",
 | 
			
		||||
                stderr
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    {
 | 
			
		||||
        // Unix implementation using ps
 | 
			
		||||
        let output = Command::new("ps")
 | 
			
		||||
            .args(&["-eo", "pid,comm"])
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(ProcessError::CommandExecutionFailed)?;
 | 
			
		||||
 | 
			
		||||
        if output.status.success() {
 | 
			
		||||
            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
 | 
			
		||||
 | 
			
		||||
            // Parse output (assuming format: PID COMMAND)
 | 
			
		||||
            for line in stdout.lines().skip(1) {
 | 
			
		||||
                // Skip header
 | 
			
		||||
                let parts: Vec<&str> = line.trim().split_whitespace().collect();
 | 
			
		||||
                if parts.len() >= 2 {
 | 
			
		||||
                    let pid = parts[0].parse::<i64>().unwrap_or(0);
 | 
			
		||||
                    let name = parts[1].to_string();
 | 
			
		||||
 | 
			
		||||
                    // Filter by pattern if provided
 | 
			
		||||
                    if !pattern.is_empty() && !name.contains(pattern) {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    processes.push(ProcessInfo {
 | 
			
		||||
                        pid,
 | 
			
		||||
                        name,
 | 
			
		||||
                        memory: 0.0, // Placeholder
 | 
			
		||||
                        cpu: 0.0,    // Placeholder
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
 | 
			
		||||
            return Err(ProcessError::CommandFailed(format!(
 | 
			
		||||
                "Failed to list processes: {}",
 | 
			
		||||
                stderr
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(processes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get a single process matching the pattern (error if 0 or more than 1 match).
 | 
			
		||||
 *
 | 
			
		||||
 * # Arguments
 | 
			
		||||
 *
 | 
			
		||||
 * * `pattern` - The pattern to match against process names
 | 
			
		||||
 *
 | 
			
		||||
 * # Returns
 | 
			
		||||
 *
 | 
			
		||||
 * * `Ok(ProcessInfo)` - Information about the matching process
 | 
			
		||||
 * * `Err(ProcessError)` - An error if no process or multiple processes match
 | 
			
		||||
 *
 | 
			
		||||
 * # Examples
 | 
			
		||||
 *
 | 
			
		||||
 * ```no_run
 | 
			
		||||
 * use sal_process::process_get;
 | 
			
		||||
 *
 | 
			
		||||
 * fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
 *     let process = process_get("unique-server-name")?;
 | 
			
		||||
 *     println!("Found process: {} (PID: {})", process.name, process.pid);
 | 
			
		||||
 *     Ok(())
 | 
			
		||||
 * }
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
 | 
			
		||||
    let processes = process_list(pattern)?;
 | 
			
		||||
 | 
			
		||||
    match processes.len() {
 | 
			
		||||
        0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
 | 
			
		||||
        1 => Ok(processes[0].clone()),
 | 
			
		||||
        _ => Err(ProcessError::MultipleProcessesFound(
 | 
			
		||||
            pattern.to_string(),
 | 
			
		||||
            processes.len(),
 | 
			
		||||
        )),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										212
									
								
								packages/system/process/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								packages/system/process/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,212 @@
 | 
			
		||||
//! Rhai wrappers for Process module functions
 | 
			
		||||
//!
 | 
			
		||||
//! This module provides Rhai wrappers for the functions in the Process module.
 | 
			
		||||
 | 
			
		||||
use crate::{self as process, CommandResult, ProcessError, ProcessInfo, RunError};
 | 
			
		||||
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
 | 
			
		||||
use std::clone::Clone;
 | 
			
		||||
 | 
			
		||||
/// Register Process module functions with the Rhai engine
 | 
			
		||||
///
 | 
			
		||||
/// # Arguments
 | 
			
		||||
///
 | 
			
		||||
/// * `engine` - The Rhai engine to register the functions with
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
 | 
			
		||||
pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    // Register types
 | 
			
		||||
    // register_process_types(engine)?; // Removed
 | 
			
		||||
 | 
			
		||||
    // Register CommandResult type and its methods
 | 
			
		||||
    engine.register_type_with_name::<CommandResult>("CommandResult");
 | 
			
		||||
    engine.register_get("stdout", |r: &mut CommandResult| r.stdout.clone());
 | 
			
		||||
    engine.register_get("stderr", |r: &mut CommandResult| r.stderr.clone());
 | 
			
		||||
    engine.register_get("success", |r: &mut CommandResult| r.success);
 | 
			
		||||
    engine.register_get("code", |r: &mut CommandResult| r.code);
 | 
			
		||||
 | 
			
		||||
    // Register ProcessInfo type and its methods
 | 
			
		||||
    engine.register_type_with_name::<ProcessInfo>("ProcessInfo");
 | 
			
		||||
    engine.register_get("pid", |p: &mut ProcessInfo| p.pid);
 | 
			
		||||
    engine.register_get("name", |p: &mut ProcessInfo| p.name.clone());
 | 
			
		||||
    engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
 | 
			
		||||
    engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
 | 
			
		||||
 | 
			
		||||
    // Register CommandBuilder type and its methods
 | 
			
		||||
    engine.register_type_with_name::<RhaiCommandBuilder>("CommandBuilder");
 | 
			
		||||
    engine.register_fn("run", RhaiCommandBuilder::new_rhai); // This is the builder entry point
 | 
			
		||||
    engine.register_fn("silent", RhaiCommandBuilder::silent); // Method on CommandBuilder
 | 
			
		||||
    engine.register_fn("ignore_error", RhaiCommandBuilder::ignore_error); // Method on CommandBuilder
 | 
			
		||||
    engine.register_fn("log", RhaiCommandBuilder::log); // Method on CommandBuilder
 | 
			
		||||
    engine.register_fn("execute", RhaiCommandBuilder::execute_command); // Method on CommandBuilder
 | 
			
		||||
 | 
			
		||||
    // Register other process management functions
 | 
			
		||||
    engine.register_fn("which", which);
 | 
			
		||||
    engine.register_fn("kill", kill);
 | 
			
		||||
    engine.register_fn("process_list", process_list);
 | 
			
		||||
    engine.register_fn("process_get", process_get);
 | 
			
		||||
 | 
			
		||||
    // Register legacy functions for backward compatibility
 | 
			
		||||
    engine.register_fn("run_command", run_command);
 | 
			
		||||
    engine.register_fn("run_silent", run_silent);
 | 
			
		||||
    engine.register_fn("run", run_with_options);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper functions for error conversion
 | 
			
		||||
fn run_error_to_rhai_error<T>(result: Result<T, RunError>) -> Result<T, Box<EvalAltResult>> {
 | 
			
		||||
    result.map_err(|e| {
 | 
			
		||||
        Box::new(EvalAltResult::ErrorRuntime(
 | 
			
		||||
            format!("Run error: {}", e).into(),
 | 
			
		||||
            rhai::Position::NONE,
 | 
			
		||||
        ))
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Define a Rhai-facing builder struct
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
struct RhaiCommandBuilder {
 | 
			
		||||
    command: String,
 | 
			
		||||
    die_on_error: bool,
 | 
			
		||||
    is_silent: bool,
 | 
			
		||||
    enable_log: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RhaiCommandBuilder {
 | 
			
		||||
    // Constructor function for Rhai (registered as `run`)
 | 
			
		||||
    pub fn new_rhai(command: &str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            command: command.to_string(),
 | 
			
		||||
            die_on_error: true, // Default: die on error
 | 
			
		||||
            is_silent: false,
 | 
			
		||||
            enable_log: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rhai method: .silent()
 | 
			
		||||
    pub fn silent(mut self) -> Self {
 | 
			
		||||
        self.is_silent = true;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rhai method: .ignore_error()
 | 
			
		||||
    pub fn ignore_error(mut self) -> Self {
 | 
			
		||||
        self.die_on_error = false;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rhai method: .log()
 | 
			
		||||
    pub fn log(mut self) -> Self {
 | 
			
		||||
        self.enable_log = true;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rhai method: .execute() - Execute the command
 | 
			
		||||
    pub fn execute_command(self) -> Result<CommandResult, Box<EvalAltResult>> {
 | 
			
		||||
        let builder = process::run(&self.command)
 | 
			
		||||
            .die(self.die_on_error)
 | 
			
		||||
            .silent(self.is_silent)
 | 
			
		||||
            .log(self.enable_log);
 | 
			
		||||
 | 
			
		||||
        // Execute the command
 | 
			
		||||
        run_error_to_rhai_error(builder.execute())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn process_error_to_rhai_error<T>(
 | 
			
		||||
    result: Result<T, ProcessError>,
 | 
			
		||||
) -> Result<T, Box<EvalAltResult>> {
 | 
			
		||||
    result.map_err(|e| {
 | 
			
		||||
        Box::new(EvalAltResult::ErrorRuntime(
 | 
			
		||||
            format!("Process error: {}", e).into(),
 | 
			
		||||
            rhai::Position::NONE,
 | 
			
		||||
        ))
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// Process Management Function Wrappers
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
/// Wrapper for process::which
 | 
			
		||||
///
 | 
			
		||||
/// Check if a command exists in PATH.
 | 
			
		||||
pub fn which(cmd: &str) -> Dynamic {
 | 
			
		||||
    match process::which(cmd) {
 | 
			
		||||
        Some(path) => path.into(),
 | 
			
		||||
        None => Dynamic::UNIT,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Wrapper for process::kill
 | 
			
		||||
///
 | 
			
		||||
/// Kill processes matching a pattern.
 | 
			
		||||
pub fn kill(pattern: &str) -> Result<String, Box<EvalAltResult>> {
 | 
			
		||||
    process_error_to_rhai_error(process::kill(pattern))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Wrapper for process::process_list
 | 
			
		||||
///
 | 
			
		||||
/// List processes matching a pattern (or all if pattern is empty).
 | 
			
		||||
pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
 | 
			
		||||
    let processes = process_error_to_rhai_error(process::process_list(pattern))?;
 | 
			
		||||
 | 
			
		||||
    // Convert Vec<ProcessInfo> to Rhai Array
 | 
			
		||||
    let mut array = Array::new();
 | 
			
		||||
    for process in processes {
 | 
			
		||||
        array.push(Dynamic::from(process));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(array)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Wrapper for process::process_get
 | 
			
		||||
///
 | 
			
		||||
/// Get a single process matching the pattern (error if 0 or more than 1 match).
 | 
			
		||||
pub fn process_get(pattern: &str) -> Result<ProcessInfo, Box<EvalAltResult>> {
 | 
			
		||||
    process_error_to_rhai_error(process::process_get(pattern))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Legacy wrapper for process::run
 | 
			
		||||
///
 | 
			
		||||
/// Run a command and return the result.
 | 
			
		||||
pub fn run_command(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
 | 
			
		||||
    run_error_to_rhai_error(process::run(cmd).execute())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Legacy wrapper for process::run with silent option
 | 
			
		||||
///
 | 
			
		||||
/// Run a command silently and return the result.
 | 
			
		||||
pub fn run_silent(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
 | 
			
		||||
    run_error_to_rhai_error(process::run(cmd).silent(true).execute())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Legacy wrapper for process::run with options
 | 
			
		||||
///
 | 
			
		||||
/// Run a command with options and return the result.
 | 
			
		||||
pub fn run_with_options(cmd: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
 | 
			
		||||
    let mut builder = process::run(cmd);
 | 
			
		||||
 | 
			
		||||
    // Apply options
 | 
			
		||||
    if let Some(silent) = options.get("silent") {
 | 
			
		||||
        if let Ok(silent_bool) = silent.as_bool() {
 | 
			
		||||
            builder = builder.silent(silent_bool);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(die) = options.get("die") {
 | 
			
		||||
        if let Ok(die_bool) = die.as_bool() {
 | 
			
		||||
            builder = builder.die(die_bool);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(log) = options.get("log") {
 | 
			
		||||
        if let Ok(log_bool) = log.as_bool() {
 | 
			
		||||
            builder = builder.log(log_bool);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    run_error_to_rhai_error(builder.execute())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										535
									
								
								packages/system/process/src/run.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										535
									
								
								packages/system/process/src/run.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,535 @@
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::fs::{self, File};
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::io::{BufRead, BufReader, Write};
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::process::{Child, Command, Output, Stdio};
 | 
			
		||||
use std::thread;
 | 
			
		||||
 | 
			
		||||
use sal_text;
 | 
			
		||||
 | 
			
		||||
/// Error type for command and script execution operations
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum RunError {
 | 
			
		||||
    /// The command string was empty
 | 
			
		||||
    EmptyCommand,
 | 
			
		||||
    /// An error occurred while executing a command
 | 
			
		||||
    CommandExecutionFailed(io::Error),
 | 
			
		||||
    /// A command executed successfully but returned an error
 | 
			
		||||
    CommandFailed(String),
 | 
			
		||||
    /// An error occurred while preparing a script for execution
 | 
			
		||||
    ScriptPreparationFailed(String),
 | 
			
		||||
    /// An error occurred in a child process
 | 
			
		||||
    ChildProcessError(String),
 | 
			
		||||
    /// Failed to create a temporary directory
 | 
			
		||||
    TempDirCreationFailed(io::Error),
 | 
			
		||||
    /// Failed to create a script file
 | 
			
		||||
    FileCreationFailed(io::Error),
 | 
			
		||||
    /// Failed to write to a script file
 | 
			
		||||
    FileWriteFailed(io::Error),
 | 
			
		||||
    /// Failed to set file permissions
 | 
			
		||||
    PermissionError(io::Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Implement Display for RunError to provide human-readable error messages
 | 
			
		||||
impl fmt::Display for RunError {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            RunError::EmptyCommand => write!(f, "Empty command"),
 | 
			
		||||
            RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
 | 
			
		||||
            RunError::CommandFailed(e) => write!(f, "{}", e),
 | 
			
		||||
            RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
 | 
			
		||||
            RunError::ChildProcessError(e) => write!(f, "{}", e),
 | 
			
		||||
            RunError::TempDirCreationFailed(e) => {
 | 
			
		||||
                write!(f, "Failed to create temporary directory: {}", e)
 | 
			
		||||
            }
 | 
			
		||||
            RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e),
 | 
			
		||||
            RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e),
 | 
			
		||||
            RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implement Error trait for RunError
 | 
			
		||||
impl Error for RunError {
 | 
			
		||||
    fn source(&self) -> Option<&(dyn Error + 'static)> {
 | 
			
		||||
        match self {
 | 
			
		||||
            RunError::CommandExecutionFailed(e) => Some(e),
 | 
			
		||||
            RunError::TempDirCreationFailed(e) => Some(e),
 | 
			
		||||
            RunError::FileCreationFailed(e) => Some(e),
 | 
			
		||||
            RunError::FileWriteFailed(e) => Some(e),
 | 
			
		||||
            RunError::PermissionError(e) => Some(e),
 | 
			
		||||
            _ => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A structure to hold command execution results
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct CommandResult {
 | 
			
		||||
    pub stdout: String,
 | 
			
		||||
    pub stderr: String,
 | 
			
		||||
    pub success: bool,
 | 
			
		||||
    pub code: i32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommandResult {
 | 
			
		||||
    // Implementation methods can be added here as needed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Prepare a script file and return the path and interpreter
 | 
			
		||||
fn prepare_script_file(
 | 
			
		||||
    script_content: &str,
 | 
			
		||||
) -> Result<(PathBuf, String, tempfile::TempDir), RunError> {
 | 
			
		||||
    // Dedent the script
 | 
			
		||||
    let dedented = sal_text::dedent(script_content);
 | 
			
		||||
 | 
			
		||||
    // Create a temporary directory
 | 
			
		||||
    let temp_dir = tempfile::tempdir().map_err(RunError::TempDirCreationFailed)?;
 | 
			
		||||
 | 
			
		||||
    // Determine script extension and interpreter
 | 
			
		||||
    #[cfg(target_os = "windows")]
 | 
			
		||||
    let (ext, interpreter) = (".bat", "cmd.exe".to_string());
 | 
			
		||||
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    let (ext, interpreter) = (".sh", "/bin/bash".to_string());
 | 
			
		||||
 | 
			
		||||
    // Create the script file
 | 
			
		||||
    let script_path = temp_dir.path().join(format!("script{}", ext));
 | 
			
		||||
    let mut file = File::create(&script_path).map_err(RunError::FileCreationFailed)?;
 | 
			
		||||
 | 
			
		||||
    // For Unix systems, ensure the script has a shebang line with -e flag
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    {
 | 
			
		||||
        let script_with_shebang = if dedented.trim_start().starts_with("#!") {
 | 
			
		||||
            // Script already has a shebang, use it as is
 | 
			
		||||
            dedented
 | 
			
		||||
        } else {
 | 
			
		||||
            // Add shebang with -e flag to ensure script fails on errors
 | 
			
		||||
            format!("#!/bin/bash -e\n{}", dedented)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Write the script content with shebang
 | 
			
		||||
        file.write_all(script_with_shebang.as_bytes())
 | 
			
		||||
            .map_err(RunError::FileWriteFailed)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For Windows, just write the script as is
 | 
			
		||||
    #[cfg(target_os = "windows")]
 | 
			
		||||
    {
 | 
			
		||||
        file.write_all(dedented.as_bytes())
 | 
			
		||||
            .map_err(RunError::FileWriteFailed)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Make the script executable (Unix only)
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    {
 | 
			
		||||
        use std::os::unix::fs::PermissionsExt;
 | 
			
		||||
        let mut perms = fs::metadata(&script_path)
 | 
			
		||||
            .map_err(|e| RunError::PermissionError(e))?
 | 
			
		||||
            .permissions();
 | 
			
		||||
        perms.set_mode(0o755); // rwxr-xr-x
 | 
			
		||||
        fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok((script_path, interpreter, temp_dir))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Capture output from Child's stdio streams with optional printing
 | 
			
		||||
fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> {
 | 
			
		||||
    // Prepare to read stdout & stderr line-by-line
 | 
			
		||||
    let stdout = child.stdout.take();
 | 
			
		||||
    let stderr = child.stderr.take();
 | 
			
		||||
 | 
			
		||||
    // Process stdout
 | 
			
		||||
    let stdout_handle = if let Some(out) = stdout {
 | 
			
		||||
        let reader = BufReader::new(out);
 | 
			
		||||
        let silent_clone = silent;
 | 
			
		||||
        // Spawn a thread to capture and optionally print stdout
 | 
			
		||||
        Some(std::thread::spawn(move || {
 | 
			
		||||
            let mut local_buffer = String::new();
 | 
			
		||||
            for line in reader.lines() {
 | 
			
		||||
                if let Ok(l) = line {
 | 
			
		||||
                    // Print the line if not silent and flush immediately
 | 
			
		||||
                    if !silent_clone {
 | 
			
		||||
                        println!("{}", l);
 | 
			
		||||
                        std::io::stdout().flush().unwrap_or(());
 | 
			
		||||
                    }
 | 
			
		||||
                    // Store it in our captured buffer
 | 
			
		||||
                    local_buffer.push_str(&l);
 | 
			
		||||
                    local_buffer.push('\n');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            local_buffer
 | 
			
		||||
        }))
 | 
			
		||||
    } else {
 | 
			
		||||
        None
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Process stderr
 | 
			
		||||
    let stderr_handle = if let Some(err) = stderr {
 | 
			
		||||
        let reader = BufReader::new(err);
 | 
			
		||||
        let silent_clone = silent;
 | 
			
		||||
        // Spawn a thread to capture and optionally print stderr
 | 
			
		||||
        Some(std::thread::spawn(move || {
 | 
			
		||||
            let mut local_buffer = String::new();
 | 
			
		||||
            for line in reader.lines() {
 | 
			
		||||
                if let Ok(l) = line {
 | 
			
		||||
                    // Print the line if not silent and flush immediately
 | 
			
		||||
                    if !silent_clone {
 | 
			
		||||
                        // Print all stderr messages
 | 
			
		||||
                        eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
 | 
			
		||||
                        std::io::stderr().flush().unwrap_or(());
 | 
			
		||||
                    }
 | 
			
		||||
                    // Store it in our captured buffer
 | 
			
		||||
                    local_buffer.push_str(&l);
 | 
			
		||||
                    local_buffer.push('\n');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            local_buffer
 | 
			
		||||
        }))
 | 
			
		||||
    } else {
 | 
			
		||||
        None
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Wait for the child process to exit
 | 
			
		||||
    let status = child.wait().map_err(|e| {
 | 
			
		||||
        RunError::ChildProcessError(format!("Failed to wait on child process: {}", e))
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    // Join our stdout thread if it exists
 | 
			
		||||
    let captured_stdout = if let Some(handle) = stdout_handle {
 | 
			
		||||
        handle.join().unwrap_or_default()
 | 
			
		||||
    } else {
 | 
			
		||||
        "Failed to capture stdout".to_string()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Join our stderr thread if it exists
 | 
			
		||||
    let captured_stderr = if let Some(handle) = stderr_handle {
 | 
			
		||||
        handle.join().unwrap_or_default()
 | 
			
		||||
    } else {
 | 
			
		||||
        "Failed to capture stderr".to_string()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // If the command failed, print the stderr if it wasn't already printed
 | 
			
		||||
    if !status.success() && silent && !captured_stderr.is_empty() {
 | 
			
		||||
        eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
 | 
			
		||||
        for line in captured_stderr.lines() {
 | 
			
		||||
            eprintln!("\x1b[31m{}\x1b[0m", line);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Return the command result
 | 
			
		||||
    Ok(CommandResult {
 | 
			
		||||
        stdout: captured_stdout,
 | 
			
		||||
        stderr: captured_stderr,
 | 
			
		||||
        success: status.success(),
 | 
			
		||||
        code: status.code().unwrap_or(-1),
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Processes Output structure from Command::output() into CommandResult
 | 
			
		||||
fn process_command_output(
 | 
			
		||||
    output: Result<Output, std::io::Error>,
 | 
			
		||||
) -> Result<CommandResult, RunError> {
 | 
			
		||||
    match output {
 | 
			
		||||
        Ok(out) => {
 | 
			
		||||
            let stdout = String::from_utf8_lossy(&out.stdout).to_string();
 | 
			
		||||
            let stderr = String::from_utf8_lossy(&out.stderr).to_string();
 | 
			
		||||
            // We'll collect stderr but not print it here
 | 
			
		||||
            // It will be included in the error message if the command fails
 | 
			
		||||
 | 
			
		||||
            // If the command failed, print a clear error message
 | 
			
		||||
            if !out.status.success() {
 | 
			
		||||
                eprintln!(
 | 
			
		||||
                    "\x1b[31mCommand failed with exit code: {}\x1b[0m",
 | 
			
		||||
                    out.status.code().unwrap_or(-1)
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Ok(CommandResult {
 | 
			
		||||
                stdout,
 | 
			
		||||
                stderr,
 | 
			
		||||
                success: out.status.success(),
 | 
			
		||||
                code: out.status.code().unwrap_or(-1),
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => Err(RunError::CommandExecutionFailed(e)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Common logic for running a command with optional silent mode
 | 
			
		||||
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
 | 
			
		||||
    let mut parts = command.split_whitespace();
 | 
			
		||||
    let cmd = match parts.next() {
 | 
			
		||||
        Some(c) => c,
 | 
			
		||||
        None => return Err(RunError::EmptyCommand),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let args: Vec<&str> = parts.collect();
 | 
			
		||||
 | 
			
		||||
    // Spawn the child process with piped stdout & stderr
 | 
			
		||||
    let child = Command::new(cmd)
 | 
			
		||||
        .args(&args)
 | 
			
		||||
        .stdout(Stdio::piped())
 | 
			
		||||
        .stderr(Stdio::piped())
 | 
			
		||||
        .spawn()
 | 
			
		||||
        .map_err(RunError::CommandExecutionFailed)?;
 | 
			
		||||
 | 
			
		||||
    handle_child_output(child, silent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Execute a script with the given interpreter and path
 | 
			
		||||
fn execute_script_internal(
 | 
			
		||||
    interpreter: &str,
 | 
			
		||||
    script_path: &Path,
 | 
			
		||||
    silent: bool,
 | 
			
		||||
) -> Result<CommandResult, RunError> {
 | 
			
		||||
    #[cfg(target_os = "windows")]
 | 
			
		||||
    let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
 | 
			
		||||
 | 
			
		||||
    #[cfg(any(target_os = "macos", target_os = "linux"))]
 | 
			
		||||
    let command_args = vec!["-e", script_path.to_str().unwrap_or("")];
 | 
			
		||||
 | 
			
		||||
    if silent {
 | 
			
		||||
        // For silent execution, use output() which captures but doesn't display
 | 
			
		||||
        let output = Command::new(interpreter).args(&command_args).output();
 | 
			
		||||
 | 
			
		||||
        let result = process_command_output(output)?;
 | 
			
		||||
 | 
			
		||||
        // If the script failed, return an error
 | 
			
		||||
        if !result.success {
 | 
			
		||||
            return Err(RunError::CommandFailed(format!(
 | 
			
		||||
                "Script execution failed with exit code {}: {}",
 | 
			
		||||
                result.code,
 | 
			
		||||
                result.stderr.trim()
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    } else {
 | 
			
		||||
        // For normal execution, spawn and handle the output streams
 | 
			
		||||
        let child = Command::new(interpreter)
 | 
			
		||||
            .args(&command_args)
 | 
			
		||||
            .stdout(Stdio::piped())
 | 
			
		||||
            .stderr(Stdio::piped())
 | 
			
		||||
            .spawn()
 | 
			
		||||
            .map_err(RunError::CommandExecutionFailed)?;
 | 
			
		||||
 | 
			
		||||
        let result = handle_child_output(child, false)?;
 | 
			
		||||
 | 
			
		||||
        // If the script failed, return an error
 | 
			
		||||
        if !result.success {
 | 
			
		||||
            return Err(RunError::CommandFailed(format!(
 | 
			
		||||
                "Script execution failed with exit code {}: {}",
 | 
			
		||||
                result.code,
 | 
			
		||||
                result.stderr.trim()
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Run a multiline script with optional silent mode
 | 
			
		||||
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
 | 
			
		||||
    // Prepare the script file first to get the content with shebang
 | 
			
		||||
    let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
 | 
			
		||||
 | 
			
		||||
    // Print the script being executed if not silent
 | 
			
		||||
    if !silent {
 | 
			
		||||
        println!("\x1b[36mExecuting script:\x1b[0m");
 | 
			
		||||
 | 
			
		||||
        // Read the script file to get the content with shebang
 | 
			
		||||
        if let Ok(script_content) = fs::read_to_string(&script_path) {
 | 
			
		||||
            for (i, line) in script_content.lines().enumerate() {
 | 
			
		||||
                println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Fallback to original script if reading fails
 | 
			
		||||
            for (i, line) in script.lines().enumerate() {
 | 
			
		||||
                println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        println!("\x1b[36m---\x1b[0m");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // _temp_dir is kept in scope until the end of this function to ensure
 | 
			
		||||
    // it's not dropped prematurely, which would clean up the directory
 | 
			
		||||
 | 
			
		||||
    // Execute the script and handle the result
 | 
			
		||||
    let result = execute_script_internal(&interpreter, &script_path, silent);
 | 
			
		||||
 | 
			
		||||
    // If there was an error, print a clear error message only if it's not a CommandFailed error
 | 
			
		||||
    // (which would already have printed the stderr)
 | 
			
		||||
    if let Err(ref e) = result {
 | 
			
		||||
        if !matches!(e, RunError::CommandFailed(_)) {
 | 
			
		||||
            eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A builder for configuring and executing commands or scripts
 | 
			
		||||
pub struct RunBuilder<'a> {
 | 
			
		||||
    /// The command or script to run
 | 
			
		||||
    cmd: &'a str,
 | 
			
		||||
    /// Whether to return an error if the command fails (default: true)
 | 
			
		||||
    die: bool,
 | 
			
		||||
    /// Whether to suppress output to stdout/stderr (default: false)
 | 
			
		||||
    silent: bool,
 | 
			
		||||
    /// Whether to run the command asynchronously (default: false)
 | 
			
		||||
    async_exec: bool,
 | 
			
		||||
    /// Whether to log command execution (default: false)
 | 
			
		||||
    log: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> RunBuilder<'a> {
 | 
			
		||||
    /// Create a new RunBuilder with default settings
 | 
			
		||||
    pub fn new(cmd: &'a str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            cmd,
 | 
			
		||||
            die: true,
 | 
			
		||||
            silent: false,
 | 
			
		||||
            async_exec: false,
 | 
			
		||||
            log: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set whether to return an error if the command fails
 | 
			
		||||
    pub fn die(mut self, die: bool) -> Self {
 | 
			
		||||
        self.die = die;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set whether to suppress output to stdout/stderr
 | 
			
		||||
    pub fn silent(mut self, silent: bool) -> Self {
 | 
			
		||||
        self.silent = silent;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set whether to run the command asynchronously
 | 
			
		||||
    pub fn async_exec(mut self, async_exec: bool) -> Self {
 | 
			
		||||
        self.async_exec = async_exec;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set whether to log command execution
 | 
			
		||||
    pub fn log(mut self, log: bool) -> Self {
 | 
			
		||||
        self.log = log;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Execute the command or script with the configured options
 | 
			
		||||
    pub fn execute(self) -> Result<CommandResult, RunError> {
 | 
			
		||||
        let trimmed = self.cmd.trim();
 | 
			
		||||
 | 
			
		||||
        // Log command execution if enabled
 | 
			
		||||
        if self.log {
 | 
			
		||||
            println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Handle async execution
 | 
			
		||||
        if self.async_exec {
 | 
			
		||||
            let cmd_copy = trimmed.to_string();
 | 
			
		||||
            let silent = self.silent;
 | 
			
		||||
            let log = self.log;
 | 
			
		||||
 | 
			
		||||
            // Spawn a thread to run the command asynchronously
 | 
			
		||||
            thread::spawn(move || {
 | 
			
		||||
                if log {
 | 
			
		||||
                    println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let result = if cmd_copy.contains('\n') {
 | 
			
		||||
                    run_script_internal(&cmd_copy, silent)
 | 
			
		||||
                } else {
 | 
			
		||||
                    run_command_internal(&cmd_copy, silent)
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if log {
 | 
			
		||||
                    match &result {
 | 
			
		||||
                        Ok(res) => {
 | 
			
		||||
                            if res.success {
 | 
			
		||||
                                println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
 | 
			
		||||
                            } else {
 | 
			
		||||
                                eprintln!(
 | 
			
		||||
                                    "\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m",
 | 
			
		||||
                                    res.code
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Return a placeholder result for async execution
 | 
			
		||||
            return Ok(CommandResult {
 | 
			
		||||
                stdout: String::new(),
 | 
			
		||||
                stderr: String::new(),
 | 
			
		||||
                success: true,
 | 
			
		||||
                code: 0,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Execute the command or script
 | 
			
		||||
        let result = if trimmed.contains('\n') {
 | 
			
		||||
            // This is a multiline script
 | 
			
		||||
            run_script_internal(trimmed, self.silent)
 | 
			
		||||
        } else {
 | 
			
		||||
            // This is a single command
 | 
			
		||||
            run_command_internal(trimmed, self.silent)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Handle die=false: convert errors to CommandResult with success=false
 | 
			
		||||
        match result {
 | 
			
		||||
            Ok(res) => {
 | 
			
		||||
                // If the command failed but die is false, print a warning
 | 
			
		||||
                if !res.success && !self.die && !self.silent {
 | 
			
		||||
                    eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
 | 
			
		||||
                }
 | 
			
		||||
                Ok(res)
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                // Print the error only if it's not a CommandFailed error
 | 
			
		||||
                // (which would already have printed the stderr)
 | 
			
		||||
                if !matches!(e, RunError::CommandFailed(_)) {
 | 
			
		||||
                    eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if self.die {
 | 
			
		||||
                    Err(e)
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Convert error to CommandResult with success=false
 | 
			
		||||
                    Ok(CommandResult {
 | 
			
		||||
                        stdout: String::new(),
 | 
			
		||||
                        stderr: format!("Error: {}", e),
 | 
			
		||||
                        success: false,
 | 
			
		||||
                        code: -1,
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a new RunBuilder for executing a command or script
 | 
			
		||||
pub fn run(cmd: &str) -> RunBuilder {
 | 
			
		||||
    RunBuilder::new(cmd)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Run a command or multiline script with arguments
 | 
			
		||||
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
 | 
			
		||||
    run(command).execute()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Run a command or multiline script with arguments silently
 | 
			
		||||
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
 | 
			
		||||
    run(command).silent(true).execute()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								packages/system/process/src/screen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								packages/system/process/src/screen.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
use crate::run_command;
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use std::fs;
 | 
			
		||||
 | 
			
		||||
/// Executes a command in a new screen session.
 | 
			
		||||
///
 | 
			
		||||
/// # Arguments
 | 
			
		||||
///
 | 
			
		||||
/// * `name` - The name of the screen session.
 | 
			
		||||
/// * `cmd` - The command to execute.
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// * `Result<()>` - Ok if the command was executed successfully, otherwise an error.
 | 
			
		||||
pub fn new(name: &str, cmd: &str) -> Result<()> {
 | 
			
		||||
    let script_path = format!("/tmp/cmd_{}.sh", name);
 | 
			
		||||
    let mut script_content = String::new();
 | 
			
		||||
 | 
			
		||||
    if !cmd.starts_with("#!") {
 | 
			
		||||
        script_content.push_str("#!/bin/bash\n");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    script_content.push_str("set -e\n");
 | 
			
		||||
    script_content.push_str(cmd);
 | 
			
		||||
 | 
			
		||||
    fs::write(&script_path, script_content)?;
 | 
			
		||||
    fs::set_permissions(
 | 
			
		||||
        &script_path,
 | 
			
		||||
        std::os::unix::fs::PermissionsExt::from_mode(0o755),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    let screen_cmd = format!("screen -d -m -S {} {}", name, script_path);
 | 
			
		||||
    run_command(&screen_cmd)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Kills a screen session.
 | 
			
		||||
///
 | 
			
		||||
/// # Arguments
 | 
			
		||||
///
 | 
			
		||||
/// * `name` - The name of the screen session to kill.
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// * `Result<()>` - Ok if the session was killed successfully, otherwise an error.
 | 
			
		||||
pub fn kill(name: &str) -> Result<()> {
 | 
			
		||||
    let cmd = format!("screen -S {} -X quit", name);
 | 
			
		||||
    run_command(&cmd)?;
 | 
			
		||||
    std::thread::sleep(std::time::Duration::from_millis(500));
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user