use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use crate::config::FormatterConfig;
const EXTERNAL_TIMEOUT: Duration = Duration::from_secs(5);
const POLL_INTERVAL: Duration = Duration::from_millis(20);
pub fn run_external(formatter: &FormatterConfig, text: &str, cwd: &Path) -> Result<String> {
let mut child = Command::new(&formatter.command)
.args(&formatter.args)
.current_dir(cwd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("spawning formatter `{}`", formatter.command))?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(text.as_bytes())
.with_context(|| format!("writing stdin to `{}`", formatter.command))?;
}
let deadline = Instant::now() + EXTERNAL_TIMEOUT;
loop {
match child
.try_wait()
.with_context(|| format!("polling formatter `{}`", formatter.command))?
{
Some(_) => break,
None => {
if Instant::now() >= deadline {
let _ = child.kill();
bail!(
"formatter `{}` timed out after {:?}",
formatter.command,
EXTERNAL_TIMEOUT
);
}
std::thread::sleep(POLL_INTERVAL);
}
}
}
let output = child
.wait_with_output()
.with_context(|| format!("collecting output from `{}`", formatter.command))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let msg = stderr.trim();
if msg.is_empty() {
bail!(
"formatter `{}` exited with {}",
formatter.command,
output.status
);
}
bail!("formatter `{}`: {}", formatter.command, msg);
}
let out = String::from_utf8(output.stdout)
.with_context(|| format!("formatter `{}` produced non-UTF-8 output", formatter.command))?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_external_pipes_stdin_through_cat() {
let f = FormatterConfig {
command: "cat".into(),
args: vec![],
};
let out = run_external(&f, "hello\nworld\n", Path::new(".")).unwrap();
assert_eq!(out, "hello\nworld\n");
}
#[test]
fn run_external_surfaces_nonzero_exit_with_stderr() {
let f = FormatterConfig {
command: "sh".into(),
args: vec!["-c".into(), "cat >&2; exit 1".into()],
};
let err = run_external(&f, "boom", Path::new("."))
.unwrap_err()
.to_string();
assert!(err.contains("boom"), "stderr should bubble up: {}", err);
}
#[test]
fn run_external_reports_spawn_failure() {
let f = FormatterConfig {
command: "this-binary-does-not-exist-zzz".into(),
args: vec![],
};
assert!(run_external(&f, "x", Path::new(".")).is_err());
}
}