use crate::DriverError;
use crate::result::DriverResult;
use std::{collections::HashMap, process::Stdio};
use std::io::Write;
use std::process::{Command, Output};
use std::time::Duration;
use tokio::time::timeout;
use tracing::{debug, info, warn};
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub fn hidden_cmd(program: &str) -> Command {
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
let mut cmd = Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
cmd
}
#[cfg(not(target_os = "windows"))]
{
Command::new(program)
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecOptions {
pub cwd: Option<String>,
pub env: HashMap<String, String>,
pub timeout_secs: Option<u64>,
pub capture_stdout: bool,
pub capture_stderr: bool,
}
impl ExecOptions {
pub fn new() -> Self {
debug!("Creating new ExecOptions instance");
return Self::default();
}
pub fn with_cwd(mut self, cwd: impl Into<String>) -> Self {
self.cwd = Some(cwd.into());
return self;
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
return self;
}
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = Some(secs);
return self;
}
pub fn with_stdout(mut self, capture: bool) -> Self {
self.capture_stdout = capture;
return self;
}
pub fn with_stderr(mut self, capture: bool) -> Self {
self.capture_stderr = capture;
return self;
}
}
#[derive(Debug, Clone)]
pub struct ExecResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub success: bool,
}
impl ExecResult {
fn from_output(output: Output) -> Self {
return Self {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
success: output.status.success(),
};
}
pub fn ok() -> Self {
return Self { exit_code: 0, stdout: String::new(), stderr: String::new(), success: true };
}
pub fn error(msg: impl Into<String>) -> Self {
let msg = msg.into();
return Self { exit_code: -1, stdout: String::new(), stderr: msg.clone(), success: false };
}
}
pub fn exec(program: &str, args: &[&str], opts: Option<ExecOptions>) -> DriverResult<ExecResult> {
debug!("Executing command: {} with {:?} args", program, args);
let opts = opts.unwrap_or_default();
let mut cmd = hidden_cmd(program);
cmd.args(args);
if let Some(dir) = opts.cwd {
cmd.current_dir(dir.clone());
debug!("Set working directory: {}", dir);
}
for (k, v) in opts.env {
cmd.env(k, v);
}
if opts.capture_stdout {
cmd.stdout(Stdio::piped());
} else {
cmd.stdout(Stdio::null());
}
if opts.capture_stderr {
cmd.stderr(Stdio::piped());
} else {
cmd.stderr(Stdio::null());
}
let output = if let Some(timeout_secs) = opts.timeout_secs {
debug!("Command will timeout after {} seconds", timeout_secs);
let handle = std::thread::spawn(move || cmd.output());
match std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(timeout_secs));
return handle;
})
.join()
{
Ok(join_handle) => match join_handle.join() {
Ok(Ok(output)) => output,
Ok(Err(e)) => {
let err_msg = format!("IO error: {}", e);
warn!("{}", err_msg);
return Err(DriverError::io(err_msg));
}
Err(_) => {
let err_msg = "Command thread panicked".to_string();
warn!("{}", err_msg);
return Err(DriverError::internal(err_msg));
}
},
Err(_) => {
let err_msg = format!("Timeout after {}s", timeout_secs);
warn!("{}", err_msg);
return Err(DriverError::timeout(Some(err_msg)));
}
}
} else {
match cmd.output() {
Ok(o) => o,
Err(e) => {
let err_msg = format!("Failed to execute command: {}", e);
warn!("{}", err_msg);
return Err(DriverError::io(err_msg));
}
}
};
let result = ExecResult::from_output(output);
info!("Command executed: exit_code={}, success={}", result.exit_code, result.success);
return Ok(result);
}
pub async fn exec_async(program: &str, args: &[&str], opts: Option<ExecOptions>) -> DriverResult<ExecResult> {
debug!("Executing async command: {} with {:?} args", program, args);
let opts = opts.unwrap_or_default();
let timeout_secs = opts.timeout_secs.unwrap_or(30);
let program_owned = program.to_string();
let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let cwd = opts.cwd.clone();
let env = opts.env.clone();
let capture_stdout = opts.capture_stdout;
let capture_stderr = opts.capture_stderr;
info!("Async command timeout: {}s", timeout_secs);
let result = timeout(Duration::from_secs(timeout_secs), async move {
tokio::task::spawn_blocking(move || {
let mut cmd = hidden_cmd(&program_owned);
cmd.args(&args_owned);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
for (k, v) in env {
cmd.env(k, v);
}
if capture_stdout {
cmd.stdout(Stdio::piped());
} else {
cmd.stdout(Stdio::null());
}
if capture_stderr {
cmd.stderr(Stdio::piped());
} else {
cmd.stderr(Stdio::null());
}
match cmd.output() {
Ok(output) => ExecResult::from_output(output),
Err(e) => ExecResult::error(format!("IO error: {}", e)),
}
})
.await
.unwrap_or_else(|_| ExecResult::error("Task spawn failed".to_string()))
})
.await;
match result {
Ok(r) => {
info!("Async command completed: exit_code={}", r.exit_code);
return Ok(r);
}
Err(_) => {
let err_msg = format!("Timeout after {}s", timeout_secs);
warn!("{}", err_msg);
return Err(DriverError::timeout(Some(err_msg)));
}
}
}
pub fn exec_stdout(program: &str, args: &[&str]) -> DriverResult<String> {
debug!("Executing stdout command: {}", program);
let result = exec(program, args, None)?;
if result.success {
info!("Stdout command succeeded: {}", program);
return Ok(result.stdout);
} else {
let err_msg = format!("Command failed: {}", result.stderr);
warn!("{}", err_msg);
return Err(DriverError::execution(err_msg));
}
}
pub async fn exec_stdout_async(program: &str, args: &[&str]) -> DriverResult<String> {
debug!("Executing async stdout command: {}", program);
let result = exec_async(program, args, None).await?;
if result.success {
info!("Async stdout command succeeded: {}", program);
return Ok(result.stdout);
} else {
let err_msg = format!("Command failed: {}", result.stderr);
warn!("{}", err_msg);
return Err(DriverError::execution(err_msg));
}
}
pub fn exec_check(program: &str, args: &[&str]) -> bool {
debug!("Checking command: {}", program);
let success = exec(program, args, None).map(|r| r.success).unwrap_or(false);
info!("Command check result: {}", success);
return success;
}
pub fn exec_with_stdin(program: &str, args: &[&str], stdin_content: &str, opts: Option<ExecOptions>) -> DriverResult<ExecResult> {
debug!("Executing with stdin: {}", program);
let opts = opts.unwrap_or_default();
let mut cmd = hidden_cmd(program);
cmd.args(args);
cmd.stdin(Stdio::piped());
if let Some(dir) = opts.cwd {
cmd.current_dir(dir);
}
for (k, v) in opts.env {
cmd.env(k, v);
}
if opts.capture_stdout {
cmd.stdout(Stdio::piped());
} else {
cmd.stdout(Stdio::null());
}
if opts.capture_stderr {
cmd.stderr(Stdio::piped());
} else {
cmd.stderr(Stdio::null());
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
let err_msg = format!("Failed to spawn command: {}", e);
warn!("{}", err_msg);
return Err(DriverError::io(err_msg));
}
};
if let Some(mut stdin) = child.stdin.take() {
if let Err(e) = stdin.write_all(stdin_content.as_bytes()) {
let err_msg = format!("Failed to write stdin: {}", e);
warn!("{}", err_msg);
return Err(DriverError::io(err_msg));
}
}
let output = match child.wait_with_output() {
Ok(o) => o,
Err(e) => {
let err_msg = format!("Failed to wait for output: {}", e);
warn!("{}", err_msg);
return Err(DriverError::io(err_msg));
}
};
let result = ExecResult::from_output(output);
info!("Command with stdin completed: exit_code={}", result.exit_code);
return Ok(result);
}
pub async fn exec_with_stdin_async(program: &str, args: &[&str], stdin_content: &str, opts: Option<ExecOptions>) -> DriverResult<ExecResult> {
debug!("Executing async with stdin: {}", program);
let opts = opts.unwrap_or_default();
let timeout_secs = opts.timeout_secs.unwrap_or(30);
let program_owned = program.to_string();
let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let cwd = opts.cwd.clone();
let env = opts.env.clone();
let capture_stdout = opts.capture_stdout;
let capture_stderr = opts.capture_stderr;
let stdin_owned = stdin_content.to_string();
info!("Async stdin command timeout: {}s", timeout_secs);
let result = timeout(Duration::from_secs(timeout_secs), async move {
tokio::task::spawn_blocking(move || {
let mut cmd = hidden_cmd(&program_owned);
cmd.args(&args_owned);
cmd.stdin(Stdio::piped());
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
for (k, v) in env {
cmd.env(k, v);
}
if capture_stdout {
cmd.stdout(Stdio::piped());
} else {
cmd.stdout(Stdio::null());
}
if capture_stderr {
cmd.stderr(Stdio::piped());
} else {
cmd.stderr(Stdio::null());
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => return ExecResult::error(format!("Spawn failed: {}", e)),
};
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(stdin_owned.as_bytes());
}
match child.wait_with_output() {
Ok(output) => ExecResult::from_output(output),
Err(e) => ExecResult::error(format!("Wait failed: {}", e)),
}
})
.await
.unwrap_or_else(|_| ExecResult::error("Task spawn failed".to_string()))
})
.await;
match result {
Ok(r) => {
info!("Async stdin command completed: exit_code={}", r.exit_code);
return Ok(r);
}
Err(_) => {
let err_msg = format!("Timeout after {}s", timeout_secs);
warn!("{}", err_msg);
return Err(DriverError::timeout(Some(err_msg)));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_echo() {
let result = exec("echo", &["hello"], None).unwrap();
assert!(result.success);
assert!(result.stdout.contains("hello"));
}
#[test]
fn test_false() {
let result = exec("false", &[], None).unwrap();
assert!(!result.success);
}
#[tokio::test]
async fn test_async_echo() {
let result = exec_async("echo", &["hello"], None).await.unwrap();
assert!(result.success);
assert!(result.stdout.contains("hello"));
}
#[test]
fn test_with_stdin() {
let result = exec_with_stdin("grep", &["hello"], "hello world\nfoo bar", None).unwrap();
assert!(result.success);
assert!(result.stdout.contains("hello world"));
}
}