Fix/run in background #23
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"
|
||||
|
||||
@@ -18,12 +18,22 @@ aarel server # launch the server for the detected beamline
|
||||
```
|
||||
|
||||
Each command has a single-letter alias (`p`, `g`, `s`). `--config <path>`
|
||||
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 <name>` to launch a different one.
|
||||
`gui` and `server` default to the deployment for the detected beamline. Pass
|
||||
`-d`/`--deployment <name>` 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
|
||||
|
||||
|
||||
+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