use std::collections::HashSet;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
pub(crate) fn clear_inherited_git_env(cmd: &mut std::process::Command) {
for var in [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_PREFIX",
"GIT_COMMON_DIR",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_NAMESPACE",
] {
cmd.env_remove(var);
}
}
pub(crate) fn git_command(path: &Path) -> std::process::Command {
let mut cmd = std::process::Command::new("git");
clear_inherited_git_env(&mut cmd);
cmd.arg("-C").arg(path);
cmd
}
pub(crate) fn output_text(
output: std::io::Result<std::process::Output>,
) -> color_eyre::Result<String> {
let output = output
.map_err(|error| color_eyre::eyre::eyre!("{}", super::describe_spawn_error(&error)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(color_eyre::eyre::eyre!(
"Git command failed ({}): {}",
output.status,
stderr.trim()
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(unix)]
const MUTATING_OP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
#[cfg(unix)]
const DRAIN_GRACE: std::time::Duration = std::time::Duration::from_secs(2);
static KILLABLE_GIT_PIDS: OnceLock<Mutex<HashSet<i32>>> = OnceLock::new();
static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
fn killable_pids() -> &'static Mutex<HashSet<i32>> {
KILLABLE_GIT_PIDS.get_or_init(|| Mutex::new(HashSet::new()))
}
pub(crate) fn spawn_killable(
cmd: &mut std::process::Command,
) -> std::io::Result<std::process::Child> {
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
let child = {
let mut pids = killable_pids().lock().unwrap();
if SHUTTING_DOWN.load(Ordering::SeqCst) {
return Err(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"gitpane is shutting down",
));
}
let child = cmd.spawn()?;
pids.insert(child.id() as i32);
child
};
Ok(child)
}
pub(crate) fn unregister_killable_pid(pid: i32) {
killable_pids().lock().unwrap().remove(&pid);
}
pub(crate) fn kill_in_flight_git_ops() {
SHUTTING_DOWN.store(true, Ordering::SeqCst);
let pids: Vec<i32> = {
let mut pids = killable_pids().lock().unwrap();
let snapshot: Vec<i32> = pids.iter().copied().collect();
pids.clear();
snapshot
};
#[cfg(unix)]
for pid in pids {
unsafe { libc::killpg(pid, libc::SIGKILL) };
}
#[cfg(not(unix))]
let _ = pids;
}
#[cfg(unix)]
pub(crate) fn run_git_op_capturing(
path: &Path,
args: &[String],
) -> std::io::Result<std::process::Output> {
use std::process::Stdio;
let mut cmd = git_command(path);
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = spawn_killable(&mut cmd)?;
let pid = child.id() as i32;
let result = capture_with_timeout(&mut child, MUTATING_OP_TIMEOUT);
unregister_killable_pid(pid);
result
}
#[cfg(not(unix))]
pub(crate) fn run_git_op_capturing(
path: &Path,
args: &[String],
) -> std::io::Result<std::process::Output> {
use std::process::Stdio;
let mut cmd = git_command(path);
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
cmd.output()
}
#[cfg(unix)]
pub(super) fn capture_with_timeout(
child: &mut std::process::Child,
timeout: std::time::Duration,
) -> std::io::Result<std::process::Output> {
let deadline = std::time::Instant::now() + timeout;
let mut stdout = child.stdout.take();
let mut stderr = child.stderr.take();
let mut out = Vec::new();
let mut err = Vec::new();
let mut chunk = [0u8; 8192];
let status = loop {
read_ready(stdout.as_mut(), &mut out, &mut chunk);
read_ready(stderr.as_mut(), &mut err, &mut chunk);
match child.try_wait() {
Ok(Some(st)) => break Some(st),
Ok(None) => {}
Err(e) => {
kill_process_group(child);
let _ = child.wait();
return Err(e);
}
}
if std::time::Instant::now() >= deadline {
break None;
}
std::thread::sleep(std::time::Duration::from_millis(20));
};
match status {
Some(st) => {
let (o2, e2) = drain_pipes(stdout.as_mut(), stderr.as_mut(), DRAIN_GRACE);
out.extend(o2);
err.extend(e2);
Ok(std::process::Output {
status: st,
stdout: out,
stderr: err,
})
}
None => {
kill_process_group(child);
let _ = child.wait();
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("timed out after {}s", timeout.as_secs()),
))
}
}
}
#[cfg(unix)]
fn read_ready<R>(r: Option<&mut R>, buf: &mut Vec<u8>, chunk: &mut [u8])
where
R: std::io::Read + std::os::fd::AsRawFd,
{
let Some(r) = r else { return };
for _ in 0..64 {
let mut pfd = libc::pollfd {
fd: r.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut pfd, 1, 0) };
if rc <= 0 {
break;
}
match r.read(chunk) {
Ok(0) => break,
Ok(n) => buf.extend_from_slice(&chunk[..n]),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
}
#[cfg(unix)]
fn drain_pipes(
mut stdout: Option<&mut std::process::ChildStdout>,
mut stderr: Option<&mut std::process::ChildStderr>,
grace: std::time::Duration,
) -> (Vec<u8>, Vec<u8>) {
use std::io::Read;
use std::os::fd::AsRawFd;
let deadline = std::time::Instant::now() + grace;
let mut out = Vec::new();
let mut err = Vec::new();
let mut chunk = [0u8; 8192];
let mut done = [false, false];
loop {
if std::time::Instant::now() >= deadline || (done[0] && done[1]) {
break;
}
let mut fds = [
libc::pollfd {
fd: -1,
events: 0,
revents: 0,
},
libc::pollfd {
fd: -1,
events: 0,
revents: 0,
},
];
if !done[0]
&& let Some(o) = stdout.as_deref()
{
fds[0].fd = o.as_raw_fd();
fds[0].events = libc::POLLIN;
}
if !done[1]
&& let Some(e) = stderr.as_deref()
{
fds[1].fd = e.as_raw_fd();
fds[1].events = libc::POLLIN;
}
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;
let rc = unsafe { libc::poll(fds.as_mut_ptr(), 2, timeout_ms) };
if rc < 0 {
if std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
continue;
}
break;
}
if rc == 0 {
break; }
if fds[0].revents != 0 && !done[0] {
match stdout.as_deref_mut().map(|o| o.read(&mut chunk)) {
Some(Ok(0)) => done[0] = true,
Some(Ok(n)) => out.extend_from_slice(&chunk[..n]),
Some(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => {}
Some(Err(_)) => done[0] = true,
None => done[0] = true,
}
}
if fds[1].revents != 0 && !done[1] {
match stderr.as_deref_mut().map(|e| e.read(&mut chunk)) {
Some(Ok(0)) => done[1] = true,
Some(Ok(n)) => err.extend_from_slice(&chunk[..n]),
Some(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => {}
Some(Err(_)) => done[1] = true,
None => done[1] = true,
}
}
}
(out, err)
}
#[cfg(unix)]
pub(crate) fn kill_process_group(child: &mut std::process::Child) {
let pgid = child.id() as i32;
if unsafe { libc::killpg(pgid, libc::SIGKILL) } != 0
&& std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
{
let _ = child.kill();
}
}
#[cfg(not(unix))]
pub(crate) fn kill_process_group(child: &mut std::process::Child) {
let _ = child.kill();
}
#[cfg(all(test, unix))]
pub(super) fn register_killable_pid(pid: i32) {
killable_pids().lock().unwrap().insert(pid);
}
#[cfg(all(test, unix))]
pub(super) struct ResetsShuttingDown;
#[cfg(all(test, unix))]
impl ResetsShuttingDown {
pub(super) fn new() -> Self {
Self
}
}
#[cfg(all(test, unix))]
impl Drop for ResetsShuttingDown {
fn drop(&mut self) {
SHUTTING_DOWN.store(false, Ordering::SeqCst);
}
}
#[cfg(all(test, unix))]
static TEST_KILL_LOCK: Mutex<()> = Mutex::new(());
#[cfg(test)]
mod tests;