use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use crate::errors::{Result, WtpMcpError};
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
pub struct WtpRunner {
binary_path: PathBuf,
repo_root: PathBuf,
}
impl WtpRunner {
pub fn new(binary_path: PathBuf, repo_root: PathBuf) -> Self {
Self {
binary_path,
repo_root,
}
}
pub fn repo_root(&self) -> &PathBuf {
&self.repo_root
}
pub async fn run(&self, args: &[&str]) -> Result<CommandOutput> {
tracing::debug!(
binary = %self.binary_path.display(),
cwd = %self.repo_root.display(),
?args,
"executing wtp command"
);
let output = self.run_with_retry(args)?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
tracing::debug!(exit_code, "wtp command completed");
Ok(CommandOutput {
stdout,
stderr,
exit_code,
})
}
pub async fn run_checked(&self, args: &[&str]) -> Result<String> {
let output = self.run(args).await?;
if output.exit_code != 0 {
return Err(WtpMcpError::CommandFailed {
exit_code: output.exit_code,
message: format!("wtp {:?} failed", args),
stderr: output.stderr,
});
}
Ok(output.stdout)
}
fn run_with_retry(&self, args: &[&str]) -> std::io::Result<std::process::Output> {
const MAX_RETRIES: u8 = 5;
const BASE_SLEEP_MS: u64 = 10;
let mut attempt = 0;
loop {
match Command::new(&self.binary_path)
.args(args)
.current_dir(&self.repo_root)
.output()
{
Ok(output) => return Ok(output),
Err(err) if is_executable_file_busy(&err) && attempt < MAX_RETRIES => {
attempt += 1;
std::thread::sleep(Duration::from_millis(BASE_SLEEP_MS * attempt as u64));
}
Err(err) => return Err(err),
}
}
}
}
fn is_executable_file_busy(err: &std::io::Error) -> bool {
#[cfg(unix)]
{
err.raw_os_error() == Some(26)
}
#[cfg(not(unix))]
{
let _ = err;
false
}
}