//! Running a verb's binary: the hand-off, and the version probe `--list` //! makes. use std::ffi::OsStr; use std::path::Path; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use didbot_cli::Refusal; /// How long `--list` waits for one binary to answer `--version`. A verb /// that starts doing work instead of answering is killed, so one stray /// binary on `PATH` cannot hang the listing. const VERSION_PATIENCE: Duration = Duration::from_secs(3); /// The command as a verb receives it: `binary argv…`, this process's /// environment minus every credential variable, and nothing added. fn command(binary: &Path, argv: I) -> Command where I: IntoIterator, S: AsRef, { let mut command = Command::new(binary); command.args(argv); for variable in didbot_cli::env::CREDENTIALS { command.env_remove(variable); } command } /// Replaces this process with `binary argv…`. /// /// Returns only when the binary could not be started. On unix the verb's /// exit status is then the shell's directly; elsewhere this waits and /// exits with the same status. pub fn exec(binary: &Path, argv: I) -> Result<(), Refusal> where I: IntoIterator, S: AsRef, { let mut command = command(binary, argv); let could_not_start = |error: std::io::Error| { Refusal::failed(format!("could not run {}: {error}", binary.display())) }; #[cfg(unix)] { use std::os::unix::process::CommandExt; Err(could_not_start(command.exec())) } #[cfg(not(unix))] { let status = command.status().map_err(could_not_start)?; std::process::exit(status.code().unwrap_or(1)); } } /// What `binary --version` prints on its first line, if it answers with /// exit 0 inside [`VERSION_PATIENCE`]. pub fn version_of(binary: &Path) -> Option { let mut child = command(binary, ["--version"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .ok()?; let started = Instant::now(); let status = loop { match child.try_wait() { Ok(Some(status)) => break status, Ok(None) if started.elapsed() < VERSION_PATIENCE => { std::thread::sleep(Duration::from_millis(20)); } _ => { let _ = child.kill(); let _ = child.wait(); return None; } } }; if !status.success() { return None; } let mut stdout = child.stdout.take()?; let mut output = String::new(); std::io::Read::read_to_string(&mut stdout, &mut output).ok()?; output .lines() .next() .map(str::trim) .filter(|line| !line.is_empty()) .map(str::to_owned) }