1#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::ffi::OsStr;
9use std::io::{ErrorKind, PipeReader, PipeWriter, Read, Write};
10use std::os::fd::AsFd;
11use std::os::unix::process::CommandExt;
12use std::panic::{self, AssertUnwindSafe, Location};
13use std::process::{Child, Command, ExitStatus, Stdio};
14use std::rc::{Rc, Weak};
15use std::sync::Once;
16use std::time::Instant;
17
18use calloop::timer::TimeoutAction;
19use calloop::{Interest, RegistrationToken};
20use crabshell_reactive::{ReadSignal, Signal, untrack};
21use crabshell_ui::Cx;
22use rustix::process::{Pid, PidfdFlags, Signal as Kill, kill_process_group, pidfd_open};
23
24use crate::lines::{Lines, decode, panic_message};
25use crate::{Shared, spawn};
26
27pub(crate) const MAX_OUTPUT: usize = 1024 * 1024;
30
31pub(crate) const MAX_INPUT: usize = 4 * 1024 * 1024;
34
35pub(crate) struct Processes {
37 shared: Rc<Shared>,
38 live: RefCell<Vec<Weak<Inner>>>,
40}
41
42impl Processes {
43 pub(crate) fn new(shared: Rc<Shared>) -> Processes {
44 Processes {
45 shared,
46 live: RefCell::default(),
47 }
48 }
49}
50
51pub(crate) fn stop_all(cx: &Cx) {
55 let Some(processes) = cx.service::<Processes>() else {
56 return;
57 };
58 let live: Vec<Rc<Inner>> = processes
59 .live
60 .borrow()
61 .iter()
62 .filter_map(Weak::upgrade)
63 .collect();
64 for inner in live {
65 inner.stop();
66 }
67}
68
69pub trait IntoCommand {
73 fn into_command(self) -> Option<Command>;
75}
76
77impl IntoCommand for Command {
78 fn into_command(self) -> Option<Command> {
79 Some(self)
80 }
81}
82
83impl<S: AsRef<OsStr>> IntoCommand for Vec<S> {
84 fn into_command(self) -> Option<Command> {
85 let mut words = self.into_iter();
86 let mut command = Command::new(words.next()?);
87 command.args(words);
88 Some(command)
89 }
90}
91
92impl<S: AsRef<OsStr>, const N: usize> IntoCommand for [S; N] {
93 fn into_command(self) -> Option<Command> {
94 Vec::from(self).into_command()
95 }
96}
97
98#[derive(Clone)]
107pub struct Process {
108 inner: Rc<Inner>,
109}
110
111struct Inner {
112 processes: Option<Rc<Shared>>,
114 running: Signal<bool>,
116 status: Signal<Option<ExitStatus>>,
117 stdout: Signal<String>,
118 stderr: Signal<String>,
119 restarts: Signal<u64>,
122 state: RefCell<State>,
123}
124
125#[derive(Default)]
126struct State {
127 pending: Option<Command>,
130 starter: Option<RegistrationToken>,
131 run: Option<Run>,
132 keep_stdin: bool,
133 input: Vec<u8>,
135 closing: bool,
138 ran: bool,
141 on_line: Option<OnLine>,
142 at: Option<(&'static str, &'static Location<'static>)>,
144 lines: Lines,
148 later: bool,
150}
151
152type OnLine = Rc<dyn Fn(&str)>;
154
155struct Run {
157 child: Child,
158 program: String,
159 exit: Option<RegistrationToken>,
160 out: Output,
161 err: Output,
162 stdin: Option<PipeWriter>,
163 outbox: Vec<u8>,
164 writer: Option<RegistrationToken>,
165}
166
167#[derive(Default)]
169struct Output {
170 stream: Option<(PipeReader, RegistrationToken)>,
171 partial: Vec<u8>,
173 kept: usize,
175 capped: bool,
176}
177
178#[derive(Clone, Copy, PartialEq)]
179enum Which {
180 Out,
181 Err,
182}
183
184#[track_caller]
186pub(crate) fn process<C: IntoCommand>(cx: &Cx, command: impl Fn() -> C + 'static) -> Process {
187 let at = Location::caller();
188 let service = cx.service::<Processes>();
189 let processes = service
190 .as_ref()
191 .map(|processes| Rc::clone(&processes.shared));
192 if processes.is_none() {
193 static LOGGED: Once = Once::new();
194 LOGGED.call_once(|| {
195 tracing::warn!("cx.process() needs the services the shell provides; nothing runs");
196 });
197 }
198 let inner = Rc::new(Inner {
199 processes,
200 running: cx.signal(true),
203 status: cx.signal(None),
204 stdout: cx.signal(String::new()),
205 stderr: cx.signal(String::new()),
206 restarts: cx.signal(0),
207 state: RefCell::default(),
208 });
209 cx.owner().signal(StopOnDrop(Rc::clone(&inner)));
211 if let Some(service) = service {
212 let mut live = service.live.borrow_mut();
213 live.retain(|inner| inner.strong_count() > 0);
214 live.push(Rc::downgrade(&inner));
215 }
216 let weak = Rc::downgrade(&inner);
217 let restarts = inner.restarts;
218 cx.effect(move || {
222 restarts.get();
223 let built = panic::catch_unwind(AssertUnwindSafe(|| command().into_command()));
224 let Some(inner) = weak.upgrade() else {
225 return;
226 };
227 match built {
228 Ok(next) => untrack(|| inner.start(next)),
229 Err(payload) => {
230 let message = panic_message(payload.as_ref());
231 tracing::error!("cx.process (called at {at}) panicked building its command: {message}");
232 untrack(|| inner.cancel());
233 }
234 }
235 });
236 Process { inner }
237}
238
239pub(crate) fn exec_detached(cx: &Cx, command: impl IntoCommand) {
241 let Some(processes) = cx.service::<Processes>() else {
242 tracing::warn!("cx.exec_detached() needs the services the shell provides; nothing runs");
243 return;
244 };
245 let Some(mut command) = command.into_command() else {
246 return tracing::warn!("cx.exec_detached() got an empty command; nothing runs");
247 };
248 let _ = spawn::spawn(&processes.shared, &mut command);
250}
251
252struct StopOnDrop(Rc<Inner>);
254
255impl Drop for StopOnDrop {
256 fn drop(&mut self) {
257 self.0.stop();
258 }
259}
260
261impl Process {
262 pub fn running(&self) -> ReadSignal<bool> {
265 self.inner.running.read_only()
266 }
267
268 pub fn exit_status(&self) -> ReadSignal<Option<ExitStatus>> {
271 self.inner.status.read_only()
272 }
273
274 pub fn stdout(&self) -> ReadSignal<String> {
278 self.inner.stdout.read_only()
279 }
280
281 pub fn stderr(&self) -> ReadSignal<String> {
283 self.inner.stderr.read_only()
284 }
285
286 #[track_caller]
294 pub fn on_line(self, on_line: impl Fn(&str) + 'static) -> Process {
295 self.handle("on_line", "\n", Rc::new(on_line))
296 }
297
298 #[track_caller]
307 pub fn on_split(self, marker: &str, on_split: impl Fn(&str) + 'static) -> Process {
308 self.handle("on_split", marker, Rc::new(on_split))
309 }
310
311 #[track_caller]
312 fn handle(self, name: &'static str, marker: &str, handler: OnLine) -> Process {
313 let mut state = self.inner.state.borrow_mut();
314 state.on_line = Some(handler);
315 state.at = Some((name, Location::caller()));
316 state.lines.set_marker(marker);
317 let waiting = state.lines.waiting();
318 drop(state);
319 if waiting {
320 self.inner.deliver_later();
321 }
322 self
323 }
324
325 pub fn with_stdin(self) -> Process {
330 self.inner.state.borrow_mut().keep_stdin = true;
331 self
332 }
333
334 pub fn write(&self, data: impl AsRef<[u8]>) {
340 self.inner.write(data.as_ref());
341 }
342
343 pub fn close_stdin(&self) {
346 {
347 let mut state = self.inner.state.borrow_mut();
348 if state.run.is_none() && state.ran {
350 return;
351 }
352 state.closing = true;
353 }
354 self.inner.flush();
355 }
356
357 pub fn kill(&self) {
361 if let Some(run) = &self.inner.state.borrow().run {
362 return terminate(&run.child);
363 }
364 self.inner.cancel();
365 }
366
367 pub fn restart(&self) {
370 self.inner.restarts.try_update(|count| *count += 1);
371 }
372}
373
374fn terminate(child: &Child) {
376 if let Err(error) = kill_process_group(Pid::from_child(child), Kill::TERM) {
378 tracing::debug!(%error, "couldn't stop a program; it may have ended");
379 }
380}
381
382impl Inner {
383 fn shared(&self) -> Option<&Rc<Shared>> {
384 self.processes.as_ref()
385 }
386
387 fn event_loop(&self) -> Option<Box<dyn crate::EventLoop>> {
388 self.shared()?.event_loop()
389 }
390
391 fn start(self: &Rc<Self>, command: Option<Command>) {
394 self.stop_run();
395 let Some(command) = command else {
396 tracing::warn!("cx.process() got an empty command; nothing runs");
397 set(self.running, false);
398 return;
399 };
400 let Some(event_loop) = self.event_loop() else {
401 return;
402 };
403 let mut state = self.state.borrow_mut();
404 state.pending = Some(command);
405 state.ran = false;
406 if state.starter.is_none() {
407 let inner = Rc::clone(self);
408 state.starter = event_loop.insert_timer(
409 Instant::now(),
410 Box::new(move || {
411 inner.spawn_pending();
412 TimeoutAction::Drop
413 }),
414 );
415 }
416 drop(state);
417 set(self.running, true);
418 set(self.status, None);
419 set(self.stdout, String::new());
420 set(self.stderr, String::new());
421 }
422
423 fn spawn_pending(self: &Rc<Self>) {
424 let Some(event_loop) = self.event_loop() else {
425 return;
426 };
427 let (command, keep_stdin, input) = {
428 let mut state = self.state.borrow_mut();
429 state.starter = None;
430 let input = std::mem::take(&mut state.input);
431 (state.pending.take(), state.keep_stdin, input)
432 };
433 let Some(mut command) = command else {
434 return;
435 };
436 let program = command.get_program().to_string_lossy().into_owned();
437 let spawned = spawn_piped(&mut command, keep_stdin);
438 drop(command);
441 let (child, out, err, stdin) = match spawned {
442 Ok(spawned) => spawned,
443 Err(error) => {
444 tracing::warn!(program, %error, "couldn't start the program");
445 let mut state = self.state.borrow_mut();
446 state.ran = true;
447 state.closing = false;
448 drop(state);
449 set(self.running, false);
450 return;
451 }
452 };
453 let mut run = Run {
454 child,
455 program,
456 exit: None,
457 out: Output::default(),
458 err: Output::default(),
459 stdin,
460 outbox: input,
461 writer: None,
462 };
463 self.state.borrow_mut().lines.reset();
464 for (which, stream) in [(Which::Out, out), (Which::Err, err)] {
465 let Ok(fd) = stream.as_fd().try_clone_to_owned() else {
466 continue;
467 };
468 let inner = Rc::clone(self);
469 let read = Box::new(move || inner.on_output(which));
470 if let Some(token) = event_loop.insert_fd(fd, Interest::READ, read) {
471 run.output(which).stream = Some((stream, token));
472 }
473 }
474 match pidfd_open(Pid::from_child(&run.child), PidfdFlags::NONBLOCK) {
475 Ok(fd) => {
476 let inner = Rc::clone(self);
477 let ended = Box::new(move || inner.on_exit());
478 run.exit = event_loop.insert_fd(fd, Interest::READ, ended);
479 }
480 Err(error) => tracing::warn!(%error, "couldn't follow a program to its end"),
481 }
482 self.state.borrow_mut().run = Some(run);
483 self.flush();
485 }
486
487 fn on_output(&self, which: Which) {
489 let mut chunk = [0; 64 * 1024];
490 let mut state = self.state.borrow_mut();
491 let State { run, lines, .. } = &mut *state;
492 let Some(run) = run.as_mut() else {
493 return;
494 };
495 let Some((stream, _)) = run.output(which).stream.as_mut() else {
496 return;
497 };
498 let read = match stream.read(&mut chunk) {
499 Ok(read) => read,
500 Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
501 return;
502 }
503 Err(_) => 0,
504 };
505 let text = run.take(which, &chunk[..read]);
506 if which == Which::Out {
507 lines.append(&chunk[..read]);
508 }
509 let program = run.program.clone();
510 if read == 0 {
511 let token = run.output(which).stream.take().map(|(_, token)| token);
513 if let (Some(token), Some(event_loop)) = (token, self.event_loop()) {
514 event_loop.remove(token);
515 }
516 }
517 drop(state);
518 self.show(which, text);
519 self.deliver(false, &program);
520 }
521
522 fn on_exit(&self) {
525 let mut state = self.state.borrow_mut();
526 let Some(run) = state.run.as_mut() else {
527 return;
528 };
529 let status = match run.child.try_wait() {
530 Ok(Some(status)) => Some(status),
531 Ok(None) => return,
532 Err(error) => {
534 tracing::warn!(%error, program = run.program, "lost track of the program");
535 None
536 }
537 };
538 let mut run = state.run.take().expect("invariant: matched just above");
539 state.closing = false;
541 state.ran = true;
542 drop(state);
543 let mut chunk = [0; 64 * 1024];
548 for which in [Which::Out, Which::Err] {
549 let mut budget = MAX_OUTPUT;
550 while let Some((stream, _)) = run.output(which).stream.as_mut()
551 && budget > 0
552 {
553 let Ok(read @ 1..) = stream.read(&mut chunk) else {
554 break;
555 };
556 budget = budget.saturating_sub(read);
557 let text = run.take(which, &chunk[..read]);
558 if which == Which::Out {
559 self.state.borrow_mut().lines.append(&chunk[..read]);
560 }
561 self.show(which, text);
562 self.deliver(false, &run.program);
563 }
564 let text = run.take(which, &[]);
565 self.show(which, text);
566 }
567 self.deliver(true, &run.program);
568 self.remove_sources(&mut run);
569 set(self.status, status);
570 set(self.running, false);
571 }
572
573 fn show(&self, which: Which, text: String) {
575 if text.is_empty() {
576 return;
577 }
578 let signal = match which {
579 Which::Out => self.stdout,
580 Which::Err => self.stderr,
581 };
582 signal.try_update(|all| all.push_str(&text));
583 }
584
585 fn deliver_later(self: &Rc<Self>) {
588 let Some(event_loop) = self.event_loop() else {
589 return;
590 };
591 if std::mem::replace(&mut self.state.borrow_mut().later, true) {
592 return;
593 }
594 let inner = Rc::clone(self);
595 let later = move || {
596 inner.state.borrow_mut().later = false;
597 let program = inner
598 .state
599 .borrow()
600 .run
601 .as_ref()
602 .map(|run| run.program.clone());
603 if let Some(program) = program {
604 inner.deliver(false, &program);
605 }
606 TimeoutAction::Drop
607 };
608 event_loop.insert_timer(Instant::now(), Box::new(later));
609 }
610
611 fn deliver(&self, done: bool, program: &str) {
615 loop {
616 let (line, on_line, at) = {
617 let mut state = self.state.borrow_mut();
618 let line = match state.lines.next(&program) {
619 Some(line) => line,
620 None if done => match state.lines.rest(&program) {
621 Some(line) => line,
622 None => return,
623 },
624 None => return,
625 };
626 (line, state.on_line.clone(), state.at)
627 };
628 let Some(on_line) = on_line else {
629 continue;
630 };
631 if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| on_line(&line))) {
632 let (name, at) = at.map_or(("on_line", String::new()), |(name, at)| {
633 (name, format!(" (called at {at})"))
634 });
635 let message = panic_message(payload.as_ref());
636 tracing::error!("process {program} {name}{at} panicked: {message}");
637 let mut state = self.state.borrow_mut();
638 if state
640 .on_line
641 .as_ref()
642 .is_some_and(|now| Rc::ptr_eq(now, &on_line))
643 {
644 state.on_line = None;
645 }
646 }
647 }
648 }
649
650 fn write(self: &Rc<Self>, data: &[u8]) {
651 let mut state = self.state.borrow_mut();
652 if state.closing {
653 return tracing::warn!("the program's input is closed, so a write was dropped");
654 }
655 let waiting = state.keep_stdin && state.run.is_none() && !state.ran;
657 match state.run.as_mut() {
658 Some(run) if run.stdin.is_some() => run.outbox.extend_from_slice(data),
659 _ if waiting => {
660 if state.input.len() + data.len() > MAX_INPUT {
661 return tracing::warn!("over 4 MiB written before the program started; dropped");
662 }
663 return state.input.extend_from_slice(data);
664 }
665 _ => return tracing::warn!("the program takes no input now, so a write was dropped"),
666 }
667 drop(state);
668 self.flush();
669 }
670
671 fn flush(self: &Rc<Self>) {
674 let Some(event_loop) = self.event_loop() else {
675 return;
676 };
677 let mut state = self.state.borrow_mut();
678 let closing = state.closing;
679 let Some(run) = state.run.as_mut() else {
680 return;
681 };
682 let Some(stdin) = run.stdin.as_mut() else {
683 return;
684 };
685 let mut broken = false;
686 while !run.outbox.is_empty() {
687 match stdin.write(&run.outbox) {
688 Ok(written) => {
689 run.outbox.drain(..written);
690 }
691 Err(error) if error.kind() == ErrorKind::Interrupted => {}
692 Err(error) if error.kind() == ErrorKind::WouldBlock => break,
693 Err(_) => {
694 broken = true;
695 break;
696 }
697 }
698 }
699 if run.outbox.len() > MAX_INPUT {
700 let program = &run.program;
701 tracing::warn!(
702 program,
703 "the program reads nothing it is sent; closing its input"
704 );
705 broken = true;
706 }
707 if broken {
708 drop(state);
709 return self.drop_stdin();
710 }
711 if run.outbox.is_empty() {
712 if let Some(writer) = run.writer.take() {
713 event_loop.remove(writer);
714 }
715 if closing {
716 run.stdin = None;
718 }
719 } else if run.writer.is_none()
720 && let Ok(fd) = stdin.as_fd().try_clone_to_owned()
721 {
722 let inner = Rc::clone(self);
723 run.writer = event_loop.insert_fd(fd, Interest::WRITE, Box::new(move || inner.flush()));
724 }
725 }
726
727 fn drop_stdin(&self) {
729 let writer = {
730 let mut state = self.state.borrow_mut();
731 state.input.clear();
732 let Some(run) = state.run.as_mut() else {
733 return;
734 };
735 run.stdin = None;
736 run.outbox.clear();
737 run.writer.take()
738 };
739 if let (Some(writer), Some(event_loop)) = (writer, self.event_loop()) {
740 event_loop.remove(writer);
741 }
742 }
743
744 fn stop_run(&self) {
747 let run = {
748 let mut state = self.state.borrow_mut();
749 let run = state.run.take();
750 if run.is_some() {
752 state.closing = false;
753 }
754 run
755 };
756 let Some(mut run) = run else {
757 return;
758 };
759 self.state.borrow_mut().lines.reset();
761 terminate(&run.child);
762 self.remove_sources(&mut run);
763 if let Some(shared) = &self.processes {
764 spawn::reap(shared, run.child, run.program);
765 }
766 }
767
768 fn stop(&self) {
770 let (starter, on_line) = {
771 let mut state = self.state.borrow_mut();
772 state.pending = None;
773 (state.starter.take(), state.on_line.take())
774 };
775 drop(on_line);
778 if let (Some(starter), Some(event_loop)) = (starter, self.event_loop()) {
779 event_loop.remove(starter);
780 }
781 self.stop_run();
782 }
783
784 fn cancel(&self) {
787 let starter = {
788 let mut state = self.state.borrow_mut();
789 state.pending = None;
790 state.ran = true;
791 state.closing = false;
792 state.starter.take()
793 };
794 if let (Some(starter), Some(event_loop)) = (starter, self.event_loop()) {
795 event_loop.remove(starter);
796 }
797 self.stop_run();
798 let ran = self.state.borrow().run.is_none();
799 if ran {
800 set(self.running, false);
801 }
802 }
803
804 fn remove_sources(&self, run: &mut Run) {
805 let tokens = [
806 run.exit.take(),
807 run.writer.take(),
808 run.out.stream.take().map(|(_, token)| token),
809 run.err.stream.take().map(|(_, token)| token),
810 ];
811 run.stdin = None;
812 if let Some(event_loop) = self.event_loop() {
813 for token in tokens.into_iter().flatten() {
814 event_loop.remove(token);
815 }
816 }
817 }
818}
819
820#[allow(
824 clippy::type_complexity,
825 reason = "the four parts of one spawn, used once"
826)]
827fn spawn_piped(
828 command: &mut Command,
829 keep_stdin: bool,
830) -> std::io::Result<(Child, PipeReader, PipeReader, Option<PipeWriter>)> {
831 let (out, child_out) = std::io::pipe()?;
832 let (err, child_err) = std::io::pipe()?;
833 rustix::io::ioctl_fionbio(&out, true)?;
834 rustix::io::ioctl_fionbio(&err, true)?;
835 let (stdin, child_in) = if keep_stdin {
836 let (theirs, ours) = std::io::pipe()?;
837 rustix::io::ioctl_fionbio(&ours, true)?;
838 (Some(ours), Stdio::from(theirs))
839 } else {
840 (None, Stdio::null())
841 };
842 spawn::untune(command);
843 let child = command
844 .stdout(child_out)
845 .stderr(child_err)
846 .stdin(child_in)
847 .process_group(0)
848 .spawn()?;
849 Ok((child, out, err, stdin))
850}
851
852impl Run {
853 fn output(&mut self, which: Which) -> &mut Output {
854 match which {
855 Which::Out => &mut self.out,
856 Which::Err => &mut self.err,
857 }
858 }
859
860 fn take(&mut self, which: Which, bytes: &[u8]) -> String {
863 let program = self.program.clone();
864 let output = self.output(which);
865 output.partial.extend_from_slice(bytes);
866 let mut text = if bytes.is_empty() {
867 String::from_utf8_lossy(&std::mem::take(&mut output.partial)).into_owned()
868 } else {
869 decode(&mut output.partial)
870 };
871 if output.capped {
873 text.clear();
874 }
875 let room = MAX_OUTPUT.saturating_sub(output.kept);
876 if text.len() > room {
877 if !std::mem::replace(&mut output.capped, true) {
878 tracing::warn!(program, "a program printed over 1 MiB; the rest isn't kept");
879 }
880 let mut end = room;
881 while !text.is_char_boundary(end) {
882 end -= 1;
883 }
884 text.truncate(end);
885 }
886 output.kept += text.len();
887 text
888 }
889}
890
891fn set<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
894 if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
895 signal.try_set(value);
896 }
897}