use crate::bounded_push::Patience;
use crate::push_progress::Clock;
use crate::output_capture::Capture;
use crate::quiet_ledger;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolCommand {
pub arguments: Vec<OsString>,
pub directory: PathBuf,
pub set: Vec<(OsString, OsString)>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ToolLimits {
pub window: Duration,
pub ceiling: Duration,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ToolEnd {
Succeeded,
Failed(String),
TimedOut,
CeilingReached(Duration),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolRun {
pub end: ToolEnd,
pub millis: u128,
pub stdout: Vec<u8>,
}
const OUTPUT_TAIL_BYTES: usize = 600;
const POLL_INTERVAL: Duration = Duration::from_millis(10);
pub fn find_program(name: &str, search_path: Option<&OsStr>) -> Option<PathBuf> {
use std::os::unix::fs::PermissionsExt;
let search: &OsStr = search_path?;
for directory in std::env::split_paths(search) {
if directory.as_os_str().is_empty() {
continue;
}
let candidate: PathBuf = directory.join(name);
let runnable: bool = std::fs::metadata(&candidate).is_ok_and(|found: std::fs::Metadata| { return found.is_file() && found.permissions().mode() & 0o111 != 0; });
if runnable {
return Some(candidate);
}
}
return None;
}
fn tail_of(bytes: &[u8]) -> String {
let cut: usize = bytes.len().saturating_sub(OUTPUT_TAIL_BYTES);
let text: String = String::from_utf8_lossy(&bytes[cut..]).into_owned();
let informative: String = text.lines().filter(|line: &&str| { return !line.trim_start().starts_with("Blocking waiting for file lock"); }).collect::<Vec<&str>>().join("\n");
let trimmed: &str = informative.trim();
if cut == 0 {
return trimmed.to_owned();
}
return trimmed.split_once('\n').map_or(trimmed, |(_ragged, rest): (&str, &str)| { return rest; }).trim().to_owned();
}
fn describe_status(status: std::process::ExitStatus) -> String {
use std::os::unix::process::ExitStatusExt;
return match (status.code(), status.signal()) {
(Some(code), _) => format!("exit status {code}"),
(None, Some(signal)) => format!("stopped by signal {signal}"),
(None, None) => String::from("ended without a status"),
};
}
pub fn run_tool(program: &Path, command: &ToolCommand, limits: ToolLimits) -> ToolRun {
use std::os::unix::process::CommandExt;
let started: Instant = Instant::now();
let mut starting: Command = Command::new(program);
let captured: Capture = match Capture::new().and_then(|files| { return files.stdio().map(|streams| { return (files, streams); }); }) {
Ok((files, (out, err))) => {
starting.stdout(out).stderr(err);
files
}
Err(error) => return ToolRun { end: ToolEnd::Failed(format!("{} could not be started: no place for its output ({error})", program.display())), millis: started.elapsed().as_millis(), stdout: Vec::new() },
};
starting.args(&command.arguments).current_dir(&command.directory).stdin(Stdio::null()).process_group(0);
for (name, value) in &command.set {
starting.env(name, value);
}
let mut child: Child = match starting.spawn() {
Ok(running) => running,
Err(error) => return ToolRun { end: ToolEnd::Failed(format!("{} could not start: {error}", program.display())), millis: started.elapsed().as_millis(), stdout: Vec::new() },
};
let mut clock: Clock = Clock::start(child.id(), Patience::Progress { stall: limits.window }).without_wall_fallback();
let mut at_ceiling: bool = false;
let status: Option<std::process::ExitStatus> = loop {
match child.try_wait() {
Ok(Some(done)) => break Some(done),
Ok(None) if started.elapsed() >= limits.ceiling => {
at_ceiling = true;
break None;
}
Ok(None) if clock.check().is_some() => break None,
Ok(None) => std::thread::sleep(POLL_INTERVAL),
Err(error) => return ToolRun { end: ToolEnd::Failed(format!("{} could not be waited for: {error}", program.display())), millis: started.elapsed().as_millis(), stdout: Vec::new() },
}
};
if clock.measures() {
quiet_ledger::note(tool_step(program, command).as_str(), clock.longest_quiet(), status.is_none() && !at_ceiling);
}
let Some(finished) = status else {
stop_group(&child);
let _reaped = child.wait();
let end: ToolEnd = if at_ceiling { ToolEnd::CeilingReached(limits.ceiling) } else { ToolEnd::TimedOut };
return ToolRun { end, millis: started.elapsed().as_millis(), stdout: Vec::new() };
};
let (stdout, errors): (Vec<u8>, Vec<u8>) = captured.read();
let millis: u128 = started.elapsed().as_millis();
if finished.success() {
return ToolRun { end: ToolEnd::Succeeded, millis, stdout };
}
let combined: Vec<u8> = [stdout.as_slice(), errors.as_slice()].concat();
let text: String = tail_of(combined.as_slice());
return ToolRun { end: ToolEnd::Failed(format!("{}: {text}", describe_status(finished))), millis, stdout: Vec::new() };
}
fn tool_step(program: &Path, command: &ToolCommand) -> String {
let name: String = program.file_name().map_or_else(String::new, |file| { return file.to_string_lossy().into_owned(); });
return match command.arguments.first() {
Some(first) => format!("{name} {}", first.to_string_lossy()),
None => name,
};
}
fn stop_group(child: &Child) {
let Ok(leader) = libc::pid_t::try_from(child.id()) else { return };
let _stopped = unsafe { libc::kill(-leader, libc::SIGKILL) };
}
#[cfg(test)]
#[path = "package_repair_command_tests.rs"]
mod tests;