1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Child-process lifecycle and resize.
#[cfg(feature = "async")]
use std::sync::Arc;
use std::time::Duration;
use super::Session;
use super::signal::send_group_signal_silencing_esrch;
use crate::{Error, ExitStatus, Result, Signal};
use tastty::TerminalSize;
impl Session {
/// Return whether the child process is still running.
///
/// Mirrors the polling shape of [`Session::try_wait`] but boils the answer
/// down to a single boolean for callers that only care about liveness.
/// Returns `false` once the child has been observed to exit.
///
/// If the underlying reap fails, returns `true`: the convention is "I do
/// not know that the child is dead, so assume it is not". Callers that
/// need to distinguish a reap failure from a still-running child should
/// use [`Session::try_wait`] instead and inspect the [`Result`].
#[must_use]
pub fn is_alive(&self) -> bool {
match self.terminal.try_wait() {
Ok(Some(_)) => false,
Ok(None) | Err(_) => true,
}
}
/// Return the child exit status if it is already available without
/// blocking.
///
/// Modeled on [`std::process::Child::try_wait`]:
/// `Ok(Some(status))` if the child has exited, `Ok(None)` if it is
/// still running.
///
/// # Errors
///
/// Returns [`Error::ExitStatus`] wrapping
/// [`tastty::Error::ExitStatusUnavailable`] if the internal waiter
/// thread has dropped its sender (the session was joined or the
/// terminal was destroyed).
pub fn try_wait(&self) -> Result<Option<ExitStatus>> {
self.terminal
.try_wait()
.map(|maybe| maybe.map(ExitStatus::from_tastty))
.map_err(Error::ExitStatus)
}
/// Return the managed child process id, if available.
#[must_use]
pub fn process_id(&self) -> Option<u32> {
self.terminal.process_id()
}
/// Block the calling thread until the child exits.
///
/// Coalesces every concurrent waiter (sync and async) onto the
/// session's exit reaper: the reaper polls `try_wait` once per tick
/// and notifies a shared condvar plus any registered async wakers
/// when it observes the exit. The calling thread parks on the
/// condvar until then, with no per-call polling thread and no busy-
/// poll between ticks.
///
/// # Errors
///
/// Returns [`Error::ExitStatus`] wrapping
/// [`tastty::Error::ExitStatusUnavailable`] if the reaper finalised
/// without observing a clean exit (a `try_wait` syscall failure or
/// the [`Session`] being dropped before the child exited).
pub fn wait_exit(&self) -> Result<ExitStatus> {
self.exit_notifier.wait_blocking()
}
/// Asynchronous variant of [`Session::wait_exit`].
///
/// Returns a runtime-agnostic [`Future`](std::future::Future) built
/// on `std` primitives only (no tokio dependency). The future
/// registers a [`Waker`](std::task::Waker) on the session's shared
/// exit notifier; the reaper fires the waker when it observes the
/// child exit. There is no per-future worker thread, no busy-poll,
/// and no timeout - callers that want one layer it themselves (for
/// example, with `tokio::time::timeout`).
///
/// # Cancellation
///
/// Dropping the returned future removes its waker entry from the
/// notifier. No background thread is spawned on its behalf, so
/// cancellation is immediate and leak-free.
///
/// # Errors
///
/// Resolves to [`Error::ExitStatus`] wrapping
/// [`tastty::Error::ExitStatusUnavailable`] if the reaper finalised
/// without observing a clean exit (a `try_wait` syscall failure or
/// the [`Session`] being dropped before the child exited).
///
/// # Send bound
///
/// The returned future is `Send + 'static` so it can be spawned on
/// any standard executor.
#[cfg(feature = "async")]
pub fn wait_exit_async(
&self,
) -> impl std::future::Future<Output = Result<ExitStatus>> + Send + 'static {
crate::wait::exit::ExitWaitFuture::new(Arc::clone(&self.exit_notifier))
}
/// Send a termination signal to the process group when supported.
///
/// # Errors
///
/// Same as [`Session::signal_group`].
pub fn terminate(&self) -> Result<()> {
self.signal_group(Signal::TERM)
}
/// Terminate the child gracefully, escalating to `SIGKILL` after
/// `grace` if it has not exited.
///
/// Sends `SIGTERM` to the child process group and parks on the
/// shared exit notifier. If the child exits within `grace`, the
/// observed [`ExitStatus`] is returned and no further signals are
/// sent. Otherwise `SIGKILL` is sent to the same group and the
/// call blocks (without further bound) until the reaper observes
/// the exit; `SIGKILL` cannot be caught or ignored, so the wait
/// terminates as soon as the kernel delivers and the reaper next
/// polls.
///
/// Both `kill(2)` calls silence `ESRCH`: a "no such process" error
/// at either step means the desired post-condition (process gone)
/// already holds, so the call falls through to the cached exit
/// status instead of surfacing a misleading [`Error::Signal`].
/// Other signal-send errors are returned as [`Error::Signal`].
///
/// # Returned status
///
/// On the graceful path, [`ExitStatus::signal`] reflects the signal
/// the kernel actually recorded for the leader (typically `SIGTERM`
/// for cooperative children, though a child that installs a handler
/// may exit cleanly or through a different signal). On the hard
/// path, [`ExitStatus::signal`] is `SIGKILL`. On the already-exited
/// path the cached status is returned unchanged.
///
/// # Errors
///
/// Returns [`Error::ExitStatus`] if the exit reaper finalised the
/// notifier without observing a clean exit (a `try_wait` syscall
/// failure or the [`Session`] being dropped concurrently). Returns
/// [`Error::Signal`] for `kill(2)` failures other than `ESRCH`.
pub fn terminate_with_grace(&self, grace: Duration) -> Result<ExitStatus> {
send_group_signal_silencing_esrch(self, Signal::TERM)?;
if let Some(status) = self.exit_notifier.wait_blocking_timeout(grace) {
return status;
}
send_group_signal_silencing_esrch(self, Signal::KILL)?;
self.wait_exit()
}
/// Terminate and, if needed, force-kill the process group.
///
/// # Errors
///
/// Returns [`Error::Kill`] wrapping the underlying [`tastty::Error`]
/// (typically [`Error::TerminateFailed`] or [`Error::ForceKillFailed`])
/// when either signal-send fails.
///
/// [`Error::TerminateFailed`]: tastty::Error::TerminateFailed
/// [`Error::ForceKillFailed`]: tastty::Error::ForceKillFailed
pub fn kill(&self) -> Result<()> {
self.terminal.kill().map_err(Error::Kill)
}
/// Resize the PTY and parsed screen.
///
/// # Errors
///
/// Returns [`Error::Resize`] wrapping [`tastty::Error::InvalidResize`]
/// when either dimension is zero, or [`tastty::Error::ResizeFailed`]
/// when the underlying PTY `TIOCSWINSZ` ioctl fails.
pub fn resize(&self, size: TerminalSize) -> Result<()> {
self.terminal.resize(size).map_err(Error::Resize)?;
if let Some(observer) = &self.observer {
observer.on_resize(size);
}
Ok(())
}
}