use crate::device::common::{DeviceError, DeviceResult, validate_args, validate_command};
use crate::utils::{command_timeout::run_command_with_timeout, run_command_fast_fail};
use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct CommandOptions {
pub timeout: Option<Duration>,
pub check_status: bool,
}
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
pub fn execute_command(
command: &str,
args: &[&str],
options: &CommandOptions,
) -> DeviceResult<CommandOutput> {
if !validate_command(command) {
return Err(DeviceError::Other(format!(
"Invalid command rejected: {command}"
)));
}
if !validate_args(args) {
return Err(DeviceError::Other(format!(
"Invalid arguments rejected for command: {command}"
)));
}
let output = if let Some(timeout) = options.timeout {
run_command_with_timeout(command, args, timeout)?
} else {
run_command_fast_fail(command, args)?
};
let status_code = output.status.code().unwrap_or(-1);
let out = CommandOutput {
status: status_code,
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
};
if options.check_status && status_code != 0 {
let full_command = if args.is_empty() {
command.to_string()
} else {
format!("{command} {}", args.join(" "))
};
eprintln!("Command execution failed: '{full_command}' (exit code: {status_code})");
if !out.stderr.is_empty() {
eprintln!("Stderr output: {}", out.stderr);
}
return Err(DeviceError::CommandFailed {
command: full_command,
code: Some(status_code),
stderr: out.stderr.clone(),
});
}
Ok(out)
}
pub fn execute_command_default(command: &str, args: &[&str]) -> DeviceResult<CommandOutput> {
execute_command(command, args, &CommandOptions::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
const ECHO: (&str, &[&str]) = ("echo", &["hello"]);
#[cfg(windows)]
const ECHO: (&str, &[&str]) = ("cmd", &["/C", "echo", "hello"]);
#[cfg(unix)]
const FAILS: (&str, &[&str]) = ("false", &[]);
#[cfg(windows)]
const FAILS: (&str, &[&str]) = ("cmd", &["/C", "exit", "1"]);
#[test]
fn test_execute_command_default_success() {
let (cmd, args) = ECHO;
let out = execute_command_default(cmd, args).expect("echo should succeed");
assert_eq!(out.status, 0);
assert!(out.stdout.contains("hello"));
}
#[test]
fn test_execute_command_with_status_check() {
let opts = CommandOptions {
timeout: Some(Duration::from_secs(2)),
check_status: true,
};
let (cmd, args) = FAILS;
let err = execute_command(cmd, args, &opts).unwrap_err();
match err {
DeviceError::CommandFailed { .. } => {}
_ => panic!("Expected CommandFailed error"),
}
}
}