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                    let _ = attach.send(Attach { sink, process: Arc::clone(&real.process) });
208                }
209            }
210            Inner::Double(double) => {
211                let mut state = lock(&double.state);
212                if state.sink.is_none() {
213                    let mut sink = sink;
214                    for line in state.waiting.drain(..) {
215                        sink(line);
216                    }
217                    state.sink = Some(sink);
218                }
219            }
220        }
221    }
222}
223
224fn closed() -> io::Error {
225    io::Error::new(io::ErrorKind::BrokenPipe, "the program's standard input is closed")
226}
227
228impl Drop for Inner {
229    fn drop(&mut self) {
230        // A real program's input closes as its pipe is dropped with it; the stand-in records it.
231        if let Self::Double(double) = self {
232            lock(&double.state).stdin_open = false;
233        }
234    }
235}
236
237impl fmt::Debug for LiveChild {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match &*self.inner {
240            Inner::Real(real) => f.debug_struct("LiveChild").field("id", &real.id).finish_non_exhaustive(),
241            Inner::Double(_) => f.write_str("LiveChild(test)"),
242        }
243    }
244}
245
246/// Two handles are equal when they are clones of each other: the same program.
247impl PartialEq for LiveChild {
248    fn eq(&self, other: &Self) -> bool {
249        Arc::ptr_eq(&self.inner, &other.inner)
250    }
251}
252
253impl Eq for LiveChild {}
254
255/// The test's side of the stand-in from [`LiveChild::for_tests`]: it plays the program.
256///
257/// Lines it says reach the application through
258/// [`DetachedHandoff::on_line`](super::DetachedHandoff::on_line) at the harness's next step, such
259/// as [`Harness::render`](super::Harness::render); lines said before the handoff was answered
260/// wait for it, as they would in a pipe.
261#[derive(Clone)]
262pub struct TestChild {
263    double: Arc<Double>,
264}
265
266#[derive(Default)]
267struct Double {
268    state: Mutex<DoubleState>,
269}
270
271struct DoubleState {
272    written: Vec<String>,
273    stdin_open: bool,
274    killed: bool,
275    code: Option<Option<i32>>,
276    sink: Option<Sink>,
277    /// What was said before a sink was attached, oldest first.
278    waiting: Vec<ChildLine>,
279}
280
281impl Default for DoubleState {
282    fn default() -> Self {
283        Self { written: Vec::new(), stdin_open: true, killed: false, code: None, sink: None, waiting: Vec::new() }
284    }
285}
286
287impl DoubleState {
288    fn say(&mut self, line: ChildLine) {
289        match &mut self.sink {
290            Some(sink) => sink(line),
291            None => self.waiting.push(line),
292        }
293    }
294
295    fn end(&mut self, code: Option<i32>) {
296        if self.code.is_none() {
297            self.code = Some(code);
298            self.say(ChildLine::Ended { code });
299        }
300    }
301}
302
303impl TestChild {
304    /// The program writes `line` on its standard output. Nothing is said after it ended.
305    pub fn say(&self, line: impl Into<String>) {
306        let mut state = lock(&self.double.state);
307        if state.code.is_none() {
308            state.say(ChildLine::Line(line.into()));
309        }
310    }
311
312    /// The program ends with `code` (`None` for a signal): [`LiveChild::try_wait`] reports it
313    /// and [`ChildLine::Ended`] is delivered. Only the first end counts.
314    pub fn exit(&self, code: Option<i32>) {
315        lock(&self.double.state).end(code);
316    }
317
318    /// Every line the application wrote with [`LiveChild::write_line`], oldest first.
319    #[must_use]
320    pub fn written(&self) -> Vec<String> {
321        lock(&self.double.state).written.clone()
322    }
323
324    /// Whether the program's standard input is still open: `false` after
325    /// [`LiveChild::close_stdin`] and once every [`LiveChild`] clone was dropped.
326    #[must_use]
327    pub fn stdin_open(&self) -> bool {
328        lock(&self.double.state).stdin_open
329    }
330
331    /// Whether the application called [`LiveChild::kill`].
332    #[must_use]
333    pub fn killed(&self) -> bool {
334        lock(&self.double.state).killed
335    }
336}
337
338impl fmt::Debug for TestChild {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        f.write_str("TestChild")
341    }
342}
343
344/// The longest pause between two looks at a program that closed its output but has not ended.
345const LONGEST_LOOK: Duration = Duration::from_millis(500);
346
347/// Reads the program's output on the calling thread: the first line (or `None` at the end of
348/// the output) goes to `first`, every later line to the sink `attach` delivers, and the end of
349/// the program after them.
350///
351/// When nobody attaches — the handoff ended without detaching, or the application dropped the
352/// child before it had the screen back — the rest of the output is read and dropped, so the
353/// program is never stopped by a full pipe or ended by a closed one on the way out.
354pub(crate) fn read(mut stdout: ChildStdout, first: &SyncSender<Option<String>>, attach: &Receiver<Attach>) {
355    let mut lines = Lines::default();
356    let mut ready = VecDeque::new();
357    let mut chunk = [0_u8; CHUNK];
358    let mut open = true;
359    while open && ready.is_empty() {
360        open = read_some(&mut stdout, &mut chunk, &mut lines, &mut ready);
361    }
362    let first_line = ready.pop_front();
363    let said = first_line.is_some();
364    if first.send(first_line).is_err() || !said {
365        drain(open, &mut stdout, &mut chunk);
366        return;
367    }
368    let Ok(Attach { mut sink, process }) = attach.recv() else {
369        drain(open, &mut stdout, &mut chunk);
370        return;
371    };
372    loop {
373        for line in ready.drain(..) {
374            sink(ChildLine::Line(line));
375        }
376        if !open {
377            break;
378        }
379        open = read_some(&mut stdout, &mut chunk, &mut lines, &mut ready);
380    }
381    drop(stdout);
382    sink(ChildLine::Ended { code: wait_for_end(&process) });
383}
384
385/// Reads once, adding finished lines to `ready`. Returns whether the output is still open.
386fn read_some(stdout: &mut ChildStdout, chunk: &mut [u8], lines: &mut Lines, ready: &mut VecDeque<String>) -> bool {
387    loop {
388        match stdout.read(chunk) {
389            Ok(0) => break,
390            Ok(count) => {
391                lines.feed(&chunk[..count], &mut |line| ready.push_back(line));
392                return true;
393            }
394            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
395            Err(_) => break,
396        }
397    }
398    lines.finish(&mut |line| ready.push_back(line));
399    false
400}
401
402fn drain(open: bool, stdout: &mut ChildStdout, chunk: &mut [u8]) {
403    if open {
404        while !matches!(stdout.read(chunk), Ok(0) | Err(_)) {}
405    }
406}
407
408/// Waits for a program whose output ended to end too, and returns its exit code. It usually
409/// ends right away; one that only closed its output is looked at less and less often.
410fn wait_for_end(process: &Mutex<Child>) -> Option<i32> {
411    let mut pause = Duration::from_millis(5);
412    loop {
413        match lock(process).try_wait() {
414            Ok(Some(status)) => return status.code(),
415            Ok(None) => {}
416            Err(_) => return None,
417        }
418        std::thread::sleep(pause);
419        pause = (pause * 2).min(LONGEST_LOOK);
420    }
421}