1use std::ffi::OsString;
11#[cfg(not(unix))]
12use std::io;
13use std::path::PathBuf;
14use std::process::Stdio;
15#[cfg(not(unix))]
16use std::process::{Child, Command as ChildCommand, ExitStatus};
17use std::sync::Arc;
18use std::sync::mpsc::{self, RecvTimeoutError, Sender};
19use std::time::Duration;
20
21use super::command::MapFn;
22#[cfg(unix)]
23use super::foreground::Foreground;
24use super::handoff::{HandoffRequest, HandoffScreen, Program};
25use super::live_child::{self, ChildLine, LiveChild, Sink};
26use super::task::Delivery;
27
28const LOOK: Duration = Duration::from_millis(20);
31
32const LAST_WORDS: Duration = Duration::from_millis(100);
35
36type LineMessage<Msg> = Arc<dyn Fn(ChildLine) -> Msg + Send + Sync>;
37
38pub struct DetachedHandoff<Msg> {
86 program: Program,
87 on_start: Box<dyn FnOnce(DetachedOutcome) -> Msg + Send>,
88 on_line: Option<LineMessage<Msg>>,
89}
90
91impl<Msg: Send + 'static> DetachedHandoff<Msg> {
92 pub fn new(program: impl Into<OsString>, on_start: impl FnOnce(DetachedOutcome) -> Msg + Send + 'static) -> Self {
95 Self { program: Program::new(program.into()), on_start: Box::new(on_start), on_line: None }
96 }
97
98 #[must_use]
100 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
101 self.program.args.push(arg.into());
102 self
103 }
104
105 #[must_use]
107 pub fn args(mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
108 self.program.args.extend(args.into_iter().map(Into::into));
109 self
110 }
111
112 #[must_use]
114 pub fn dir(mut self, dir: impl Into<PathBuf>) -> Self {
115 self.program.dir = Some(dir.into());
116 self
117 }
118
119 #[must_use]
121 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
122 self.program.env.push((key.into(), value.into()));
123 self
124 }
125
126 #[must_use]
129 pub fn notice(mut self, text: impl Into<String>) -> Self {
130 self.program.notice = Some(text.into());
131 self
132 }
133
134 #[must_use]
138 pub fn pause(mut self, pause: bool) -> Self {
139 self.program.pause = pause;
140 self
141 }
142
143 #[must_use]
148 pub fn on_line(mut self, message: impl Fn(ChildLine) -> Msg + Send + Sync + 'static) -> Self {
149 self.on_line = Some(Arc::new(message));
150 self
151 }
152
153 pub(crate) fn request(&self) -> HandoffRequest {
155 self.program.request()
156 }
157
158 pub(crate) fn finish(self, outcome: DetachedOutcome, deliveries: Sender<Delivery<Msg>>) -> Msg {
161 if let DetachedOutcome::Detached { child, .. } = &outcome {
162 let sink: Sink = match self.on_line {
163 Some(message) => Box::new(move |line| {
164 let _ = deliveries.send(Delivery::Message(message(line)));
167 super::signals::wake();
168 }),
169 None => Box::new(|_| {}),
170 };
171 child.attach(sink);
172 }
173 (self.on_start)(outcome)
174 }
175
176 pub(crate) fn map<B: Send + 'static>(self, map: MapFn<Msg, B>) -> DetachedHandoff<B> {
178 let on_start = self.on_start;
179 let on_line = self.on_line.map(|message| {
180 let map = Arc::clone(&map);
181 Arc::new(move |line| map(message(line))) as LineMessage<B>
182 });
183 DetachedHandoff { program: self.program, on_start: Box::new(move |outcome| map(on_start(outcome))), on_line }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum DetachedOutcome {
190 Detached {
192 child: LiveChild,
194 first_line: String,
196 },
197 Finished {
199 code: Option<i32>,
201 },
202 Failed(String),
204}
205
206pub(crate) fn run<Msg: Send + 'static>(
210 handoff: DetachedHandoff<Msg>,
211 screen: &mut HandoffScreen<'_>,
212 deliveries: &Sender<Delivery<Msg>>,
213) -> Msg {
214 let outcome = match (screen.release)(handoff.program.notice.as_deref()) {
215 Ok(()) => {
216 let outcome = start(&handoff.program);
217 if handoff.program.pause && matches!(outcome, DetachedOutcome::Finished { .. }) {
218 let _ = (screen.wait_for_key)();
222 }
223 match (screen.take)() {
224 Ok(()) => outcome,
225 Err(error) => DetachedOutcome::Failed(error.to_string()),
228 }
229 }
230 Err(error) => {
231 let _ = (screen.take)();
233 DetachedOutcome::Failed(error.to_string())
234 }
235 };
236 handoff.finish(outcome, deliveries.clone())
237}
238
239fn start(program: &Program) -> DetachedOutcome {
242 let mut command = program.command();
247 command.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit());
248 let (mut child, foreground) = match Foreground::spawn(&mut command) {
249 Ok(started) => started,
250 Err(error) => return DetachedOutcome::Failed(error.to_string()),
251 };
252 drop(command);
253 let (Some(stdin), Some(stdout)) = (child.stdin.take(), child.stdout.take()) else {
254 let _ = child.kill();
255 let _ = foreground.wait(&mut child);
256 return DetachedOutcome::Failed("the program's pipes could not be opened".to_owned());
257 };
258 let (first_sender, first) = mpsc::sync_channel(1);
259 let (attach, attached) = mpsc::sync_channel(1);
260 let reader = std::thread::Builder::new()
261 .name("quvyta-live-child".to_owned())
262 .spawn(move || live_child::read(stdout, &first_sender, &attached));
263 if let Err(error) = reader {
264 let _ = child.kill();
265 let _ = foreground.wait(&mut child);
266 return DetachedOutcome::Failed(error.to_string());
267 }
268 let first_line = loop {
269 match first.recv_timeout(LOOK) {
270 Ok(line) => break line,
271 Err(RecvTimeoutError::Disconnected) => break None,
272 Err(RecvTimeoutError::Timeout) => match foreground.check(&mut child) {
273 Ok(None) => {}
274 Ok(Some(_)) => break first.recv_timeout(LAST_WORDS).ok().flatten(),
275 Err(error) => {
276 let _ = child.kill();
277 let _ = foreground.wait(&mut child);
278 return DetachedOutcome::Failed(error.to_string());
279 }
280 },
281 }
282 };
283 match first_line {
284 Some(first_line) => {
285 let child = LiveChild::running(child, stdin, attach);
287 match foreground.give_back() {
288 Ok(()) => DetachedOutcome::Detached { child, first_line },
289 Err(error) => DetachedOutcome::Failed(error.to_string()),
290 }
291 }
292 None => {
293 drop(stdin);
296 let status = foreground.wait(&mut child);
297 let taken = foreground.give_back();
298 match (status, taken) {
299 (Ok(status), Ok(())) => DetachedOutcome::Finished { code: status.code() },
300 (Err(error), _) | (_, Err(error)) => DetachedOutcome::Failed(error.to_string()),
301 }
302 }
303 }
304}
305
306#[cfg(not(unix))]
309struct Foreground;
310
311#[cfg(not(unix))]
312impl Foreground {
313 fn spawn(command: &mut ChildCommand) -> io::Result<(Child, Self)> {
314 Ok((command.spawn()?, Self))
315 }
316
317 fn wait(&self, child: &mut Child) -> io::Result<ExitStatus> {
318 child.wait()
319 }
320
321 fn check(&self, child: &mut Child) -> io::Result<Option<ExitStatus>> {
322 child.try_wait()
323 }
324
325 fn give_back(self) -> io::Result<()> {
326 Ok(())
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use std::cell::RefCell;
333 use std::io;
334 use std::sync::mpsc::{self, Receiver};
335 use std::time::Duration;
336
337 use super::{DetachedHandoff, DetachedOutcome, run};
338 use crate::runtime::ChildLine;
339 use crate::runtime::handoff::HandoffScreen;
340 use crate::runtime::task::Delivery;
341
342 const PATIENCE: Duration = Duration::from_secs(30);
344
345 #[derive(Debug, PartialEq)]
347 enum Heard {
348 Started(DetachedOutcome),
349 Said(ChildLine),
350 }
351
352 fn detach(
355 handoff: DetachedHandoff<Heard>,
356 release_fails: bool,
357 ) -> (DetachedOutcome, Vec<String>, Receiver<Delivery<Heard>>) {
358 let steps = RefCell::new(Vec::new());
359 let mut release = |notice: Option<&str>| -> io::Result<()> {
360 steps.borrow_mut().push(notice.map_or_else(|| "release".to_owned(), |text| format!("release {text}")));
361 if release_fails { Err(io::Error::other("no terminal")) } else { Ok(()) }
362 };
363 let mut take = || -> io::Result<()> {
364 steps.borrow_mut().push("take".to_owned());
365 Ok(())
366 };
367 let mut wait_for_key = || -> io::Result<()> {
368 steps.borrow_mut().push("key".to_owned());
369 Ok(())
370 };
371 let (deliveries, lines) = mpsc::channel();
372 let message = run(
373 handoff,
374 &mut HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key },
375 &deliveries,
376 );
377 let Heard::Started(outcome) = message else {
378 panic!("the handoff delivers its outcome first: {message:?}");
379 };
380 (outcome, steps.into_inner(), lines)
381 }
382
383 fn shell(script: &str) -> DetachedHandoff<Heard> {
384 DetachedHandoff::new("sh", Heard::Started).args(["-c", script]).on_line(Heard::Said)
385 }
386
387 fn next(lines: &Receiver<Delivery<Heard>>) -> ChildLine {
389 match lines.recv_timeout(PATIENCE) {
390 Ok(Delivery::Message(Heard::Said(line))) => line,
391 Ok(Delivery::Message(other)) => panic!("only lines follow the outcome: {other:?}"),
392 Ok(Delivery::Ended) => panic!("a child's lines are not background work that ends"),
393 Err(error) => panic!("no line arrived: {error}"),
394 }
395 }
396
397 #[test]
398 fn the_first_line_brings_the_screen_back_and_the_child_runs_on() {
399 let (outcome, steps, lines) = detach(shell("echo ready; echo more; cat").notice("Starting the helper"), false);
400 let DetachedOutcome::Detached { child, first_line } = outcome else {
401 panic!("the program said it was ready: {outcome:?}");
402 };
403 assert_eq!(first_line, "ready");
404 assert_eq!(steps, ["release Starting the helper", "take"], "the screen came back while the child runs");
405 assert_eq!(child.try_wait().expect("its state"), None, "the child is still running");
406 assert_eq!(next(&lines), ChildLine::Line("more".to_owned()), "a line right after the first is kept");
407 child.write_line("ping").expect("the child reads its input");
408 assert_eq!(next(&lines), ChildLine::Line("ping".to_owned()), "what the application wrote reached the child");
409 child.write_line("päckage ünïcode").expect("the child reads its input");
410 assert_eq!(next(&lines), ChildLine::Line("päckage ünïcode".to_owned()));
411 child.close_stdin();
412 assert_eq!(next(&lines), ChildLine::Ended { code: Some(0) }, "`cat` ended at the end of its input");
413 assert_eq!(child.try_wait().expect("its state"), Some(Some(0)));
414 let refused = child.write_line("late").expect_err("the input is closed");
415 assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);
416 }
417
418 #[test]
419 fn dropping_the_last_clone_closes_the_childs_input() {
420 let (outcome, _, lines) = detach(shell("echo ready; cat; echo bye"), false);
421 let DetachedOutcome::Detached { child, .. } = outcome else {
422 panic!("the program said it was ready: {outcome:?}");
423 };
424 let kept = child.clone();
425 drop(child);
426 kept.write_line("still open").expect("a clone keeps the input open");
427 assert_eq!(next(&lines), ChildLine::Line("still open".to_owned()));
428 drop(kept);
430 assert_eq!(next(&lines), ChildLine::Line("bye".to_owned()), "the child read the end of its input");
431 assert_eq!(next(&lines), ChildLine::Ended { code: Some(0) });
432 }
433
434 #[test]
435 fn a_program_that_ends_before_its_first_line_finishes_as_a_handoff_would() {
436 let (outcome, steps, _) = detach(shell("exit 126"), false);
437 assert_eq!(outcome, DetachedOutcome::Finished { code: Some(126) }, "pkexec's code for a refused password");
438 assert_eq!(steps, ["release", "take"]);
439 let (outcome, _, _) = detach(shell("echo refused >&2; exit 127"), false);
441 assert_eq!(outcome, DetachedOutcome::Finished { code: Some(127) });
442 let (outcome, _, _) = detach(shell("kill -TERM $$"), false);
443 assert_eq!(outcome, DetachedOutcome::Finished { code: None }, "a signal leaves no code");
444 }
445
446 #[test]
447 fn a_program_that_closes_its_output_is_waited_for() {
448 let (outcome, _, _) = detach(shell("exec >&-; sleep 0.2; exit 4"), false);
449 assert_eq!(outcome, DetachedOutcome::Finished { code: Some(4) }, "the end of the output is not the end");
450 }
451
452 #[test]
453 fn a_line_said_just_before_the_end_still_detaches_and_the_end_follows() {
454 let (outcome, _, lines) = detach(shell("echo ready; exit 5"), false);
455 let DetachedOutcome::Detached { first_line, .. } = outcome else {
456 panic!("the line came first: {outcome:?}");
457 };
458 assert_eq!(first_line, "ready");
459 assert_eq!(next(&lines), ChildLine::Ended { code: Some(5) });
460 }
461
462 #[test]
463 fn a_child_can_be_killed() {
464 let (outcome, _, lines) = detach(shell("echo ready; exec sleep 30"), false);
465 let DetachedOutcome::Detached { child, .. } = outcome else {
466 panic!("the program said it was ready: {outcome:?}");
467 };
468 assert!(child.id().is_some(), "a real child has a process id");
469 child.kill().expect("our own child may be killed");
470 assert_eq!(next(&lines), ChildLine::Ended { code: None }, "killed by a signal");
471 }
472
473 #[test]
474 fn pause_waits_for_a_key_only_when_the_program_ended_without_detaching() {
475 let (_, finished, _) = detach(shell("exit 1").pause(true), false);
476 assert_eq!(finished, ["release", "key", "take"], "the reason can be read before the screen comes back");
477 let (outcome, detached, _) = detach(shell("echo ready; cat").pause(true), false);
478 assert!(matches!(outcome, DetachedOutcome::Detached { .. }), "{outcome:?}");
479 assert_eq!(detached, ["release", "take"], "a program that is ready leaves nothing to read");
480 }
481
482 #[test]
483 fn an_unstartable_program_fails_and_the_screen_still_comes_back() {
484 let (outcome, steps, _) = detach(DetachedHandoff::new("quvyta-no-such-program", Heard::Started), false);
485 let DetachedOutcome::Failed(reason) = outcome else {
486 panic!("a program that is not there cannot have started: {outcome:?}");
487 };
488 assert!(!reason.is_empty(), "the reason names what went wrong");
489 assert_eq!(steps, ["release", "take"]);
490 }
491
492 #[test]
493 fn a_terminal_that_cannot_be_released_fails_without_running_the_program() {
494 let (outcome, steps, _) = detach(shell("echo ready; cat"), true);
495 assert_eq!(outcome, DetachedOutcome::Failed("no terminal".to_owned()));
496 assert_eq!(steps, ["release", "take"], "application mode is put back");
497 }
498
499 #[test]
500 fn arguments_the_directory_and_the_environment_reach_the_program() {
501 let handoff = DetachedHandoff::new("sh", Heard::Started)
502 .arg("-c")
503 .arg(r#"echo "$(pwd) $QUVYTA_DETACHED_TEST"; cat"#)
504 .dir("/")
505 .env("QUVYTA_DETACHED_TEST", "ok");
506 let request = handoff.request();
507 assert_eq!(request.program, std::ffi::OsString::from("sh"));
508 assert!(!request.pause);
509 let (outcome, _, _) = detach(handoff, false);
510 let DetachedOutcome::Detached { first_line, .. } = outcome else {
511 panic!("the program said it was ready: {outcome:?}");
512 };
513 assert_eq!(first_line, "/ ok");
514 }
515}