use std::path::Path;
use std::process::Command;
pub const EXEC_VISIBILITY_WAIT: std::time::Duration = std::time::Duration::from_secs(10);
pub const EXEC_VISIBILITY_POLL: std::time::Duration = std::time::Duration::from_millis(2);
pub fn wait_for_exec_visibility(
pid: u32,
expected_argv0_basename: &str,
wait: std::time::Duration,
poll: std::time::Duration,
) -> bool {
let self_cmdline = std::fs::read(format!("/proc/{}/cmdline", std::process::id())).ok();
let deadline = std::time::Instant::now() + wait;
loop {
if !crate::agent::agent_running(pid) {
return false;
}
if let Ok(raw) = std::fs::read(format!("/proc/{pid}/cmdline")) {
let args: Vec<String> = raw
.split(|&byte| byte == 0)
.filter(|arg| !arg.is_empty())
.map(|arg| String::from_utf8_lossy(arg).into_owned())
.collect();
let basename_matches = args
.first()
.and_then(|argv0| crate::agent::argv_basename(argv0))
.is_some_and(|name| name == expected_argv0_basename);
let differs_from_caller = self_cmdline.as_deref() != Some(raw.as_slice());
if basename_matches && differs_from_caller {
return true;
}
}
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::sleep(poll);
}
}
pub const REPO_LOCAL_GIT_VARS: &[&str] = &[
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_CONFIG",
"GIT_CONFIG_PARAMETERS",
"GIT_CONFIG_COUNT",
"GIT_OBJECT_DIRECTORY",
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_IMPLICIT_WORK_TREE",
"GIT_GRAFT_FILE",
"GIT_INDEX_FILE",
"GIT_NO_REPLACE_OBJECTS",
"GIT_REPLACE_REF_BASE",
"GIT_PREFIX",
"GIT_SHALLOW_FILE",
"GIT_COMMON_DIR",
];
pub const ALSO_REDIRECTING_GIT_VARS: &[&str] =
&["GIT_NAMESPACE", "GIT_DISCOVERY_ACROSS_FILESYSTEM"];
pub fn git_command(repo: &Path) -> Command {
hermetic_command("git", repo)
}
pub fn hermetic_command(program: &str, dir: &Path) -> Command {
let mut cmd = Command::new(program);
cmd.current_dir(dir);
for var in REPO_LOCAL_GIT_VARS.iter().chain(ALSO_REDIRECTING_GIT_VARS) {
cmd.env_remove(var);
}
cmd
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn git_command_marks_every_redirecting_var_for_removal() {
let cmd = git_command(Path::new("/tmp"));
let removed: Vec<&str> = cmd
.get_envs()
.filter(|(_, value)| value.is_none())
.filter_map(|(key, _)| key.to_str())
.collect();
for var in REPO_LOCAL_GIT_VARS.iter().chain(ALSO_REDIRECTING_GIT_VARS) {
assert!(
removed.contains(var),
"{var} is not cleared by git_command — a fixture inheriting it \
would operate on that repository instead of its tempdir"
);
}
}
#[test]
fn git_command_preserves_git_exec_path() {
let cmd = git_command(Path::new("/tmp"));
assert!(
!cmd.get_envs()
.any(|(key, value)| key == "GIT_EXEC_PATH" && value.is_none()),
"GIT_EXEC_PATH must not be cleared"
);
}
#[test]
fn local_env_vars_match_git() {
let output = Command::new("git")
.args(["rev-parse", "--local-env-vars"])
.output()
.expect("run `git rev-parse --local-env-vars`");
assert!(
output.status.success(),
"`git rev-parse --local-env-vars` failed"
);
let mut from_git: Vec<String> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(str::to_string)
.collect();
let mut ours: Vec<String> = REPO_LOCAL_GIT_VARS
.iter()
.map(|v| (*v).to_string())
.collect();
from_git.sort();
ours.sort();
assert_eq!(
ours, from_git,
"REPO_LOCAL_GIT_VARS has drifted from `git rev-parse --local-env-vars`"
);
}
#[test]
fn wait_for_exec_visibility_detects_a_real_child_and_leaves_it_exec_visible() {
let mut child = std::process::Command::new("sleep")
.arg("5")
.spawn()
.expect("spawn sleep fixture");
let pid = child.id();
let visible = wait_for_exec_visibility(
pid,
"sleep",
std::time::Duration::from_secs(5),
std::time::Duration::from_millis(2),
);
assert!(visible, "a real sleep child must become exec-visible");
let raw = std::fs::read(format!("/proc/{pid}/cmdline"))
.expect("must be able to read the child's cmdline after the barrier returns");
let args: Vec<String> = raw
.split(|&b| b == 0)
.filter(|a| !a.is_empty())
.map(|a| String::from_utf8_lossy(a).into_owned())
.collect();
assert_eq!(
args.first().map(String::as_str),
Some("sleep"),
"after the barrier returns, cmdline must be the child's own argv, not the caller's"
);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn wait_for_exec_visibility_times_out_bounded_when_it_never_matches() {
let start = std::time::Instant::now();
let visible = wait_for_exec_visibility(
std::process::id(),
"this-basename-can-never-match-anything",
std::time::Duration::from_millis(200),
std::time::Duration::from_millis(2),
);
let elapsed = start.elapsed();
assert!(
!visible,
"a basename that can never match must return false"
);
assert!(
elapsed < std::time::Duration::from_secs(1),
"must return within roughly `wait`, not hang indefinitely — took {elapsed:?}"
);
}
#[test]
fn wait_for_exec_visibility_returns_false_promptly_for_a_dead_pid() {
let start = std::time::Instant::now();
let visible = wait_for_exec_visibility(
0x7FFF_FFFE,
"anything",
std::time::Duration::from_secs(5),
std::time::Duration::from_millis(20),
);
let elapsed = start.elapsed();
assert!(!visible, "a dead pid must never be reported exec-visible");
assert!(
elapsed < std::time::Duration::from_secs(1),
"a dead pid must not wait out the full ceiling, took {elapsed:?}"
);
}
#[test]
fn wait_for_exec_visibility_rejects_a_self_match_on_unchanged_cmdline() {
let self_pid = std::process::id();
let raw = std::fs::read(format!("/proc/{self_pid}/cmdline"))
.expect("must be able to read this process's own cmdline");
let self_basename = raw
.split(|&b| b == 0)
.find(|a| !a.is_empty())
.map(|a| String::from_utf8_lossy(a).into_owned())
.and_then(|arg0| {
std::path::Path::new(&arg0)
.file_name()
.and_then(|n| n.to_str())
.map(str::to_string)
})
.expect("must be able to derive this process's own argv[0] basename");
let visible = wait_for_exec_visibility(
self_pid,
&self_basename,
std::time::Duration::from_millis(200),
std::time::Duration::from_millis(2),
);
assert!(
!visible,
"a self-match on unchanged cmdline must never report exec-visible, even though \
argv[0]'s basename matches by construction"
);
}
}