Skip to main content

eska_tool_runtime/
process.rs

1//! Delegated interactive processes inherit terminal streams and are always reaped.
2
3use crate::{Component, Error, resolve};
4use std::{
5    ffi::OsString,
6    io,
7    process::{Child, Command, ExitStatus},
8    sync::{Arc, atomic::AtomicUsize},
9};
10#[cfg(windows)]
11use std::{
12    path::Path,
13    process::Stdio,
14    sync::atomic::Ordering,
15    thread,
16    time::{Duration, Instant},
17};
18
19/// Replace the Unix shell with its owner, or await the attached Windows process.
20///
21/// # Errors
22/// Returns discovery/exec failures on Unix, or discovery/handler/process failures on Windows.
23pub fn delegate(component: Component, args: &[OsString]) -> Result<ExitStatus, Error> {
24    let binary = resolve(component)?;
25    #[cfg(unix)]
26    {
27        use std::os::unix::process::CommandExt;
28        Err(Error::Process {
29            component,
30            source: Command::new(binary).args(args).exec(),
31        })
32    }
33    #[cfg(windows)]
34    {
35        let signal = SignalState::new().map_err(Error::Signal)?;
36        execute(&binary, args, &signal).map_err(|source| Error::Process { component, source })
37    }
38}
39
40/// Keep a child attached to the same terminal; the parent never reads or decodes its streams.
41#[cfg(windows)]
42fn execute(binary: &Path, args: &[OsString], signal: &SignalState) -> io::Result<ExitStatus> {
43    let child = Command::new(binary)
44        .args(args)
45        .stdin(Stdio::inherit())
46        .stdout(Stdio::inherit())
47        .stderr(Stdio::inherit())
48        .spawn()?;
49    let mut child = OwnedChild(child);
50    let mut cancellation = None;
51    loop {
52        if let Some(status) = child.0.try_wait()? {
53            return Ok(status);
54        }
55        let received = signal.received.load(Ordering::Acquire);
56        if received != 0 && cancellation.is_none() {
57            cancellation = Some(Instant::now());
58        }
59        if cancellation.is_some_and(|started| started.elapsed() >= Duration::from_secs(10)) {
60            child.0.kill()?;
61            return child.0.wait();
62        }
63        thread::sleep(Duration::from_millis(10));
64    }
65}
66
67pub struct OwnedChild(pub Child);
68
69impl Drop for OwnedChild {
70    /// An early wait/signal failure still reaps the exact child, without searching by process name.
71    fn drop(&mut self) {
72        if !matches!(self.0.try_wait(), Ok(Some(_))) {
73            let _ = self.0.kill();
74            let _ = self.0.wait();
75        }
76    }
77}
78
79pub struct SignalState {
80    pub received: Arc<AtomicUsize>,
81    #[cfg(unix)]
82    handlers: Vec<signal_hook::SigId>,
83}
84
85impl SignalState {
86    /// Record the exact Unix signal without doing I/O in a signal handler.
87    #[cfg(unix)]
88    pub fn new() -> io::Result<Self> {
89        use signal_hook::consts::signal::{SIGHUP, SIGINT, SIGTERM};
90        let mut state = Self {
91            received: Arc::new(AtomicUsize::new(0)),
92            handlers: Vec::new(),
93        };
94        for signal in [SIGINT, SIGTERM, SIGHUP] {
95            state.handlers.push(signal_hook::flag::register_usize(
96                signal,
97                Arc::clone(&state.received),
98                usize::try_from(signal).map_err(io::Error::other)?,
99            )?);
100        }
101        Ok(state)
102    }
103
104    /// Windows console events are already delivered to both attached console processes.
105    #[cfg(windows)]
106    pub fn new() -> io::Result<Self> {
107        let received = Arc::new(AtomicUsize::new(0));
108        let flag = Arc::clone(&received);
109        ctrlc::set_handler(move || flag.store(2, Ordering::Release)).map_err(io::Error::other)?;
110        Ok(Self { received })
111    }
112}
113
114#[cfg(unix)]
115impl Drop for SignalState {
116    /// Release this operation's subscriptions before another command owns process cancellation.
117    fn drop(&mut self) {
118        for handler in self.handlers.drain(..) {
119            signal_hook::low_level::unregister(handler);
120        }
121    }
122}