meta lock waiters keep their queue place; deadlines cover pin_agent git and kill the whole process group
This commit is contained in:
parent
7cde4e7b15
commit
161dd8c44b
1 changed files with 138 additions and 51 deletions
|
|
@ -3,10 +3,11 @@
|
|||
//! fetch proposal commits into the applied repo.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Output;
|
||||
use std::process::{Output, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Deadline for a child that talks to a remote (forge fetch/push, `nix flake
|
||||
|
|
@ -18,19 +19,54 @@ pub const FETCH_DEADLINE: Duration = Duration::from_mins(10);
|
|||
pub const GIT_LOCAL_DEADLINE: Duration = Duration::from_mins(2);
|
||||
|
||||
/// Run `cmd` to completion and capture its output, failing once `deadline`
|
||||
/// passes. `what` names the command in the error. The child is killed when the
|
||||
/// deadline drops its future, so a hung remote cannot leave an orphan behind.
|
||||
/// passes. `what` names the command in the error. The child leads its own
|
||||
/// process group and the whole group is `SIGKILL`ed on timeout, so a
|
||||
/// grandchild (a `git` or credential helper under `nix`) dies with it instead
|
||||
/// of outliving the lock.
|
||||
pub async fn run_with_deadline(
|
||||
cmd: &mut Command,
|
||||
deadline: Duration,
|
||||
what: &str,
|
||||
) -> Result<Output> {
|
||||
// `kill_on_drop` is what turns the timeout's drop into a kill.
|
||||
cmd.kill_on_drop(true);
|
||||
tokio::time::timeout(deadline, cmd.output())
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("{what} timed out after {}s", deadline.as_secs_f64()))?
|
||||
.with_context(|| what.to_owned())
|
||||
// `kill_on_drop` covers the caller dropping this future; `process_group(0)`
|
||||
// makes the child's pid the group id the timeout path kills.
|
||||
let mut child = cmd
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.with_context(|| what.to_owned())?;
|
||||
// Read before waiting: a reaped child has no id.
|
||||
let pgid = child.id().and_then(|pid| i32::try_from(pid).ok());
|
||||
let (stdout, stderr) = (child.stdout.take(), child.stderr.take());
|
||||
let io = async { tokio::join!(child.wait(), drain(stdout), drain(stderr)) };
|
||||
if let Ok((status, stdout, stderr)) = tokio::time::timeout(deadline, io).await {
|
||||
return Ok(Output {
|
||||
status: status.with_context(|| what.to_owned())?,
|
||||
stdout: stdout.with_context(|| what.to_owned())?,
|
||||
stderr: stderr.with_context(|| what.to_owned())?,
|
||||
});
|
||||
}
|
||||
if let Some(pgid) = pgid {
|
||||
// SAFETY: plain `kill(2)`; `-pgid` targets the child's own process
|
||||
// group, which `process_group(0)` made it lead.
|
||||
unsafe {
|
||||
libc::kill(-pgid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
child.wait().await.with_context(|| what.to_owned())?;
|
||||
bail!("{what} timed out after {}s", deadline.as_secs_f64())
|
||||
}
|
||||
|
||||
/// Everything `pipe` yields until EOF.
|
||||
async fn drain<R: AsyncRead + Unpin>(pipe: Option<R>) -> std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
if let Some(mut p) = pipe {
|
||||
p.read_to_end(&mut buf).await?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
const GIT_NAME: &str = "c0re";
|
||||
|
|
@ -135,12 +171,12 @@ pub async fn git_authed(dir: &Path, args: &[&str], auth_header: &str) -> Result<
|
|||
|
||||
/// Resolve `refname` (a tag, branch, or sha) in `dir` to its full sha.
|
||||
pub async fn git_rev_parse(dir: &Path, refname: &str) -> Result<String> {
|
||||
let out = git_command()
|
||||
.current_dir(dir)
|
||||
.args(["rev-parse", refname])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("git rev-parse {refname} in {}", dir.display()))?;
|
||||
let out = run_with_deadline(
|
||||
git_command().current_dir(dir).args(["rev-parse", refname]),
|
||||
GIT_LOCAL_DEADLINE,
|
||||
&format!("git rev-parse {refname} in {}", dir.display()),
|
||||
)
|
||||
.await?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"git rev-parse {refname} failed ({}): {}",
|
||||
|
|
@ -155,12 +191,14 @@ pub async fn git_rev_parse(dir: &Path, refname: &str) -> Result<String> {
|
|||
/// such file. Reads the object database, not the working tree.
|
||||
pub async fn git_show_file(dir: &Path, rev: &str, path: &str) -> Result<Option<String>> {
|
||||
let spec = format!("{rev}:{path}");
|
||||
let listed = git_command()
|
||||
.current_dir(dir)
|
||||
.args(["ls-tree", "--name-only", rev, "--", path])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("git ls-tree {spec} in {}", dir.display()))?;
|
||||
let listed = run_with_deadline(
|
||||
git_command()
|
||||
.current_dir(dir)
|
||||
.args(["ls-tree", "--name-only", rev, "--", path]),
|
||||
GIT_LOCAL_DEADLINE,
|
||||
&format!("git ls-tree {spec} in {}", dir.display()),
|
||||
)
|
||||
.await?;
|
||||
if !listed.status.success() {
|
||||
bail!(
|
||||
"git ls-tree {spec} failed ({}): {}",
|
||||
|
|
@ -171,12 +209,12 @@ pub async fn git_show_file(dir: &Path, rev: &str, path: &str) -> Result<Option<S
|
|||
if listed.stdout.iter().all(u8::is_ascii_whitespace) {
|
||||
return Ok(None);
|
||||
}
|
||||
let shown = git_command()
|
||||
.current_dir(dir)
|
||||
.args(["show", &spec])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("git show {spec} in {}", dir.display()))?;
|
||||
let shown = run_with_deadline(
|
||||
git_command().current_dir(dir).args(["show", &spec]),
|
||||
GIT_LOCAL_DEADLINE,
|
||||
&format!("git show {spec} in {}", dir.display()),
|
||||
)
|
||||
.await?;
|
||||
if !shown.status.success() {
|
||||
bail!(
|
||||
"git show {spec} failed ({}): {}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue