feat: Add process package to monorepo
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Add `sal-process` package for cross-platform process management. - Update workspace members in `Cargo.toml`. - Mark process package as complete in MONOREPO_CONVERSION_PLAN.md - Remove license information from `mycelium` and `os` READMEs.
This commit is contained in:
@@ -42,7 +42,7 @@ pub use sal_mycelium as mycelium;
|
||||
pub use sal_net as net;
|
||||
pub use sal_os as os;
|
||||
pub mod postgresclient;
|
||||
pub mod process;
|
||||
pub use sal_process as process;
|
||||
pub use sal_redisclient as redisclient;
|
||||
pub mod rhai;
|
||||
pub use sal_text as text;
|
||||
|
@@ -1,150 +0,0 @@
|
||||
# SAL Process Module (`sal::process`)
|
||||
|
||||
The `process` module in the SAL (System Abstraction Layer) library provides a robust and cross-platform interface for creating, managing, and interacting with system processes. It is divided into two main sub-modules: `run` for command and script execution, and `mgmt` for process management tasks like listing, finding, and terminating processes.
|
||||
|
||||
## Core Functionalities
|
||||
|
||||
### 1. Command and Script Execution (`run.rs`)
|
||||
|
||||
The `run.rs` sub-module offers flexible ways to execute external commands and multi-line scripts.
|
||||
|
||||
#### `RunBuilder`
|
||||
|
||||
The primary interface for execution is the `RunBuilder`, obtained via `sal::process::run("your_command_or_script")`. It allows for fluent configuration:
|
||||
|
||||
- `.die(bool)`: If `true` (default), an error is returned if the command fails. If `false`, a `CommandResult` with `success: false` is returned instead.
|
||||
- `.silent(bool)`: If `true` (default is `false`), suppresses `stdout` and `stderr` from being printed to the console during execution. Output is still captured in `CommandResult`.
|
||||
- `.async_exec(bool)`: If `true` (default is `false`), executes the command or script in a separate thread, returning an immediate placeholder `CommandResult`.
|
||||
- `.log(bool)`: If `true` (default is `false`), prints a log message before executing the command.
|
||||
- `.execute() -> Result<CommandResult, RunError>`: Executes the configured command or script.
|
||||
|
||||
**Input Handling**:
|
||||
- **Single-line commands**: Treated as a command and its arguments (e.g., `"ls -la"`).
|
||||
- **Multi-line scripts**: If the input string contains newline characters (`\n`), it's treated as a script.
|
||||
- The script content is automatically dedented.
|
||||
- On Unix-like systems, `#!/bin/bash -e` is prepended (if no shebang exists) to ensure the script exits on error.
|
||||
- A temporary script file is created, made executable, and then run.
|
||||
|
||||
#### `CommandResult`
|
||||
|
||||
All execution functions return a `Result<CommandResult, RunError>`. The `CommandResult` struct contains:
|
||||
- `stdout: String`: Captured standard output.
|
||||
- `stderr: String`: Captured standard error.
|
||||
- `success: bool`: `true` if the command exited with a zero status code.
|
||||
- `code: i32`: The exit code of the command.
|
||||
|
||||
#### Convenience Functions:
|
||||
- `sal::process::run_command("cmd_or_script")`: Equivalent to `run("cmd_or_script").execute()`.
|
||||
- `sal::process::run_silent("cmd_or_script")`: Equivalent to `run("cmd_or_script").silent(true).execute()`.
|
||||
|
||||
#### Error Handling:
|
||||
- `RunError`: Enum for errors specific to command/script execution (e.g., `EmptyCommand`, `CommandExecutionFailed`, `ScriptPreparationFailed`).
|
||||
|
||||
### 2. Process Management (`mgmt.rs`)
|
||||
|
||||
The `mgmt.rs` sub-module provides tools for querying and managing system processes.
|
||||
|
||||
#### `ProcessInfo`
|
||||
A struct holding basic process information:
|
||||
- `pid: i64`
|
||||
- `name: String`
|
||||
- `memory: f64` (currently a placeholder)
|
||||
- `cpu: f64` (currently a placeholder)
|
||||
|
||||
#### Functions:
|
||||
- `sal::process::which(command_name: &str) -> Option<String>`:
|
||||
Checks if a command exists in the system's `PATH`. Returns the full path if found.
|
||||
```rust
|
||||
if let Some(path) = sal::process::which("git") {
|
||||
println!("Git found at: {}", path);
|
||||
}
|
||||
```
|
||||
|
||||
- `sal::process::kill(pattern: &str) -> Result<String, ProcessError>`:
|
||||
Kills processes matching the given `pattern` (name or part of the command line).
|
||||
Uses `taskkill` on Windows and `pkill -f` on Unix-like systems.
|
||||
```rust
|
||||
match sal::process::kill("my-server-proc") {
|
||||
Ok(msg) => println!("{}", msg), // "Successfully killed processes" or "No matching processes found"
|
||||
Err(e) => eprintln!("Error killing process: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
- `sal::process::process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError>`:
|
||||
Lists running processes, optionally filtering by a `pattern` (substring match on name). If `pattern` is empty, lists all accessible processes.
|
||||
Uses `wmic` on Windows and `ps` on Unix-like systems.
|
||||
```rust
|
||||
match sal::process::process_list("nginx") {
|
||||
Ok(procs) => {
|
||||
for p in procs {
|
||||
println!("PID: {}, Name: {}", p.pid, p.name);
|
||||
}
|
||||
},
|
||||
Err(e) => eprintln!("Error listing processes: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
- `sal::process::process_get(pattern: &str) -> Result<ProcessInfo, ProcessError>`:
|
||||
Retrieves a single `ProcessInfo` for a process matching `pattern`.
|
||||
Returns an error if zero or multiple processes match.
|
||||
```rust
|
||||
match sal::process::process_get("unique_process_name") {
|
||||
Ok(p) => println!("Found: PID {}, Name {}", p.pid, p.name),
|
||||
Err(sal::process::ProcessError::NoProcessFound(patt)) => eprintln!("No process like '{}'", patt),
|
||||
Err(sal::process::ProcessError::MultipleProcessesFound(patt, count)) => {
|
||||
eprintln!("Found {} processes like '{}'", count, patt);
|
||||
}
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Handling:
|
||||
- `ProcessError`: Enum for errors specific to process management (e.g., `CommandExecutionFailed`, `NoProcessFound`, `MultipleProcessesFound`).
|
||||
|
||||
## Examples
|
||||
|
||||
### Running a simple command
|
||||
```rust
|
||||
use sal::process;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let result = process::run("echo 'Hello from SAL!'").execute()?;
|
||||
println!("Output: {}", result.stdout);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Running a multi-line script silently
|
||||
```rust
|
||||
use sal::process;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let script = r#"
|
||||
echo "Starting script..."
|
||||
date
|
||||
echo "Script finished."
|
||||
"#;
|
||||
let result = process::run(script).silent(true).execute()?;
|
||||
if result.success {
|
||||
println!("Script executed successfully. Output:\n{}", result.stdout);
|
||||
} else {
|
||||
eprintln!("Script failed. Error:\n{}", result.stderr);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Checking if a command exists and then running it
|
||||
```rust
|
||||
use sal::process;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if process::which("figlet").is_some() {
|
||||
process::run("figlet 'SAL Process'").execute()?;
|
||||
} else {
|
||||
println!("Figlet not found, using echo instead:");
|
||||
process::run("echo 'SAL Process'").execute()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
@@ -1,351 +0,0 @@
|
||||
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(),
|
||||
)),
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
//! # Process Module
|
||||
//!
|
||||
//! The `process` module 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
|
||||
//!
|
||||
//! This module is designed to work consistently across Windows, macOS, and Linux.
|
||||
|
||||
mod run;
|
||||
mod mgmt;
|
||||
mod screen;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use run::*;
|
||||
pub use mgmt::*;
|
||||
pub use screen::{new as new_screen, kill as kill_screen};
|
@@ -1,532 +0,0 @@
|
||||
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::fmt;
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::thread;
|
||||
|
||||
use crate::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 {
|
||||
/// 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/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()
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
use crate::process::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(())
|
||||
}
|
@@ -1,169 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use crate::process::run::{run, RunError};
|
||||
use crate::text::dedent;
|
||||
|
||||
#[test]
|
||||
fn test_run_command() {
|
||||
// Test running a simple echo command using the builder pattern
|
||||
let result = run("echo hello").execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("hello"));
|
||||
assert_eq!(result.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_silent_command() {
|
||||
// Test running a command silently using the builder pattern
|
||||
let result = run("echo silent test").silent(true).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("silent test"));
|
||||
assert_eq!(result.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_script() {
|
||||
// Test running a multi-line script using the builder pattern
|
||||
let script = r#"
|
||||
echo "line 1"
|
||||
echo "line 2"
|
||||
"#;
|
||||
|
||||
let result = run(script).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.contains("line 1"));
|
||||
assert!(result.stdout.contains("line 2"));
|
||||
assert_eq!(result.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_with_dedent() {
|
||||
// Test that run properly dedents scripts
|
||||
let script = r#"
|
||||
echo "This has 12 spaces of indentation"
|
||||
echo "This has 16 spaces (4 more than the common indentation)"
|
||||
"#;
|
||||
|
||||
// The dedent function should remove the common 12 spaces
|
||||
let dedented = dedent(script);
|
||||
assert!(dedented.contains("echo \"This has 12 spaces of indentation\""));
|
||||
assert!(dedented.contains(" echo \"This has 16 spaces (4 more than the common indentation)\""));
|
||||
|
||||
// Running the script should work with the dedented content
|
||||
let result = run(script).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.contains("This has 12 spaces of indentation"));
|
||||
assert!(result.stdout.contains("This has 16 spaces (4 more than the common indentation)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_detects_script_vs_command() {
|
||||
// Test that run correctly identifies scripts vs commands
|
||||
|
||||
// One-liner should be treated as a command
|
||||
let one_liner = "echo one-liner test";
|
||||
let result = run(one_liner).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.stdout.contains("one-liner test"));
|
||||
|
||||
// Multi-line input should be treated as a script
|
||||
let multi_line = "echo first line\necho second line";
|
||||
let result = run(multi_line).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.stdout.contains("first line"));
|
||||
assert!(result.stdout.contains("second line"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_empty_command() {
|
||||
// Test handling of empty commands
|
||||
let result = run("").execute();
|
||||
assert!(result.is_err());
|
||||
// The specific error should be EmptyCommand
|
||||
match result {
|
||||
Err(RunError::EmptyCommand) => (),
|
||||
_ => panic!("Expected EmptyCommand error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_die_option() {
|
||||
// Test the die option - when false, it should return a CommandResult with success=false
|
||||
// instead of an Err when the command fails
|
||||
|
||||
// With die=true (default), a non-existent command should return an error
|
||||
let result = run("non_existent_command").execute();
|
||||
assert!(result.is_err());
|
||||
|
||||
// With die=false, it should return a CommandResult with success=false
|
||||
let result = run("non_existent_command").die(false).execute().unwrap();
|
||||
assert!(!result.success);
|
||||
assert_ne!(result.code, 0);
|
||||
assert!(result.stderr.contains("Error:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_async_option() {
|
||||
// Test the async option - when true, it should spawn the process and return immediately
|
||||
|
||||
// Create a shared variable to track if the command has completed
|
||||
let completed = Arc::new(Mutex::new(false));
|
||||
let completed_clone = completed.clone();
|
||||
|
||||
// Run a command that sleeps for 2 seconds, with async=true
|
||||
let start = std::time::Instant::now();
|
||||
let result = run("sleep 2").async_exec(true).execute().unwrap();
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// The command should return immediately (much less than 2 seconds)
|
||||
assert!(elapsed < Duration::from_secs(1));
|
||||
|
||||
// The result should have empty stdout/stderr and success=true
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert_eq!(result.stdout, "");
|
||||
assert_eq!(result.stderr, "");
|
||||
|
||||
// Wait a bit to ensure the command has time to complete
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
// Verify the command completed (this is just a placeholder since we can't easily
|
||||
// check if the async command completed in this test framework)
|
||||
*completed_clone.lock().unwrap() = true;
|
||||
assert!(*completed.lock().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_log_option() {
|
||||
// Test the log option - when true, it should log command execution
|
||||
// Note: We can't easily capture stdout in tests, so this is more of a smoke test
|
||||
|
||||
// Run a command with log=true
|
||||
let result = run("echo log test").log(true).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("log test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_method_chaining() {
|
||||
// Test that all builder methods can be chained together
|
||||
let result = run("echo chaining test")
|
||||
.silent(true)
|
||||
.die(true)
|
||||
.log(true)
|
||||
.execute()
|
||||
.unwrap();
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("chaining test"));
|
||||
}
|
||||
}
|
@@ -10,10 +10,8 @@ mod nerdctl;
|
||||
// OS module is now provided by sal-os package
|
||||
// Platform module is now provided by sal-os package
|
||||
mod postgresclient;
|
||||
mod process;
|
||||
|
||||
mod rfs;
|
||||
mod screen;
|
||||
mod vault;
|
||||
// zinit module is now in sal-zinit-client package
|
||||
|
||||
@@ -50,7 +48,7 @@ pub use sal_redisclient::rhai::register_redisclient_module;
|
||||
// Re-export PostgreSQL client module registration function
|
||||
pub use postgresclient::register_postgresclient_module;
|
||||
|
||||
pub use process::{
|
||||
pub use sal_process::rhai::{
|
||||
kill,
|
||||
process_get,
|
||||
process_list,
|
||||
@@ -138,7 +136,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
sal_os::rhai::register_os_module(engine)?;
|
||||
|
||||
// Register Process module functions
|
||||
process::register_process_module(engine)?;
|
||||
sal_process::rhai::register_process_module(engine)?;
|
||||
|
||||
// Register Buildah module functions
|
||||
buildah::register_bah_module(engine)?;
|
||||
@@ -175,8 +173,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
|
||||
// Platform functions are now registered by sal-os package
|
||||
|
||||
// Register Screen module functions
|
||||
screen::register(engine);
|
||||
// Screen module functions are now part of sal-process package
|
||||
|
||||
// Register utility functions
|
||||
engine.register_fn("is_def_fn", |_name: &str| -> bool {
|
||||
|
@@ -1,212 +0,0 @@
|
||||
//! Rhai wrappers for Process module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Process module.
|
||||
|
||||
use crate::process::{self, 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())
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
use crate::process::{kill_screen, new_screen};
|
||||
use rhai::{Engine, EvalAltResult};
|
||||
|
||||
fn screen_error_to_rhai_error<T>(result: anyhow::Result<T>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Screen error: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn register(engine: &mut Engine) {
|
||||
engine.register_fn("screen_new", |name: &str, cmd: &str| {
|
||||
screen_error_to_rhai_error(new_screen(name, cmd))
|
||||
});
|
||||
|
||||
engine.register_fn("screen_kill", |name: &str| {
|
||||
screen_error_to_rhai_error(kill_screen(name))
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user