From 75c687d82a615ed9faea0c4627e9970d92c9dea7 Mon Sep 17 00:00:00 2001 From: David Perl Date: Thu, 20 Aug 2026 09:31:55 +0200 Subject: [PATCH 1/2] fix: clean up children on exit --- Cargo.lock | 1 + Cargo.toml | 1 + src/launcher.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3881ea..d4cdf00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "anyhow", "clap", "gethostname", + "libc", "ratatui", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 558f0e9..63baa7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/launcher.rs b/src/launcher.rs index 5d23fd9..f2981c3 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -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 ` 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 ` 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 ` 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<()> { -- 2.54.0 From 805c278cb540337efc6fd8652bb1665edc8b8b6e Mon Sep 17 00:00:00 2001 From: David Perl Date: Thu, 20 Aug 2026 09:40:46 +0200 Subject: [PATCH 2/2] docs: update readme --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4f719e3..ee3b89e 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,22 @@ aarel server # launch the server for the detected beamline ``` Each command has a single-letter alias (`p`, `g`, `s`). `--config ` -selects a different config file; it defaults to `data/defaults.json`. +selects a different config file; it defaults to `/etc/aarel/defaults.json`. ### Launching -`gui` and `server` pick their config entry from the detected beamline. Use -`--entry ` to launch a different one. +`gui` and `server` default to the deployment for the detected beamline. Pass +`-d`/`--deployment ` to launch a specific one: + +``` +aarel gui -d x10sa # launch the x10sa GUI +aarel server -d mytest # launch a custom deployment's server +aarel server --deployment x06da --env-file ./dev.env +``` + +Deployments found under `deployments_base` are offered alongside the configured +beamlines, so `-d` accepts either a configured beamline name or a discovered +deployment. ## Development -- 2.54.0