use crate::{Result, VaultmuxError};
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
pub async fn run_command(program: &str, args: &[&str], env: &[(&str, &str)]) -> Result<String> {
let mut cmd = Command::new(program);
cmd.args(args);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
for (key, value) in env {
cmd.env(key, value);
}
let output = cmd.output().await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
VaultmuxError::BackendNotInstalled(format!("{} command not found", program))
} else {
VaultmuxError::Io(e)
}
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(VaultmuxError::CommandFailed(format!(
"{} failed with exit code {}: {}",
program,
output.status.code().unwrap_or(-1),
stderr
)));
}
String::from_utf8(output.stdout).map_err(|e| {
VaultmuxError::Other(anyhow::anyhow!("Invalid UTF-8 in command output: {}", e))
})
}
pub async fn run_command_with_stdin(
program: &str,
args: &[&str],
env: &[(&str, &str)],
stdin_data: &str,
) -> Result<String> {
let mut cmd = Command::new(program);
cmd.args(args);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
for (key, value) in env {
cmd.env(key, value);
}
let mut child = cmd.spawn().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
VaultmuxError::BackendNotInstalled(format!("{} command not found", program))
} else {
VaultmuxError::Io(e)
}
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(stdin_data.as_bytes())
.await
.map_err(VaultmuxError::Io)?;
stdin.flush().await.map_err(VaultmuxError::Io)?;
}
let output = child.wait_with_output().await.map_err(VaultmuxError::Io)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(VaultmuxError::CommandFailed(format!(
"{} failed with exit code {}: {}",
program,
output.status.code().unwrap_or(-1),
stderr
)));
}
String::from_utf8(output.stdout).map_err(|e| {
VaultmuxError::Other(anyhow::anyhow!("Invalid UTF-8 in command output: {}", e))
})
}
pub async fn check_command_exists(program: &str) -> Result<bool> {
let output = Command::new("which")
.arg(program)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.map_err(VaultmuxError::Io)?;
Ok(output.success())
}
#[derive(Debug)]
pub struct StatusCache {
authenticated: bool,
timestamp: Option<Instant>,
ttl: Duration,
}
impl StatusCache {
pub fn new(ttl: Duration) -> Self {
Self {
authenticated: false,
timestamp: None,
ttl,
}
}
pub fn get(&self) -> Option<bool> {
if let Some(ts) = self.timestamp {
if ts.elapsed() < self.ttl {
return Some(self.authenticated);
}
}
None
}
pub fn set(&mut self, authenticated: bool) {
self.authenticated = authenticated;
self.timestamp = Some(Instant::now());
}
pub fn invalidate(&mut self) {
self.timestamp = None;
}
}
impl Default for StatusCache {
fn default() -> Self {
Self::new(Duration::from_secs(5))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_run_command_success() {
let output = run_command("echo", &["hello"], &[]).await.unwrap();
assert_eq!(output.trim(), "hello");
}
#[tokio::test]
async fn test_run_command_not_found() {
let result = run_command("nonexistent-command-12345", &[], &[]).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_run_command_with_env() {
let output = run_command("printenv", &["TEST_VAR"], &[("TEST_VAR", "test-value")])
.await
.unwrap();
assert_eq!(output.trim(), "test-value");
}
#[tokio::test]
async fn test_run_command_with_stdin() {
let output = run_command_with_stdin("cat", &[], &[], "hello from stdin")
.await
.unwrap();
assert_eq!(output.trim(), "hello from stdin");
}
#[tokio::test]
async fn test_check_command_exists() {
assert!(check_command_exists("echo").await.unwrap());
assert!(!check_command_exists("nonexistent-command-12345")
.await
.unwrap());
}
#[test]
fn test_status_cache() {
let mut cache = StatusCache::new(Duration::from_secs(2));
assert_eq!(cache.get(), None);
cache.set(true);
assert_eq!(cache.get(), Some(true));
std::thread::sleep(Duration::from_millis(100));
assert_eq!(cache.get(), Some(true));
let mut cache2 = StatusCache::new(Duration::from_millis(200));
cache2.set(false);
assert_eq!(cache2.get(), Some(false));
std::thread::sleep(Duration::from_millis(250));
assert_eq!(cache2.get(), None);
}
#[test]
fn test_status_cache_invalidate() {
let mut cache = StatusCache::new(Duration::from_secs(10));
cache.set(true);
assert_eq!(cache.get(), Some(true));
cache.invalidate();
assert_eq!(cache.get(), None);
}
}