use std::ffi::OsString;
use std::io::{Read, Write as _};
use std::path::PathBuf;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::{Context as _, Result};
use openmoat_hosts::Host;
use crate::install::dir_variable;
pub const TIMEOUT: Duration = Duration::from_secs(10);
const MAX_REPLY_BYTES: u64 = 1024 * 1024;
const POLL_INTERVAL: Duration = Duration::from_millis(2);
pub enum Reply {
Exited {
code: Option<i32>,
stdout: String,
stderr: String,
},
TimedOut,
NotStarted(String),
}
impl Reply {
pub fn describe(&self) -> String {
match self {
Self::Exited { code, stdout, .. } => {
let code = code.map_or_else(|| "killed".to_owned(), |c| c.to_string());
format!("exit {code}: {}", stdout.trim())
}
Self::TimedOut => format!("no exit within {} s", TIMEOUT.as_secs()),
Self::NotStarted(error) => format!("could not start: {error}"),
}
}
}
pub struct Scratch {
root: PathBuf,
pub home: PathBuf,
pub project: PathBuf,
}
impl Scratch {
pub fn new() -> Result<Self> {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let root = std::env::temp_dir().join(format!("moat-bench-{}-{nanos}", std::process::id()));
std::fs::create_dir(&root).with_context(|| format!("creating {}", root.display()))?;
let home = root.join("home");
let project = home.join("proj");
let scratch = Self {
root,
home,
project,
};
let mut dirs = vec!["proj/.git"];
for host in Host::ALL {
dirs.push(dir_variable(host)?.1);
}
for dir in dirs {
let path = scratch.home.join(dir);
std::fs::create_dir_all(&path)
.with_context(|| format!("creating {}", path.display()))?;
}
Ok(scratch)
}
pub fn command(&self, program: impl Into<OsString>, args: &[OsString]) -> Command {
let mut cmd = Command::new(program.into());
cmd.args(args)
.current_dir(&self.project)
.env_clear()
.envs(self.environment());
cmd
}
pub fn environment(&self) -> Vec<(&'static str, OsString)> {
let mut vars = vec![
("PATH", std::env::var_os("PATH").unwrap_or_default()),
("HOME", self.home.clone().into_os_string()),
("USERPROFILE", self.home.clone().into_os_string()),
];
if let Some(root) = std::env::var_os("SYSTEMROOT") {
vars.push(("SYSTEMROOT", root));
}
vars
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
pub fn shell(command_line: &str) -> (OsString, Vec<OsString>) {
let (shell, flag) = if cfg!(windows) {
("cmd", "/C")
} else {
("sh", "-c")
};
(shell.into(), vec![flag.into(), command_line.into()])
}
pub fn send(cmd: &mut Command, payload: &str) -> Reply {
let deadline = Instant::now() + TIMEOUT;
let spawned = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
let mut child = match spawned {
Ok(child) => child,
Err(error) => return Reply::NotStarted(error.to_string()),
};
if let Some(mut stdin) = child.stdin.take() {
let payload = payload.to_owned();
std::thread::spawn(move || stdin.write_all(payload.as_bytes()));
}
let stdout = child.stdout.take().map(read_on_thread);
let stderr = child.stderr.take().map(read_on_thread);
let Some(status) = wait(&mut child, deadline) else {
return Reply::TimedOut;
};
let collect = |reader: Option<Receiver<String>>| {
reader
.and_then(|r| {
r.recv_timeout(deadline.saturating_duration_since(Instant::now()))
.ok()
})
.unwrap_or_default()
};
Reply::Exited {
code: status.code(),
stdout: collect(stdout),
stderr: collect(stderr),
}
}
fn wait(child: &mut Child, deadline: Instant) -> Option<ExitStatus> {
loop {
match child.try_wait() {
Ok(Some(status)) => return Some(status),
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(POLL_INTERVAL);
}
_ => {
let _ = child.kill();
let _ = child.wait();
return None;
}
}
}
}
fn read_on_thread(pipe: impl Read + Send + 'static) -> Receiver<String> {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut bytes = Vec::new();
let _ = pipe.take(MAX_REPLY_BYTES).read_to_end(&mut bytes);
let _ = tx.send(String::from_utf8_lossy(&bytes).into_owned());
});
rx
}