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}