Skip to main content

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}