use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use crate::support_program_names_and_time_bound::TIME_BOUND_SECONDS;
pub const TIMED_OUT_SIGNAL: i32 = 9;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Observed {
pub code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub fn without_hint_fields(bytes: &[u8]) -> Vec<u8> {
let marker: &[u8] = b",\"hint\":\"";
let mut kept: Vec<u8> = Vec::with_capacity(bytes.len());
let mut index: usize = 0;
while index < bytes.len() {
if bytes[index..].starts_with(marker) {
index += marker.len();
while index < bytes.len() && bytes[index] != b'"' {
index += if bytes[index] == b'\\' { 2 } else { 1 };
}
index += 1;
} else {
kept.push(bytes[index]);
index += 1;
}
}
return kept;
}
pub const WAIT_NOTICE_PREFIX: &[u8] = b"monochromatic-git-policy-cli: still waiting for ";
pub fn without_wait_notices(bytes: &[u8]) -> Vec<u8> {
return bytes
.split_inclusive(|byte| { return *byte == b'\n'; })
.filter(|line| { return !line.starts_with(WAIT_NOTICE_PREFIX); })
.flatten()
.copied()
.collect();
}
pub fn observe(command: &mut Command, input: &[u8]) -> Observed {
let raw: Observed = observe_raw(command, input);
return Observed {
code: raw.code,
stdout: without_hint_fields(raw.stdout.as_slice()),
stderr: without_hint_fields(raw.stderr.as_slice()),
};
}
pub enum SharedBoundMisuse {
CommitTransaction,
ManualPage,
}
pub fn shared_bound_misuse(command: &Command) -> Option<SharedBoundMisuse> {
let arguments: Vec<OsString> = command.get_args().map(OsStr::to_os_string).collect();
let [signal, bound, program, wrapped @ ..] = arguments.as_slice() else {
return None;
};
if signal != "--signal=KILL" || bound != TIME_BOUND_SECONDS {
return None;
}
let stripped = git_policy_cli::wrapper_invocation::strip_wrapper_controls(wrapped);
if stripped.layout.outcome != git_policy_cli::global_arguments::GlobalOutcome::Command {
return None;
}
let region: &[OsString] = git_policy_cli::wrapper_invocation::command_region(&stripped);
let word: &[u8] = git_policy_cli::wrapper_invocation::command_word(&stripped);
if word == b"commit"
&& git_policy_cli::command_commit::parse_commit_region(region, &[]).is_ok_and(|facts| { return !facts.dry_run; })
{
return Some(SharedBoundMisuse::CommitTransaction);
}
let has_manual_stub: bool = Path::new(program)
.parent()
.is_some_and(|directory| { return directory.join("man").exists(); });
let asks_for_help: bool = region
.iter()
.take_while(|token| { return *token != "--"; })
.any(|token| { return token == "--help"; });
if word != b"monochromatic-git-policy-cli" && word != b"cli-git" && asks_for_help && !has_manual_stub {
return Some(SharedBoundMisuse::ManualPage);
}
return None;
}
pub fn supervised_wait(mut child: Child, input: &[u8], cleanup: impl FnOnce()) -> Output {
let child_id: u32 = child.id();
let mut stdin = child.stdin.take().expect("piped stdin");
let owned: Vec<u8> = input.to_vec();
let writer = std::thread::spawn(move || { let _ = stdin.write_all(owned.as_slice()); });
let output: Output = child.wait_with_output().expect("process finishes");
let _joined = writer.join();
if let Some(message) = super::supervisor::take_kill(child_id) {
panic!("{message}");
}
cleanup();
return output;
}
pub fn observe_raw(command: &mut Command, input: &[u8]) -> Observed {
match shared_bound_misuse(command) {
Some(SharedBoundMisuse::CommitTransaction) => panic!(
"a commit under the shared bound: the work of a landed commit happens in the commit service, outside the command's \
process tree, so the supervisor would see a tree that waits and end it; run it with `run_commit` (the {}-second \
wall-clock bound of binary_commit_tests.rs)",
super::commit::COMMIT_BOUND_SECONDS
),
Some(SharedBoundMisuse::ManualPage) => panic!(
"a `--help` under the shared bound renders a manual page through man and groff (9 process starts) that is Git's \
work and the host's `man`; put an executable `man` stub in the fixture's bin directory first (`stub_manual_viewer`)"
),
None => {}
}
let child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("process starts");
let output: Output = supervised_wait(child, input, || {
if let Some(directory) = command.get_current_dir() {
settle_service_work(directory);
}
});
assert_ne!(
output.status.signal(),
Some(TIMED_OUT_SIGNAL),
"the command hit its wall-clock bound: {:?} in {:?}",
command.get_args().collect::<Vec<&OsStr>>(),
command.get_current_dir()
);
return Observed {
code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
};
}
pub const SERVICE_WORK_PATIENCE: std::time::Duration = std::time::Duration::from_secs(60);
pub fn work_listing(work: &Path) -> String {
return ["pending", "claimed", "recovering", "done", "failed"]
.iter()
.map(|state| {
let mut names: Vec<String> = std::fs::read_dir(work.join(state))
.map(|listing| { return listing.flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect(); })
.unwrap_or_default();
names.sort();
return format!("{state}={names:?}");
})
.collect::<Vec<String>>()
.join(" ");
}
pub fn settle_service_work(directory: &Path) {
let Some(git_dir) = directory.ancestors().map(|candidate| { return candidate.join(".git"); }).find(|candidate| { return candidate.is_dir(); }) else {
return;
};
let work: PathBuf = git_dir.join(git_policy_cli::work_item::WORK_DIRECTORY);
let open = |state: &str| -> usize { return std::fs::read_dir(work.join(state)).map_or(0, Iterator::count); };
let began: std::time::Instant = std::time::Instant::now();
while open("pending") + open("claimed") + open("recovering") > 0 {
assert!(began.elapsed() < SERVICE_WORK_PATIENCE, "the service never settled the after-commit work in {}: {}", work.display(), work_listing(work.as_path()));
std::thread::sleep(std::time::Duration::from_millis(20));
}
}