use anyhow::{Context, Result, bail};
use std::{
process::{Command, ExitStatus},
sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, Instant},
};
static UPDATE_CANCELED: AtomicBool = AtomicBool::new(false);
static SIGNAL_HANDLER: OnceLock<Result<(), String>> = OnceLock::new();
pub(super) fn install_cancel_handler() -> Result<()> {
SIGNAL_HANDLER
.get_or_init(|| {
ctrlc::set_handler(|| UPDATE_CANCELED.store(true, Ordering::SeqCst))
.map_err(|error| error.to_string())
})
.as_ref()
.map_err(|error| anyhow::anyhow!("cannot install update cancellation handler: {error}"))?;
UPDATE_CANCELED.store(false, Ordering::SeqCst);
Ok(())
}
pub(super) fn check_canceled() -> Result<()> {
if UPDATE_CANCELED.load(Ordering::SeqCst) {
bail!("Cargo update canceled");
}
Ok(())
}
pub(super) fn run_cargo(command: &mut Command, timeout: Duration) -> Result<ExitStatus> {
check_canceled()?;
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
let mut child = command
.spawn()
.context("cannot start Cargo; install a working Rust toolchain from https://rustup.rs")?;
let deadline = Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => return Ok(status),
Ok(None) if Instant::now() < deadline && !UPDATE_CANCELED.load(Ordering::SeqCst) => {
thread::sleep(Duration::from_millis(100))
}
result => {
let reason = if result.is_err() {
"cannot wait for Cargo"
} else if UPDATE_CANCELED.load(Ordering::SeqCst) {
"Cargo update canceled"
} else {
"Cargo update timed out"
};
#[cfg(windows)]
let cleanup = cleanup_windows_child(&mut child, terminate_windows_build_tree);
#[cfg(not(windows))]
let cleanup = crate::tools::process::terminate_child_tree_and_wait(&mut child)
.and_then(|outcome| match outcome.cleanup_warning {
Some(warning) => Err(anyhow::anyhow!(warning)),
None => Ok(()),
});
cleanup.with_context(|| reason)?;
result.context("cannot wait for Cargo")?;
bail!("{reason}; review the install records and retry manually");
}
}
}
}
#[cfg(any(windows, test))]
fn cleanup_windows_child(
child: &mut std::process::Child,
terminate_tree: impl FnOnce(u32) -> Result<()>,
) -> Result<()> {
let tree = terminate_tree(child.id());
let direct = kill_direct_child_and_wait(child);
match (tree, direct) {
(Ok(()), Ok(())) => Ok(()),
(Err(tree), Ok(())) => Err(tree).context("Cargo stopped, but build-tree cleanup failed"),
(Ok(()), Err(direct)) => Err(direct),
(Err(tree), Err(direct)) => {
Err(direct).context(format!("build-tree cleanup failed: {tree:#}"))
}
}
}
#[cfg(any(windows, test))]
fn kill_direct_child_and_wait(child: &mut std::process::Child) -> Result<()> {
let kill = child.kill();
let deadline = Instant::now() + Duration::from_secs(1);
loop {
let wait = child.try_wait();
if matches!(wait, Ok(Some(_))) {
return Ok(());
}
if Instant::now() >= deadline {
bail!("direct child exit could not be confirmed; kill: {kill:?}; wait: {wait:?}");
}
thread::sleep(Duration::from_millis(20));
}
}
#[cfg(windows)]
fn terminate_windows_build_tree(pid: u32) -> Result<()> {
use std::process::Stdio;
let mut taskkill = Command::new(super::windows::system_directory()?.join("taskkill.exe"))
.args(["/PID", &pid.to_string(), "/T", "/F"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("cannot stop Cargo build tree")?;
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let status = match taskkill.try_wait() {
Ok(status) => status,
Err(error) => {
let cleanup = kill_direct_child_and_wait(&mut taskkill);
return Err(error)
.context(format!("cannot wait for taskkill; cleanup: {cleanup:?}"));
}
};
if let Some(status) = status {
if !status.success() {
bail!("cannot stop Cargo build tree; stop Cargo and rustc manually");
}
return Ok(());
}
if Instant::now() >= deadline {
let cleanup = kill_direct_child_and_wait(&mut taskkill);
bail!(
"timed out stopping Cargo build tree; stop rustc manually; taskkill cleanup: {cleanup:?}"
);
}
thread::sleep(Duration::from_millis(20));
}
}
#[cfg(all(test, any(unix, windows)))]
mod tests {
use super::*;
#[test]
fn tree_cleanup_failure_still_kills_and_reaps_direct_child() {
#[cfg(unix)]
let mut child = Command::new("/bin/sleep").arg("30").spawn().unwrap();
#[cfg(windows)]
let mut child = Command::new(
super::super::windows::system_directory()
.unwrap()
.join("WindowsPowerShell/v1.0/powershell.exe"),
)
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"[Threading.Thread]::Sleep(30000)",
])
.spawn()
.unwrap();
let started = Instant::now();
let error =
cleanup_windows_child(&mut child, |_| bail!("injected tree failure")).unwrap_err();
assert!(format!("{error:#}").contains("injected tree failure"));
assert!(child.try_wait().unwrap().is_some());
assert!(started.elapsed() < Duration::from_secs(3));
}
#[cfg(unix)]
#[test]
fn cargo_process_failure_and_timeout_are_not_reported_as_success() {
let mut failure = Command::new("/bin/sh");
failure.args(["-c", "exit 7"]);
assert_eq!(
run_cargo(&mut failure, Duration::from_secs(1))
.unwrap()
.code(),
Some(7)
);
let mut slow = Command::new("/bin/sh");
slow.args(["-c", "sleep 30"]);
let error = run_cargo(&mut slow, Duration::from_millis(20)).unwrap_err();
assert!(error.to_string().contains("timed out"), "{error:#}");
}
}