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, 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 Location<'static>>,
144}
145
146type OnLine = Rc<dyn Fn(&str)>;
148
149struct Run {
151 child: Child,
152 program: String,
153 exit: Option<RegistrationToken>,
154 out: Output,
155 err: Output,
156 stdin: Option<PipeWriter>,
157 outbox: Vec<u8>,
158 writer: Option<RegistrationToken>,
159 lines: Lines,
160}
161
162#[derive(Default)]
164struct Output {
165 stream: Option<(PipeReader, RegistrationToken)>,
166 partial: Vec<u8>,
168 kept: usize,
170 capped: bool,
171}
172
173#[derive(Clone, Copy, PartialEq)]
174enum Which {
175 Out,
176 Err,
177}
178
179#[track_caller]
181pub(crate) fn process<C: IntoCommand>(cx: &Cx, command: impl Fn() -> C + 'static) -> Process {
182 let at = Location::caller();
183 let service = cx.service::<Processes>();
184 let processes = service
185 .as_ref()
186 .map(|processes| Rc::clone(&processes.shared));
187 if processes.is_none() {
188 static LOGGED: Once = Once::new();
189 LOGGED.call_once(|| {
190 tracing::warn!("cx.process() needs the services the shell provides; nothing runs");
191 });
192 }
193 let inner = Rc::new(Inner {
194 processes,
195 running: cx.signal(true),
198 status: cx.signal(None),
199 stdout: cx.signal(String::new()),
200 stderr: cx.signal(String::new()),
201 restarts: cx.signal(0),
202 state: RefCell::default(),
203 });
204 cx.owner().signal(StopOnDrop(Rc::clone(&inner)));
206 if let Some(service) = service {
207 let mut live = service.live.borrow_mut();
208 live.retain(|inner| inner.strong_count() > 0);
209 live.push(Rc::downgrade(&inner));
210 }
211 let weak = Rc::downgrade(&inner);
212 let restarts = inner.restarts;
213 cx.effect(move || {
217 restarts.get();
218 let built = panic::catch_unwind(AssertUnwindSafe(|| command().into_command()));
219 let Some(inner) = weak.upgrade() else {
220 return;
221 };
222 match built {
223 Ok(next) => untrack(|| inner.start(next)),
224 Err(payload) => {
225 let message = panic_message(payload.as_ref());
226 tracing::error!("cx.process (called at {at}) panicked building its command: {message}");
227 untrack(|| inner.cancel());
228 }
229 }
230 });
231 Process { inner }
232}
233
234pub(crate) fn exec_detached(cx: &Cx, command: impl IntoCommand) {
236 let Some(processes) = cx.service::<Processes>() else {
237 tracing::warn!("cx.exec_detached() needs the services the shell provides; nothing runs");
238 return;
239 };
240 let Some(mut command) = command.into_command() else {
241 return tracing::warn!("cx.exec_detached() got an empty command; nothing runs");
242 };
243 let _ = spawn::spawn(&processes.shared, &mut command);
245}
246
247struct StopOnDrop(Rc<Inner>);
249
250impl Drop for StopOnDrop {
251 fn drop(&mut self) {
252 self.0.stop();
253 }
254}
255
256impl Process {
257 pub fn running(&self) -> ReadSignal<bool> {
260 self.inner.running.read_only()
261 }
262
263 pub fn exit_status(&self) -> ReadSignal<Option<ExitStatus>> {
266 self.inner.status.read_only()
267 }
268
269 pub fn stdout(&self) -> ReadSignal<String> {
273 self.inner.stdout.read_only()
274 }
275
276 pub fn stderr(&self) -> ReadSignal<String> {
278 self.inner.stderr.read_only()
279 }
280
281 #[track_caller]
289 pub fn on_line(self, on_line: impl Fn(&str) + 'static) -> Process {
290 let mut state = self.inner.state.borrow_mut();
291 state.on_line = Some(Rc::new(on_line));
292 state.at = Some(Location::caller());
293 drop(state);
294 self
295 }
296
297 pub fn with_stdin(self) -> Process {
302 self.inner.state.borrow_mut().keep_stdin = true;
303 self
304 }
305
306 pub fn write(&self, data: impl AsRef<[u8]>) {
312 self.inner.write(data.as_ref());
313 }
314
315 pub fn close_stdin(&self) {
318 {
319 let mut state = self.inner.state.borrow_mut();
320 if state.run.is_none() && state.ran {
322 return;
323 }
324 state.closing = true;
325 }
326 self.inner.flush();
327 }
328
329 pub fn kill(&self) {
333 if let Some(run) = &self.inner.state.borrow().run {
334 return terminate(&run.child);
335 }
336 self.inner.cancel();
337 }
338
339 pub fn restart(&self) {
342 self.inner.restarts.try_update(|count| *count += 1);
343 }
344}
345
346fn terminate(child: &Child) {
348 if let Err(error) = kill_process_group(Pid::from_child(child), Kill::TERM) {
350 tracing::debug!(%error, "couldn't stop a program; it may have ended");
351 }
352}
353
354impl Inner {
355 fn shared(&self) -> Option<&Rc<Shared>> {
356 self.processes.as_ref()
357 }
358
359 fn event_loop(&self) -> Option<Box<dyn crate::EventLoop>> {
360 self.shared()?.event_loop()
361 }
362
363 fn start(self: &Rc<Self>, command: Option<Command>) {
366 self.stop_run();
367 let Some(command) = command else {
368 tracing::warn!("cx.process() got an empty command; nothing runs");
369 set(self.running, false);
370 return;
371 };
372 let Some(event_loop) = self.event_loop() else {
373 return;
374 };
375 let mut state = self.state.borrow_mut();
376 state.pending = Some(command);
377 state.ran = false;
378 if state.starter.is_none() {
379 let inner = Rc::clone(self);
380 state.starter = event_loop.insert_timer(
381 Instant::now(),
382 Box::new(move || {
383 inner.spawn_pending();
384 TimeoutAction::Drop
385 }),
386 );
387 }
388 drop(state);
389 set(self.running, true);
390 set(self.status, None);
391 set(self.stdout, String::new());
392 set(self.stderr, String::new());
393 }
394
395 fn spawn_pending(self: &Rc<Self>) {
396 let Some(event_loop) = self.event_loop() else {
397 return;
398 };
399 let (command, keep_stdin, input) = {
400 let mut state = self.state.borrow_mut();
401 state.starter = None;
402 let input = std::mem::take(&mut state.input);
403 (state.pending.take(), state.keep_stdin, input)
404 };
405 let Some(mut command) = command else {
406 return;
407 };
408 let program = command.get_program().to_string_lossy().into_owned();
409 let spawned = spawn_piped(&mut command, keep_stdin);
410 drop(command);
413 let (child, out, err, stdin) = match spawned {
414 Ok(spawned) => spawned,
415 Err(error) => {
416 tracing::warn!(program, %error, "couldn't start the program");
417 let mut state = self.state.borrow_mut();
418 state.ran = true;
419 state.closing = false;
420 drop(state);
421 set(self.running, false);
422 return;
423 }
424 };
425 let mut run = Run {
426 child,
427 program,
428 exit: None,
429 out: Output::default(),
430 err: Output::default(),
431 stdin,
432 outbox: input,
433 writer: None,
434 lines: Lines::default(),
435 };
436 for (which, stream) in [(Which::Out, out), (Which::Err, err)] {
437 let Ok(fd) = stream.as_fd().try_clone_to_owned() else {
438 continue;
439 };
440 let inner = Rc::clone(self);
441 let read = Box::new(move || inner.on_output(which));
442 if let Some(token) = event_loop.insert_fd(fd, Interest::READ, read) {
443 run.output(which).stream = Some((stream, token));
444 }
445 }
446 match pidfd_open(Pid::from_child(&run.child), PidfdFlags::NONBLOCK) {
447 Ok(fd) => {
448 let inner = Rc::clone(self);
449 let ended = Box::new(move || inner.on_exit());
450 run.exit = event_loop.insert_fd(fd, Interest::READ, ended);
451 }
452 Err(error) => tracing::warn!(%error, "couldn't follow a program to its end"),
453 }
454 self.state.borrow_mut().run = Some(run);
455 self.flush();
457 }
458
459 fn on_output(&self, which: Which) {
461 let mut chunk = [0; 64 * 1024];
462 let mut state = self.state.borrow_mut();
463 let Some(run) = state.run.as_mut() else {
464 return;
465 };
466 let Some((stream, _)) = run.output(which).stream.as_mut() else {
467 return;
468 };
469 let read = match stream.read(&mut chunk) {
470 Ok(read) => read,
471 Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
472 return;
473 }
474 Err(_) => 0,
475 };
476 let (text, lines) = run.take(which, &chunk[..read]);
477 let program = run.program.clone();
478 if read == 0 {
479 let token = run.output(which).stream.take().map(|(_, token)| token);
481 if let (Some(token), Some(event_loop)) = (token, self.event_loop()) {
482 event_loop.remove(token);
483 }
484 }
485 drop(state);
486 self.show(which, text);
487 self.deliver(lines, &program);
488 }
489
490 fn on_exit(&self) {
493 let mut state = self.state.borrow_mut();
494 let Some(run) = state.run.as_mut() else {
495 return;
496 };
497 let status = match run.child.try_wait() {
498 Ok(Some(status)) => Some(status),
499 Ok(None) => return,
500 Err(error) => {
502 tracing::warn!(%error, program = run.program, "lost track of the program");
503 None
504 }
505 };
506 let mut run = state.run.take().expect("invariant: matched just above");
507 state.closing = false;
509 state.ran = true;
510 drop(state);
511 let mut chunk = [0; 64 * 1024];
516 for which in [Which::Out, Which::Err] {
517 let mut budget = MAX_OUTPUT;
518 while let Some((stream, _)) = run.output(which).stream.as_mut()
519 && budget > 0
520 {
521 let Ok(read @ 1..) = stream.read(&mut chunk) else {
522 break;
523 };
524 budget = budget.saturating_sub(read);
525 let (text, lines) = run.take(which, &chunk[..read]);
526 self.show(which, text);
527 self.deliver(lines, &run.program);
528 }
529 let (text, lines) = run.take(which, &[]);
530 self.show(which, text);
531 self.deliver(lines, &run.program);
532 }
533 let last = run.lines.finish();
534 self.deliver(last.into_iter().collect(), &run.program);
535 self.remove_sources(&mut run);
536 set(self.status, status);
537 set(self.running, false);
538 }
539
540 fn show(&self, which: Which, text: String) {
542 if text.is_empty() {
543 return;
544 }
545 let signal = match which {
546 Which::Out => self.stdout,
547 Which::Err => self.stderr,
548 };
549 signal.try_update(|all| all.push_str(&text));
550 }
551
552 fn deliver(&self, lines: Vec<String>, program: &str) {
554 if lines.is_empty() {
555 return;
556 }
557 let (on_line, at) = {
558 let state = self.state.borrow();
559 (state.on_line.clone(), state.at)
560 };
561 let Some(on_line) = on_line else {
562 return;
563 };
564 for line in lines {
565 let current = self.state.borrow().on_line.clone();
567 if !current.is_some_and(|now| Rc::ptr_eq(&now, &on_line)) {
568 return;
569 }
570 if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| on_line(&line))) {
571 let at = at.map_or_else(String::new, |at| format!(" (called at {at})"));
572 let message = panic_message(payload.as_ref());
573 tracing::error!("process {program} on_line{at} panicked: {message}");
574 let mut state = self.state.borrow_mut();
575 if state
577 .on_line
578 .as_ref()
579 .is_some_and(|now| Rc::ptr_eq(now, &on_line))
580 {
581 state.on_line = None;
582 }
583 return;
584 }
585 }
586 }
587
588 fn write(self: &Rc<Self>, data: &[u8]) {
589 let mut state = self.state.borrow_mut();
590 if state.closing {
591 return tracing::warn!("the program's input is closed, so a write was dropped");
592 }
593 let waiting = state.keep_stdin && state.run.is_none() && !state.ran;
595 match state.run.as_mut() {
596 Some(run) if run.stdin.is_some() => run.outbox.extend_from_slice(data),
597 _ if waiting => {
598 if state.input.len() + data.len() > MAX_INPUT {
599 return tracing::warn!("over 4 MiB written before the program started; dropped");
600 }
601 return state.input.extend_from_slice(data);
602 }
603 _ => return tracing::warn!("the program takes no input now, so a write was dropped"),
604 }
605 drop(state);
606 self.flush();
607 }
608
609 fn flush(self: &Rc<Self>) {
612 let Some(event_loop) = self.event_loop() else {
613 return;
614 };
615 let mut state = self.state.borrow_mut();
616 let closing = state.closing;
617 let Some(run) = state.run.as_mut() else {
618 return;
619 };
620 let Some(stdin) = run.stdin.as_mut() else {
621 return;
622 };
623 let mut broken = false;
624 while !run.outbox.is_empty() {
625 match stdin.write(&run.outbox) {
626 Ok(written) => {
627 run.outbox.drain(..written);
628 }
629 Err(error) if error.kind() == ErrorKind::Interrupted => {}
630 Err(error) if error.kind() == ErrorKind::WouldBlock => break,
631 Err(_) => {
632 broken = true;
633 break;
634 }
635 }
636 }
637 if run.outbox.len() > MAX_INPUT {
638 let program = &run.program;
639 tracing::warn!(
640 program,
641 "the program reads nothing it is sent; closing its input"
642 );
643 broken = true;
644 }
645 if broken {
646 drop(state);
647 return self.drop_stdin();
648 }
649 if run.outbox.is_empty() {
650 if let Some(writer) = run.writer.take() {
651 event_loop.remove(writer);
652 }
653 if closing {
654 run.stdin = None;
656 }
657 } else if run.writer.is_none()
658 && let Ok(fd) = stdin.as_fd().try_clone_to_owned()
659 {
660 let inner = Rc::clone(self);
661 run.writer = event_loop.insert_fd(fd, Interest::WRITE, Box::new(move || inner.flush()));
662 }
663 }
664
665 fn drop_stdin(&self) {
667 let writer = {
668 let mut state = self.state.borrow_mut();
669 state.input.clear();
670 let Some(run) = state.run.as_mut() else {
671 return;
672 };
673 run.stdin = None;
674 run.outbox.clear();
675 run.writer.take()
676 };
677 if let (Some(writer), Some(event_loop)) = (writer, self.event_loop()) {
678 event_loop.remove(writer);
679 }
680 }
681
682 fn stop_run(&self) {
685 let run = {
686 let mut state = self.state.borrow_mut();
687 let run = state.run.take();
688 if run.is_some() {
690 state.closing = false;
691 }
692 run
693 };
694 let Some(mut run) = run else {
695 return;
696 };
697 terminate(&run.child);
698 self.remove_sources(&mut run);
699 if let Some(shared) = &self.processes {
700 spawn::reap(shared, run.child, run.program);
701 }
702 }
703
704 fn stop(&self) {
706 let (starter, on_line) = {
707 let mut state = self.state.borrow_mut();
708 state.pending = None;
709 (state.starter.take(), state.on_line.take())
710 };
711 drop(on_line);
714 if let (Some(starter), Some(event_loop)) = (starter, self.event_loop()) {
715 event_loop.remove(starter);
716 }
717 self.stop_run();
718 }
719
720 fn cancel(&self) {
723 let starter = {
724 let mut state = self.state.borrow_mut();
725 state.pending = None;
726 state.ran = true;
727 state.closing = false;
728 state.starter.take()
729 };
730 if let (Some(starter), Some(event_loop)) = (starter, self.event_loop()) {
731 event_loop.remove(starter);
732 }
733 self.stop_run();
734 let ran = self.state.borrow().run.is_none();
735 if ran {
736 set(self.running, false);
737 }
738 }
739
740 fn remove_sources(&self, run: &mut Run) {
741 let tokens = [
742 run.exit.take(),
743 run.writer.take(),
744 run.out.stream.take().map(|(_, token)| token),
745 run.err.stream.take().map(|(_, token)| token),
746 ];
747 run.stdin = None;
748 if let Some(event_loop) = self.event_loop() {
749 for token in tokens.into_iter().flatten() {
750 event_loop.remove(token);
751 }
752 }
753 }
754}
755
756#[allow(
760 clippy::type_complexity,
761 reason = "the four parts of one spawn, used once"
762)]
763fn spawn_piped(
764 command: &mut Command,
765 keep_stdin: bool,
766) -> std::io::Result<(Child, PipeReader, PipeReader, Option<PipeWriter>)> {
767 let (out, child_out) = std::io::pipe()?;
768 let (err, child_err) = std::io::pipe()?;
769 rustix::io::ioctl_fionbio(&out, true)?;
770 rustix::io::ioctl_fionbio(&err, true)?;
771 let (stdin, child_in) = if keep_stdin {
772 let (theirs, ours) = std::io::pipe()?;
773 rustix::io::ioctl_fionbio(&ours, true)?;
774 (Some(ours), Stdio::from(theirs))
775 } else {
776 (None, Stdio::null())
777 };
778 spawn::untune(command);
779 let child = command
780 .stdout(child_out)
781 .stderr(child_err)
782 .stdin(child_in)
783 .process_group(0)
784 .spawn()?;
785 Ok((child, out, err, stdin))
786}
787
788impl Run {
789 fn output(&mut self, which: Which) -> &mut Output {
790 match which {
791 Which::Out => &mut self.out,
792 Which::Err => &mut self.err,
793 }
794 }
795
796 fn take(&mut self, which: Which, bytes: &[u8]) -> (String, Vec<String>) {
800 let lines = if which == Which::Out && !bytes.is_empty() {
801 self.lines.push(bytes, &self.program)
802 } else {
803 Vec::new()
804 };
805 let program = self.program.clone();
806 let output = self.output(which);
807 output.partial.extend_from_slice(bytes);
808 let mut text = if bytes.is_empty() {
809 String::from_utf8_lossy(&std::mem::take(&mut output.partial)).into_owned()
810 } else {
811 decode(&mut output.partial)
812 };
813 if output.capped {
815 text.clear();
816 }
817 let room = MAX_OUTPUT.saturating_sub(output.kept);
818 if text.len() > room {
819 if !std::mem::replace(&mut output.capped, true) {
820 tracing::warn!(program, "a program printed over 1 MiB; the rest isn't kept");
821 }
822 let mut end = room;
823 while !text.is_char_boundary(end) {
824 end -= 1;
825 }
826 text.truncate(end);
827 }
828 output.kept += text.len();
829 (text, lines)
830 }
831}
832
833fn decode(bytes: &mut Vec<u8>) -> String {
836 let mut text = String::new();
837 let mut rest: &[u8] = bytes;
838 loop {
839 match std::str::from_utf8(rest) {
840 Ok(valid) => {
841 text.push_str(valid);
842 rest = &[];
843 break;
844 }
845 Err(error) => {
846 let (valid, after) = rest.split_at(error.valid_up_to());
847 text.push_str(&String::from_utf8_lossy(valid));
848 match error.error_len() {
849 Some(bad) => {
850 text.push('\u{fffd}');
851 rest = &after[bad..];
852 }
853 None => {
854 rest = after;
855 break;
856 }
857 }
858 }
859 }
860 }
861 *bytes = rest.to_vec();
862 text
863}
864
865fn set<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
868 if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
869 signal.try_set(value);
870 }
871}