This commit is contained in:
2025-04-02 07:33:57 +02:00
parent d8de0d7ebf
commit 5678a9aa35
16 changed files with 2467 additions and 0 deletions

493
src/process/run.rs Normal file
View File

@@ -0,0 +1,493 @@
use std::io::{BufRead, BufReader, Write};
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::collections::HashMap;
use std::fmt;
use std::error::Error;
use std::io;
use super::text;
// Define a custom error type for run operations
#[derive(Debug)]
pub enum RunError {
EmptyCommand,
CommandExecutionFailed(io::Error),
CommandFailed(String),
ScriptPreparationFailed(String),
ChildProcessError(String),
TempDirCreationFailed(io::Error),
FileCreationFailed(io::Error),
FileWriteFailed(io::Error),
PermissionError(io::Error),
}
// Implement Display for RunError
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 {
/// Create a default failed result with an error message
fn error(message: &str) -> Self {
Self {
stdout: String::new(),
stderr: message.to_string(),
success: false,
code: -1,
}
}
}
/// 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 = 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/sh".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)?;
// Write the script content
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();
// Buffers for captured output
let mut captured_stdout = String::new();
let mut captured_stderr = String::new();
// 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 {
eprintln!("{}", l);
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
if let Some(handle) = stdout_handle {
captured_stdout = handle.join().unwrap_or_default();
} else {
captured_stdout = "Failed to capture stdout".to_string();
}
// Join our stderr thread if it exists
if let Some(handle) = stderr_handle {
captured_stderr = handle.join().unwrap_or_default();
} else {
captured_stderr = "Failed to capture stderr".to_string();
}
// 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();
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.
*
* # Arguments
*
* * `command` - The command + args as a single string (e.g., "ls -la")
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the command execution
* * `Err(RunError)` - An error if the command execution failed
*/
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.
*
* # Arguments
*
* * `interpreter` - The interpreter to use (e.g., "/bin/sh")
* * `script_path` - The path to the script file
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*/
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![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();
process_command_output(output)
} 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)?;
handle_child_output(child, false)
}
}
/**
* Run a single command with arguments, showing live stdout and stderr.
*
* # Arguments
*
* * `command` - The command + args as a single string (e.g., "ls -la")
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the command execution
* * `Err(RunError)` - An error if the command execution failed
*
* # Examples
*
* ```
* let result = run_command("ls -la")?;
* println!("Command exited with code: {}", result.code);
* ```
*/
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
run_command_internal(command, /* silent = */ false)
}
/**
* Run a single command with arguments silently.
*
* # Arguments
*
* * `command` - The command + args as a single string (e.g., "ls -la")
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the command execution
* * `Err(RunError)` - An error if the command execution failed
*
* # Examples
*
* ```
* let result = run_command_silent("ls -la")?;
* println!("Command output: {}", result.stdout);
* ```
*/
pub fn run_command_silent(command: &str) -> Result<CommandResult, RunError> {
run_command_internal(command, /* silent = */ true)
}
/**
* Run a multiline script with optional silent mode.
*
* # Arguments
*
* * `script` - The script content as a string
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*/
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
// _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_script_internal(&interpreter, &script_path, silent)
}
/**
* Run a multiline script by saving it to a temporary file and executing.
*
* # Arguments
*
* * `script` - The script content as a string
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*
* # Examples
*
* ```
* let script = r#"
* echo "Hello, world!"
* ls -la
* "#;
* let result = run_script(script)?;
* println!("Script exited with code: {}", result.code);
* ```
*/
pub fn run_script(script: &str) -> Result<CommandResult, RunError> {
run_script_internal(script, false)
}
/**
* Run a multiline script silently by saving it to a temporary file and executing.
*
* # Arguments
*
* * `script` - The script content as a string
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the script execution
* * `Err(RunError)` - An error if the script execution failed
*
* # Examples
*
* ```
* let script = r#"
* echo "Hello, world!"
* ls -la
* "#;
* let result = run_script_silent(script)?;
* println!("Script output: {}", result.stdout);
* ```
*/
pub fn run_script_silent(script: &str) -> Result<CommandResult, RunError> {
run_script_internal(script, true)
}
/**
* Run a command or multiline script with arguments.
* Shows stdout/stderr as it arrives.
*
* # Arguments
*
* * `command` - The command or script to run
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the execution
* * `Err(RunError)` - An error if the execution failed
*
* # Examples
*
* ```
* // Run a single command
* let result = run("ls -la")?;
*
* // Run a multiline script
* let result = run(r#"
* echo "Hello, world!"
* ls -la
* "#)?;
* ```
*/
pub fn run(command: &str) -> Result<CommandResult, RunError> {
let trimmed = command.trim();
// Check if this is a multiline script
if trimmed.contains('\n') {
// This is a multiline script, write to a temporary file and execute
run_script(trimmed)
} else {
// This is a single command with arguments
run_command(trimmed)
}
}
/**
* Run a command or multiline script with arguments silently.
* Doesn't show stdout/stderr as it arrives.
*
* # Arguments
*
* * `command` - The command or script to run
*
* # Returns
*
* * `Ok(CommandResult)` - The result of the execution
* * `Err(RunError)` - An error if the execution failed
*
* # Examples
*
* ```
* // Run a single command silently
* let result = run_silent("ls -la")?;
*
* // Run a multiline script silently
* let result = run_silent(r#"
* echo "Hello, world!"
* ls -la
* "#)?;
* ```
*/
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
let trimmed = command.trim();
// Check if this is a multiline script
if trimmed.contains('\n') {
// This is a multiline script, write to a temporary file and execute
run_script_silent(trimmed)
} else {
// This is a single command with arguments
run_command_silent(trimmed)
}
}