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
//! Persistent terminal driver session.
mod inspection;
mod io;
mod lifecycle;
mod signal;
mod spawn;
mod wait;
pub use spawn::DriverOptions;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crate::ExitStatus;
use crate::observer::IoObserver;
use crate::wait::exit::ExitNotifier;
use crate::wait::output::OutputNotifier;
use tastty::Terminal;
const REAPER_POLL_INTERVAL: Duration = Duration::from_millis(50);
/// Persistent terminal driver session.
pub struct Session {
terminal: Arc<Terminal>,
observer: Option<Arc<dyn IoObserver>>,
exit_notifier: Arc<ExitNotifier>,
output_notifier: Arc<OutputNotifier>,
output_timer: Option<JoinHandle<()>>,
reaper_shutdown: Arc<AtomicBool>,
reaper: Option<JoinHandle<()>>,
}
impl Session {
/// Clone of the shared terminal handle. The wait module uses this to
/// drive both the synchronous wait loop and the async wait future
/// without taking a `&Session` reference, so the async future can
/// outlive the [`Session`] that produced it.
pub(crate) fn terminal_handle(&self) -> Arc<Terminal> {
Arc::clone(&self.terminal)
}
/// Clone of the session's [`OutputNotifier`]. The synchronous
/// [`Session::wait`] path uses this to block on tick wakes; async
/// wait futures take their own [`Arc`] so they can outlive the
/// [`Session`] (the timer thread is joined on session drop, but the
/// notifier itself is reference-counted, so a future still holding an
/// Arc just observes no further ticks after the session is gone).
pub(crate) fn output_notifier(&self) -> Arc<OutputNotifier> {
Arc::clone(&self.output_notifier)
}
}
impl Drop for Session {
fn drop(&mut self) {
self.reaper_shutdown.store(true, Ordering::Release);
if let Some(handle) = self.reaper.take() {
drop(handle.join());
}
self.output_notifier.shutdown();
if let Some(handle) = self.output_timer.take() {
drop(handle.join());
}
}
}
fn exit_reaper(
terminal: Arc<Terminal>,
exit: Arc<ExitNotifier>,
output: Arc<OutputNotifier>,
observer: Option<Arc<dyn IoObserver>>,
shutdown: Arc<AtomicBool>,
) {
loop {
let shutting_down = shutdown.load(Ordering::Acquire);
match terminal.try_wait() {
Ok(Some(status)) => {
let status = ExitStatus::from_tastty(status);
exit.notify_exit(status);
// Kick the output notifier so any pending wait future or
// synchronous waiter re-probes its condition. Without this,
// a waiter blocked on tick wakes alone after a clean exit
// (no further parser ticks fire post-EOF) would only
// observe `try_wait` -> Some on its next deadline-driven
// wake, adding latency to the ProcessExitedBeforeMatch
// surface for non-Exit/non-Stable conditions.
output.notify_tick();
if let Some(obs) = &observer {
obs.on_exit(status);
}
return;
}
Ok(None) => {}
Err(_) => {
// Surface to every wait-for-exit consumer (sync and async)
// as a Failed transition. The original syscall error is
// dropped here, mirroring the pre-redesign reaper which
// bailed silently with `Err(_) => return`.
exit.notify_failure();
output.notify_tick();
return;
}
}
// Final check after the post-shutdown poll catches the race where the
// child exited between the last poll and shutdown being signalled.
// If we still have not observed an exit, wake any pending consumers
// with a Failed transition: nobody else will reap this notifier
// after the reaper exits.
if shutting_down {
exit.notify_failure();
output.notify_tick();
return;
}
thread::sleep(REAPER_POLL_INTERVAL);
}
}