use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use crate::support_resource_limits::{ServiceLaunch, restrict_to_one_cpu};
pub struct Fixture {
pub root: PathBuf,
pub wrapper: PathBuf,
pub service: std::sync::Mutex<Option<FixtureService>>,
pub service_environment: std::sync::Mutex<Vec<(OsString, OsString)>>,
pub service_launch: std::sync::Mutex<ServiceLaunch>,
}
pub struct OwnedGroups {
groups: std::cell::RefCell<Vec<libc::pid_t>>,
}
impl Drop for OwnedGroups {
fn drop(&mut self) {
for group in self.groups.borrow().iter() {
unsafe { libc::killpg(*group, libc::SIGKILL) };
}
}
}
thread_local! {
static OWNED_GROUPS: OwnedGroups = const { OwnedGroups { groups: std::cell::RefCell::new(Vec::new()) } };
}
pub fn own_group(child: &Child) {
let group: libc::pid_t = libc::pid_t::try_from(child.id()).expect("a pid fits the pid type");
OWNED_GROUPS.with(|owned| { owned.groups.borrow_mut().push(group); });
}
pub fn spawn_owned(command: &mut Command) -> std::io::Result<Child> {
use std::os::unix::process::CommandExt as _;
command.process_group(0);
let child: Child = command.spawn()?;
own_group(&child);
return Ok(child);
}
pub struct FixtureService {
children: std::sync::Mutex<std::collections::HashMap<PathBuf, Child>>,
pub runtime: PathBuf,
}
impl Drop for FixtureService {
fn drop(&mut self) {
if let Ok(mut children) = self.children.lock() {
for child in children.values_mut() {
kill_with_everything_it_started(child);
}
}
let _removed = std::fs::remove_dir_all(&self.runtime);
}
}
pub fn kill_with_everything_it_started(child: &mut Child) {
let group: i32 = -i32::try_from(child.id()).expect("a process ID fits");
unsafe { libc::kill(child.id().try_into().expect("a process ID fits"), libc::SIGTERM) };
let asked: std::time::Instant = std::time::Instant::now();
while child.try_wait().is_ok_and(|status| { return status.is_none(); }) && asked.elapsed() < std::time::Duration::from_secs(2) {
std::thread::sleep(std::time::Duration::from_millis(5));
}
unsafe { libc::kill(group, libc::SIGKILL) };
let _killed = child.kill();
let _waited = child.wait();
}
pub const SERVICE_PROGRAM: &str = env!("CARGO_BIN_EXE_monochromatic-git-policy-cli");
pub fn service_command() -> Command {
let mut command: Command = Command::new(SERVICE_PROGRAM);
command.args(git_policy_cli::service_main::SERVICE_WORDS);
return command;
}
pub const fn service_executable() -> &'static str {
return SERVICE_PROGRAM;
}
pub static RUNTIME_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn start_fixture_service(fixture: &Fixture) -> FixtureService {
let _ = fixture;
let home: PathBuf = PathBuf::from(std::env::var_os("HOME").expect("a home directory for the service's runtime directory"));
let number: usize = RUNTIME_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let runtime: PathBuf = home.join("temp/agent").join(format!("rt-{}-{number}", std::process::id()));
std::fs::create_dir_all(runtime.join("monochromatic-git-policy-cli")).expect("runtime directory");
return FixtureService { children: std::sync::Mutex::new(std::collections::HashMap::new()), runtime };
}
pub fn spawn_for_the_process_lifetime(command: Command) -> std::io::Result<Child> {
type Job = (Command, std::sync::mpsc::Sender<std::io::Result<Child>>);
static SPAWNER: std::sync::OnceLock<std::sync::Mutex<std::sync::mpsc::Sender<Job>>> = std::sync::OnceLock::new();
let spawner = SPAWNER.get_or_init(|| {
let (sender, receiver) = std::sync::mpsc::channel::<Job>();
std::thread::spawn(move || {
for (mut job, reply) in receiver {
let _sent = reply.send(job.spawn());
}
});
return std::sync::Mutex::new(sender);
});
let (reply, answer) = std::sync::mpsc::channel();
spawner.lock().expect("the spawner's sender").send((command, reply)).expect("the spawner thread runs");
return answer.recv().expect("the spawner thread answers");
}
pub const SOCKET_RELEASE_PATIENCE: std::time::Duration = std::time::Duration::from_secs(60);
pub fn wait_until_nothing_listens(socket: &Path) {
let began: std::time::Instant = std::time::Instant::now();
loop {
match std::os::unix::net::UnixStream::connect(socket) {
Err(error) if matches!(error.kind(), std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound) => return,
Ok(_) | Err(_) => {}
}
assert!(began.elapsed() < SOCKET_RELEASE_PATIENCE, "something still answers at {} although the fixture ended its service", socket.display());
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
impl FixtureService {
pub fn serve(&self, fixture: &Fixture, git_dir: &Path) {
use std::os::unix::process::CommandExt;
let key: PathBuf = std::fs::canonicalize(git_dir).unwrap_or_else(|_| { return git_dir.to_path_buf(); });
let mut children = self.children.lock().expect("the fixture's services");
if children.contains_key(&key) {
return;
}
let socket: PathBuf = git_policy_cli::service_instance::socket_for(self.runtime.as_path(), key.as_path());
wait_until_nothing_listens(&socket);
let launch: ServiceLaunch = fixture.service_launch.lock().expect("the service launch").clone();
let words: [&str; 3] = if launch.earlier_command_word {
git_policy_cli::service_main::EARLIER_SERVICE_WORDS
} else {
git_policy_cli::service_main::SERVICE_WORDS
};
let mut command: Command = match launch.prefix.split_first() {
Some((program, rest)) => {
let mut prefixed: Command = Command::new(program);
prefixed.args(rest).arg(SERVICE_PROGRAM).args(words);
prefixed
}
None => {
let mut plain: Command = Command::new(SERVICE_PROGRAM);
plain.args(words);
plain
}
};
command
.args(["--journal"])
.arg(fixture.root.join("journal"))
.arg("--path")
.arg(&socket)
.arg("--instance")
.arg(git_policy_cli::service_instance::instance_of(key.as_path()))
.env_clear()
.env("PATH", format!("{}:/usr/bin:/bin", fixture.root.join("bin").display()))
.env("HOME", fixture.root.join("home"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::inherit());
for (name, value) in fixture.service_environment.lock().expect("the service environment").iter() {
command.env(name, value);
}
command.process_group(0);
unsafe {
command.pre_exec(|| {
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
return Ok(());
});
}
if launch.single_cpu {
unsafe {
command.pre_exec(restrict_to_one_cpu);
}
}
for (kind, soft, hard) in launch.limits {
unsafe {
command.pre_exec(move || { return kind.apply(soft, hard); });
}
}
let child: Child = spawn_for_the_process_lifetime(command).expect("the fixture's commit service starts");
children.insert(key.clone(), child);
let began: std::time::Instant = std::time::Instant::now();
while std::os::unix::net::UnixStream::connect(&socket).is_err() {
let ended: Option<std::process::ExitStatus> = children.get_mut(&key).and_then(|started| { return started.try_wait().ok().flatten(); });
if launch.may_fail_to_start {
if ended.is_some() || began.elapsed() > std::time::Duration::from_secs(20) {
return;
}
} else if let Some(status) = ended {
children.remove(&key);
panic!("the fixture's commit service ended ({status}) before it listened at {}; what it wrote to its log is in this test's output", socket.display());
}
assert!(began.elapsed() < std::time::Duration::from_secs(60), "the fixture's commit service never listened");
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
pub fn process_ids(&self) -> Vec<u32> {
let children = self.children.lock().expect("the fixture's services");
return children.values().map(Child::id).collect();
}
pub fn stop_all(&self) {
let mut children = self.children.lock().expect("the fixture's services");
for (key, child) in children.iter_mut() {
kill_with_everything_it_started(child);
wait_until_nothing_listens(git_policy_cli::service_instance::socket_for(self.runtime.as_path(), key.as_path()).as_path());
}
children.clear();
}
pub fn forget_ended(&self) -> bool {
let mut children = self.children.lock().expect("the fixture's services");
let ended: Vec<PathBuf> =
children.iter_mut().filter_map(|(key, child)| { return child.try_wait().ok().flatten().map(|_| { return key.clone(); }); }).collect();
for key in &ended {
children.remove(key);
}
return !ended.is_empty();
}
pub fn serve_worktrees(&self, fixture: &Fixture) {
for git_dir in worktree_git_dirs(fixture.root.as_path(), 4) {
self.serve(fixture, git_dir.as_path());
}
}
}
pub fn worktree_git_dirs(directory: &Path, depth: usize) -> Vec<PathBuf> {
let mut found: Vec<PathBuf> = Vec::new();
let Ok(entries) = std::fs::read_dir(directory) else { return found };
for entry in entries.flatten() {
let path: PathBuf = entry.path();
let name = entry.file_name();
if name == ".git" {
if path.is_dir() {
found.push(path);
} else if let Ok(text) = std::fs::read_to_string(&path)
&& let Some(target) = text.lines().find_map(|line| { return line.strip_prefix("gitdir: "); })
{
found.push(directory.join(target));
}
} else if depth > 0 && name != "node_modules" && path.is_dir() && !path.is_symlink() {
found.extend(worktree_git_dirs(path.as_path(), depth - 1));
}
}
return found;
}
pub fn restart_ended_services(fixture: &Fixture) {
let service = fixture.service.lock().expect("the fixture's service lock");
let Some(services) = service.as_ref() else { return };
if services.forget_ended() {
*fixture.service_launch.lock().expect("the service launch") = ServiceLaunch::default();
services.serve_worktrees(fixture);
}
}
pub fn service_runtime(fixture: &Fixture) -> Option<PathBuf> {
let mut service = fixture.service.lock().expect("the fixture's service lock");
if service.is_none() {
*service = Some(start_fixture_service(fixture));
}
let started = service.as_ref()?;
started.serve_worktrees(fixture);
return Some(started.runtime.clone());
}
pub const KEEP_FIXTURES_VARIABLE: &str = "CLI_GIT_KEEP_FIXTURES";
pub fn settle_fixture(root: &Path, keep: bool) {
if keep {
return;
}
for _ in 0..50 {
match std::fs::remove_dir_all(root) {
Ok(()) => return,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)),
}
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _stopped = self.service.lock().map(|mut service| { return service.take(); });
end_processes_working_in(self.root.as_path());
settle_fixture(self.root.as_path(), std::env::var_os(KEEP_FIXTURES_VARIABLE).is_some());
}
}
#[path = "../testing/test_alive.rs"]
mod test_alive;
pub use test_alive::{end_processes_working_in, while_test_lives};