fix: clean up children on exit
This commit is contained in:
Generated
+1
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"gethostname",
|
||||
"libc",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -24,6 +24,7 @@ assets = [
|
||||
anyhow = "1.0.103"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
gethostname = "1.1.0"
|
||||
libc = "0.2"
|
||||
ratatui = "0.30.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.150"
|
||||
|
||||
+66
-4
@@ -1,17 +1,40 @@
|
||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
use crate::config::LauncherEntry;
|
||||
|
||||
/// Run `python <entrypoint>` on the system with `env` added to its environment,
|
||||
/// waiting for it to finish.
|
||||
/// Process-group id of the currently running child, shared with the signal
|
||||
/// handler. Zero when no child is running.
|
||||
static CHILD_PGID: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
/// Forward termination to the launched process group so the whole tree exits.
|
||||
///
|
||||
/// Only calls the async-signal-safe `killpg`, so it is safe to run in a signal
|
||||
/// handler.
|
||||
extern "C" fn terminate_child(_signum: libc::c_int) {
|
||||
let pgid = CHILD_PGID.load(Ordering::SeqCst);
|
||||
if pgid > 0 {
|
||||
unsafe { libc::killpg(pgid, libc::SIGTERM) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `python <entrypoint>` on the system with `env` added to its environment.
|
||||
///
|
||||
/// Blocks until the process exits. The child runs in its own process group so
|
||||
/// that an interrupt (Ctrl-C) — or normal completion — tears down the whole
|
||||
/// process tree it spawned, rather than leaving anything running in the
|
||||
/// background.
|
||||
fn run(python: &Path, entrypoint: &Path, env: &[(String, String)]) -> Result<()> {
|
||||
let status = Command::new(python)
|
||||
let mut child = Command::new(python)
|
||||
.arg(entrypoint)
|
||||
.envs(env.to_owned())
|
||||
.status()
|
||||
// New process group led by the child, so we can signal the whole tree.
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to launch {} {}",
|
||||
@@ -20,6 +43,31 @@ fn run(python: &Path, entrypoint: &Path, env: &[(String, String)]) -> Result<()>
|
||||
)
|
||||
})?;
|
||||
|
||||
// The child is its own group leader, so the group id equals its pid.
|
||||
let pgid = child.id() as libc::pid_t;
|
||||
CHILD_PGID.store(pgid, Ordering::SeqCst);
|
||||
set_signal_handler(terminate_child as *const () as usize);
|
||||
|
||||
// Block until the process exits, then make sure nothing it spawned is left
|
||||
// behind in the group.
|
||||
let status = child.wait().with_context(|| {
|
||||
format!(
|
||||
"failed to wait for {} {}",
|
||||
python.display(),
|
||||
entrypoint.display()
|
||||
)
|
||||
});
|
||||
|
||||
CHILD_PGID.store(0, Ordering::SeqCst);
|
||||
set_signal_handler(libc::SIG_DFL);
|
||||
unsafe { libc::killpg(pgid, libc::SIGTERM) };
|
||||
|
||||
let status = status?;
|
||||
|
||||
// A launch we terminated on Ctrl-C is an expected exit, not an error.
|
||||
if matches!(status.signal(), Some(libc::SIGINT | libc::SIGTERM)) {
|
||||
return Ok(());
|
||||
}
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"{} {} exited with {status}",
|
||||
@@ -31,6 +79,20 @@ fn run(python: &Path, entrypoint: &Path, env: &[(String, String)]) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Point `SIGINT`/`SIGTERM` at `handler` (a handler address or `SIG_DFL`).
|
||||
fn set_signal_handler(handler: libc::sighandler_t) {
|
||||
for signum in [libc::SIGINT, libc::SIGTERM] {
|
||||
unsafe {
|
||||
let mut action: libc::sigaction = std::mem::zeroed();
|
||||
action.sa_sigaction = handler;
|
||||
libc::sigemptyset(&mut action.sa_mask);
|
||||
// Restart interrupted syscalls so our own `wait` is not disturbed.
|
||||
action.sa_flags = libc::SA_RESTART;
|
||||
libc::sigaction(signum, &action, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `python <entrypoint>` on the system with `env` added to its environment,
|
||||
/// waiting for it to finish.
|
||||
pub fn launch_with_env(python: &Path, entrypoint: &Path, env: &[(String, String)]) -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user