qframe/runtime/live_child.rs
1//! A program that keeps running after a detached handoff and talks to the application through
2//! its standard input and output.
3//!
4//! One thread reads the program's output from the moment it starts: the handoff waits on it for
5//! the first line, and once the application has the screen back the same thread hands every
6//! later line to the application's messages. Reading never stops in between, so no line written
7//! right after the first one is lost, and a program that writes faster than the application reads
8//! waits on its full pipe instead of piling lines up in memory.
9
10use std::collections::VecDeque;
11use std::fmt;
12use std::io::{self, Read, Write};
13use std::process::{Child, ChildStdin, ChildStdout};
14use std::sync::mpsc::{Receiver, SyncSender};
15use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
16use std::time::Duration;
17
18use super::process::{CHUNK, Lines};
19
20/// What a [`LiveChild`] says, delivered through
21/// [`DetachedHandoff::on_line`](super::DetachedHandoff::on_line).
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ChildLine {
24 /// One line the program wrote on its standard output after its first one, without the
25 /// newline. Text that is not UTF-8 is replaced rather than dropped.
26 Line(String),
27 /// The program closed its standard output and ended. Nothing follows.
28 Ended {
29 /// The exit code, or `None` when a signal ended it.
30 code: Option<i32>,
31 },
32}
33
34/// Where the lines of a live child go once the application has the screen back.
35pub(crate) type Sink = Box<dyn FnMut(ChildLine) + Send>;
36
37/// A program a [`DetachedHandoff`](super::DetachedHandoff) left running: its standard input is
38/// the application's to write, its later output arrives as messages through
39/// [`DetachedHandoff::on_line`](super::DetachedHandoff::on_line).
40///
41/// Clones share the one program, so the application can keep one in its state and move others
42/// into background work. When the last clone is dropped — at the latest when the application's
43/// state is dropped as the run ends — the program's standard input is closed and it reads the
44/// end of its input; ending it before that is the application's decision
45/// ([`LiveChild::close_stdin`], [`LiveChild::kill`]).
46///
47/// After its first line the program runs in the background of the terminal, which the
48/// application draws on again. Its standard error is still the terminal, so it should keep quiet
49/// there from then on: anything it writes lands on the application's screen until the next full
50/// redraw.
51#[derive(Clone)]
52pub struct LiveChild {
53 inner: Arc<Inner>,
54}
55
56enum Inner {
57 Real(Real),
58 Double(Arc<Double>),
59}
60
61/// A real program and the ends of its pipes the application holds.
62struct Real {
63 id: u32,
64 stdin: Mutex<Option<ChildStdin>>,
65 process: Arc<Mutex<Child>>,
66 /// Hands the reading thread where to send the lines; taken by the first attach.
67 attach: Mutex<Option<SyncSender<Attach>>>,
68}
69
70/// What the reading thread needs once the handoff is over.
71pub(crate) struct Attach {
72 sink: Sink,
73 process: Arc<Mutex<Child>>,
74}
75
76fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
77 mutex.lock().unwrap_or_else(PoisonError::into_inner)
78}
79
80impl LiveChild {
81 /// The program `process`, whose output the thread behind `attach` reads.
82 pub(crate) fn running(process: Child, stdin: ChildStdin, attach: SyncSender<Attach>) -> Self {
83 Self {
84 inner: Arc::new(Inner::Real(Real {
85 id: process.id(),
86 stdin: Mutex::new(Some(stdin)),
87 process: Arc::new(Mutex::new(process)),
88 attach: Mutex::new(Some(attach)),
89 })),
90 }
91 }
92
93 /// A stand-in for tests, with the handle a test drives it through: what the application
94 /// writes is recorded there, and the test says the program's lines and ends it. Answer a
95 /// detached handoff with it through
96 /// [`Harness::set_detached_outcome`](super::Harness::set_detached_outcome).
97 ///
98 /// ```
99 /// use qframe::runtime::{ChildLine, LiveChild};
100 ///
101 /// let (child, program) = LiveChild::for_tests();
102 /// child.write_line("status")?;
103 /// assert_eq!(program.written(), ["status"]);
104 /// assert_eq!(child.try_wait()?, None, "it runs until the test ends it");
105 /// program.exit(Some(0));
106 /// assert_eq!(child.try_wait()?, Some(Some(0)));
107 /// # let _ = ChildLine::Line(String::new());
108 /// # Ok::<(), std::io::Error>(())
109 /// ```
110 #[must_use]
111 pub fn for_tests() -> (Self, TestChild) {
112 let double = Arc::new(Double::default());
113 (Self { inner: Arc::new(Inner::Double(Arc::clone(&double))) }, TestChild { double })
114 }
115
116 /// The program's process id, or `None` for the stand-in of [`LiveChild::for_tests`].
117 #[must_use]
118 pub fn id(&self) -> Option<u32> {
119 match &*self.inner {
120 Inner::Real(real) => Some(real.id),
121 Inner::Double(_) => None,
122 }
123 }
124
125 /// Writes `line` and a newline to the program's standard input. A `line` holding newlines
126 /// reaches the program as several lines.
127 ///
128 /// The write blocks while the pipe is full, which only happens when the program stops
129 /// reading; a program that answers each line keeps it empty.
130 ///
131 /// # Errors
132 ///
133 /// Returns an error once the standard input is closed ([`io::ErrorKind::BrokenPipe`]) or
134 /// the program no longer reads it.
135 pub fn write_line(&self, line: &str) -> io::Result<()> {
136 match &*self.inner {
137 Inner::Real(real) => {
138 let mut stdin = lock(&real.stdin);
139 let pipe = stdin.as_mut().ok_or_else(closed)?;
140 let mut bytes = Vec::with_capacity(line.len() + 1);
141 bytes.extend_from_slice(line.as_bytes());
142 bytes.push(b'\n');
143 pipe.write_all(&bytes)?;
144 pipe.flush()
145 }
146 Inner::Double(double) => {
147 let mut state = lock(&double.state);
148 if !state.stdin_open || state.code.is_some() {
149 return Err(closed());
150 }
151 state.written.push(line.to_owned());
152 Ok(())
153 }
154 }
155 }
156
157 /// Closes the program's standard input, for every clone: the program reads the end of its
158 /// input, which is how a helper that serves one line at a time is asked to finish. Closing
159 /// it again does nothing.
160 pub fn close_stdin(&self) {
161 match &*self.inner {
162 Inner::Real(real) => drop(lock(&real.stdin).take()),
163 Inner::Double(double) => lock(&double.state).stdin_open = false,
164 }
165 }
166
167 /// Ends the program at once (`SIGKILL` on Unix); its [`ChildLine::Ended`] follows. Killing
168 /// one that already ended does nothing.
169 ///
170 /// # Errors
171 ///
172 /// Returns the system's error, such as when the program runs as another user and may not
173 /// be signalled, which is the case for one started through `pkexec` or `sudo`: close its
174 /// standard input instead.
175 pub fn kill(&self) -> io::Result<()> {
176 match &*self.inner {
177 Inner::Real(real) => lock(&real.process).kill(),
178 Inner::Double(double) => {
179 let mut state = lock(&double.state);
180 state.killed = true;
181 state.end(None);
182 Ok(())
183 }
184 }
185 }
186
187 /// How the program ended, without waiting: `None` while it runs, then `Some(code)`, where
188 /// `code` is `None` when a signal ended it, as in
189 /// [`HandoffOutcome::Finished`](super::HandoffOutcome::Finished).
190 ///
191 /// # Errors
192 ///
193 /// Returns the system's error when the program's state cannot be read.
194 pub fn try_wait(&self) -> io::Result<Option<Option<i32>>> {
195 match &*self.inner {
196 Inner::Real(real) => Ok(lock(&real.process).try_wait()?.map(|status| status.code())),
197 Inner::Double(double) => Ok(lock(&double.state).code),
198 }
199 }
200
201 /// Sends the program's lines to `sink` from now on. Only the first call counts: the lines
202 /// have one reader.
203 pub(crate) fn attach(&self, sink: Sink) {
204 match &*self.inner {
205 Inner::Real(real) => {
206 if let Some(attach) = lock(&real.attach).take() {
207 // The reading thread takes this once and then ends with the program. A send
208 // that fails means the program is already over, so there are no more lines
209 // for the sink to be given.
210 let _ = attach.send(Attach { sink, process: Arc::clone(&real.process) });
211 }
212 }
213 Inner::Double(double) => {
214 let mut state = lock(&double.state);
215 if state.sink.is_none() {
216 let mut sink = sink;
217 for line in state.waiting.drain(..) {
218 sink(line);
219 }
220 state.sink = Some(sink);
221 }
222 }
223 }
224 }
225}
226
227fn closed() -> io::Error {
228 io::Error::new(io::ErrorKind::BrokenPipe, "the program's standard input is closed")
229}
230
231impl Drop for Inner {
232 fn drop(&mut self) {
233 // A real program's input closes as its pipe is dropped with it; the stand-in records it.
234 if let Self::Double(double) = self {
235 lock(&double.state).stdin_open = false;
236 }
237 }
238}
239
240impl fmt::Debug for LiveChild {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match &*self.inner {
243 Inner::Real(real) => f.debug_struct("LiveChild").field("id", &real.id).finish_non_exhaustive(),
244 Inner::Double(_) => f.write_str("LiveChild(test)"),
245 }
246 }
247}
248
249/// Two handles are equal when they are clones of each other: the same program.
250impl PartialEq for LiveChild {
251 fn eq(&self, other: &Self) -> bool {
252 Arc::ptr_eq(&self.inner, &other.inner)
253 }
254}
255
256impl Eq for LiveChild {}
257
258/// The test's side of the stand-in from [`LiveChild::for_tests`]: it plays the program.
259///
260/// Lines it says reach the application through
261/// [`DetachedHandoff::on_line`](super::DetachedHandoff::on_line) at the harness's next step, such
262/// as [`Harness::render`](super::Harness::render); lines said before the handoff was answered
263/// wait for it, as they would in a pipe.
264#[derive(Clone)]
265pub struct TestChild {
266 double: Arc<Double>,
267}
268
269#[derive(Default)]
270struct Double {
271 state: Mutex<DoubleState>,
272}
273
274struct DoubleState {
275 written: Vec<String>,
276 stdin_open: bool,
277 killed: bool,
278 code: Option<Option<i32>>,
279 sink: Option<Sink>,
280 /// What was said before a sink was attached, oldest first.
281 waiting: Vec<ChildLine>,
282}
283
284impl Default for DoubleState {
285 fn default() -> Self {
286 Self { written: Vec::new(), stdin_open: true, killed: false, code: None, sink: None, waiting: Vec::new() }
287 }
288}
289
290impl DoubleState {
291 fn say(&mut self, line: ChildLine) {
292 match &mut self.sink {
293 Some(sink) => sink(line),
294 None => self.waiting.push(line),
295 }
296 }
297
298 fn end(&mut self, code: Option<i32>) {
299 if self.code.is_none() {
300 self.code = Some(code);
301 self.say(ChildLine::Ended { code });
302 }
303 }
304}
305
306impl TestChild {
307 /// The program writes `line` on its standard output. Nothing is said after it ended.
308 pub fn say(&self, line: impl Into<String>) {
309 let mut state = lock(&self.double.state);
310 if state.code.is_none() {
311 state.say(ChildLine::Line(line.into()));
312 }
313 }
314
315 /// The program ends with `code` (`None` for a signal): [`LiveChild::try_wait`] reports it
316 /// and [`ChildLine::Ended`] is delivered. Only the first end counts.
317 pub fn exit(&self, code: Option<i32>) {
318 lock(&self.double.state).end(code);
319 }
320
321 /// Every line the application wrote with [`LiveChild::write_line`], oldest first.
322 #[must_use]
323 pub fn written(&self) -> Vec<String> {
324 lock(&self.double.state).written.clone()
325 }
326
327 /// Whether the program's standard input is still open: `false` after
328 /// [`LiveChild::close_stdin`] and once every [`LiveChild`] clone was dropped.
329 #[must_use]
330 pub fn stdin_open(&self) -> bool {
331 lock(&self.double.state).stdin_open
332 }
333
334 /// Whether the application called [`LiveChild::kill`].
335 #[must_use]
336 pub fn killed(&self) -> bool {
337 lock(&self.double.state).killed
338 }
339}
340
341impl fmt::Debug for TestChild {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 f.write_str("TestChild")
344 }
345}
346
347/// The longest pause between two looks at a program that closed its output but has not ended.
348const LONGEST_LOOK: Duration = Duration::from_millis(500);
349
350/// Reads the program's output on the calling thread: the first line (or `None` at the end of
351/// the output) goes to `first`, every later line to the sink `attach` delivers, and the end of
352/// the program after them.
353///
354/// When nobody attaches — the handoff ended without detaching, or the application dropped the
355/// child before it had the screen back — the rest of the output is read and dropped, so the
356/// program is never stopped by a full pipe or ended by a closed one on the way out.
357pub(crate) fn read(mut stdout: ChildStdout, first: &SyncSender<Option<String>>, attach: &Receiver<Attach>) {
358 let mut lines = Lines::default();
359 let mut ready = VecDeque::new();
360 let mut chunk = [0_u8; CHUNK];
361 let mut open = true;
362 while open && ready.is_empty() {
363 open = read_some(&mut stdout, &mut chunk, &mut lines, &mut ready);
364 }
365 let first_line = ready.pop_front();
366 let said = first_line.is_some();
367 if first.send(first_line).is_err() || !said {
368 drain(open, &mut stdout, &mut chunk);
369 return;
370 }
371 let Ok(Attach { mut sink, process }) = attach.recv() else {
372 drain(open, &mut stdout, &mut chunk);
373 return;
374 };
375 loop {
376 for line in ready.drain(..) {
377 sink(ChildLine::Line(line));
378 }
379 if !open {
380 break;
381 }
382 open = read_some(&mut stdout, &mut chunk, &mut lines, &mut ready);
383 }
384 drop(stdout);
385 sink(ChildLine::Ended { code: wait_for_end(&process) });
386}
387
388/// Reads once, adding finished lines to `ready`. Returns whether the output is still open.
389fn read_some(stdout: &mut ChildStdout, chunk: &mut [u8], lines: &mut Lines, ready: &mut VecDeque<String>) -> bool {
390 loop {
391 match stdout.read(chunk) {
392 Ok(0) => break,
393 Ok(count) => {
394 lines.feed(&chunk[..count], &mut |line| ready.push_back(line));
395 return true;
396 }
397 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
398 Err(_) => break,
399 }
400 }
401 lines.finish(&mut |line| ready.push_back(line));
402 false
403}
404
405fn drain(open: bool, stdout: &mut ChildStdout, chunk: &mut [u8]) {
406 if open {
407 while !matches!(stdout.read(chunk), Ok(0) | Err(_)) {}
408 }
409}
410
411/// Waits for a program whose output ended to end too, and returns its exit code. It usually
412/// ends right away; one that only closed its output is looked at less and less often.
413fn wait_for_end(process: &Mutex<Child>) -> Option<i32> {
414 let mut pause = Duration::from_millis(5);
415 loop {
416 match lock(process).try_wait() {
417 Ok(Some(status)) => return status.code(),
418 Ok(None) => {}
419 Err(_) => return None,
420 }
421 std::thread::sleep(pause);
422 pause = (pause * 2).min(LONGEST_LOOK);
423 }
424}