1mod failure;
15mod streaming;
16
17pub(crate) use failure::{failure, stop_after_failure, terminate_runner};
18pub use failure::{LeaderTermination, RunnerFailure, RunnerPhase};
19pub use streaming::{run_streaming, StreamOptions, StreamOutput};
20
21use crate::{CapturedOutput, NativeOutputCapture, OwnedCommand};
22use std::{
23 ffi::OsStr,
24 io,
25 process::{ChildStdin, ExitStatus},
26 sync::{
27 atomic::{AtomicBool, Ordering},
28 mpsc, Arc,
29 },
30 thread::JoinHandle,
31 time::{Duration, Instant},
32};
33
34const DRAIN_WAIT: Duration = Duration::from_secs(2);
38pub(crate) const RUNNER_REAP_WAIT: Duration = Duration::from_secs(5);
39
40const STDIN_WAIT: Duration = Duration::from_millis(500);
44
45const STDIN_CANCEL_WAIT: Duration = Duration::from_secs(2);
49
50const CANCEL_RETRY: Duration = Duration::from_millis(25);
52
53#[cfg(windows)]
56const WRITE_CHUNK: usize = 64 * 1024;
57
58#[cfg(unix)]
60const WRITE_POLL: Duration = Duration::from_millis(50);
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct RunOptions {
65 pub timeout: Duration,
67 pub stdout_cap: usize,
69 pub stderr_cap: usize,
71 pub no_window: bool,
73}
74
75impl RunOptions {
76 pub fn new(timeout: Duration) -> Self {
78 Self {
79 timeout,
80 stdout_cap: 1024 * 1024,
81 stderr_cap: 1024 * 1024,
82 no_window: true,
83 }
84 }
85
86 pub fn stdout_cap(mut self, bytes: usize) -> Self {
87 self.stdout_cap = bytes;
88 self
89 }
90
91 pub fn stderr_cap(mut self, bytes: usize) -> Self {
92 self.stderr_cap = bytes;
93 self
94 }
95
96 pub fn no_window(mut self, hidden: bool) -> Self {
97 self.no_window = hidden;
98 self
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
104#[non_exhaustive]
105pub struct CaptureOptions {
106 pub timeout: Option<Duration>,
108 pub stdout_cap: usize,
110 pub stderr_cap: usize,
112 pub no_window: bool,
114 pub drain_grace: Duration,
116}
117
118impl Default for CaptureOptions {
119 fn default() -> Self {
120 Self {
121 timeout: None,
122 stdout_cap: 1024 * 1024,
123 stderr_cap: 1024 * 1024,
124 no_window: true,
125 drain_grace: DRAIN_WAIT,
126 }
127 }
128}
129
130impl CaptureOptions {
131 pub fn new() -> Self {
132 Self::default()
133 }
134 pub fn timeout(mut self, value: Duration) -> Self {
135 self.timeout = Some(value);
136 self
137 }
138 pub fn stdout_cap(mut self, value: usize) -> Self {
139 self.stdout_cap = value;
140 self
141 }
142 pub fn stderr_cap(mut self, value: usize) -> Self {
143 self.stderr_cap = value;
144 self
145 }
146 pub fn no_window(mut self, value: bool) -> Self {
147 self.no_window = value;
148 self
149 }
150 pub fn drain_grace(mut self, value: Duration) -> Self {
151 self.drain_grace = value;
152 self
153 }
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
158pub struct RunOutput {
159 pub status: ExitStatus,
162 pub timed_out: bool,
164 pub stdout: CapturedOutput,
165 pub stderr: CapturedOutput,
166}
167
168pub fn run_with_stdin<I, S>(
191 program: impl AsRef<OsStr>,
192 args: I,
193 input: &[u8],
194 options: &RunOptions,
195) -> io::Result<RunOutput>
196where
197 I: IntoIterator<Item = S>,
198 S: AsRef<OsStr>,
199{
200 run_inner(program, args, Some(input.to_vec()), options)
201}
202
203pub fn run<I, S>(program: impl AsRef<OsStr>, args: I, options: &RunOptions) -> io::Result<RunOutput>
210where
211 I: IntoIterator<Item = S>,
212 S: AsRef<OsStr>,
213{
214 run_inner(program, args, None, options)
215}
216
217fn run_inner<I, S>(
218 program: impl AsRef<OsStr>,
219 args: I,
220 input: Option<Vec<u8>>,
221 options: &RunOptions,
222) -> io::Result<RunOutput>
223where
224 I: IntoIterator<Item = S>,
225 S: AsRef<OsStr>,
226{
227 let mut command = OwnedCommand::new(program);
228 command.command_mut().args(args);
229 run_owned(command, input, options)
230}
231
232pub fn run_command(
267 command: OwnedCommand,
268 input: Option<&[u8]>,
269 options: &RunOptions,
270) -> io::Result<RunOutput> {
271 run_owned(command, input.map(<[u8]>::to_vec), options)
272}
273
274fn run_owned(
275 command: OwnedCommand,
276 input: Option<Vec<u8>>,
277 options: &RunOptions,
278) -> io::Result<RunOutput> {
279 run_captured(
280 command,
281 input,
282 &CaptureOptions {
283 timeout: Some(options.timeout),
284 stdout_cap: options.stdout_cap,
285 stderr_cap: options.stderr_cap,
286 no_window: options.no_window,
287 drain_grace: DRAIN_WAIT,
288 },
289 )
290}
291
292pub fn run_command_capture(
296 command: OwnedCommand,
297 input: Option<&[u8]>,
298 options: &CaptureOptions,
299) -> io::Result<RunOutput> {
300 run_captured(command, input.map(<[u8]>::to_vec), options)
301}
302
303fn run_captured(
304 mut command: OwnedCommand,
305 input: Option<Vec<u8>>,
306 options: &CaptureOptions,
307) -> io::Result<RunOutput> {
308 command.pipe_stdio(input.is_some());
311 command.bound_runner_cleanup(RUNNER_REAP_WAIT);
312 if options.no_window {
313 command.windows_hide();
314 } else {
315 command.windows_visible();
316 }
317 let mut child = command
318 .spawn()
319 .map_err(|error| failure(RunnerPhase::Spawn, LeaderTermination::NotStarted, error))?;
320 let writer = match (input, child.take_stdin()) {
324 (Some(bytes), Some(stdin)) => Some(
325 StdinWriter::spawn(stdin, bytes)
326 .map_err(|error| stop_after_failure(&mut child, RunnerPhase::Setup, error))?,
327 ),
328 _ => None,
329 };
330 let mut stdout = child
331 .take_stdout()
332 .map(|pipe| NativeOutputCapture::stdout(pipe, options.stdout_cap))
333 .transpose()
334 .map_err(|error| stop_after_failure(&mut child, RunnerPhase::Setup, error))?;
335 let mut stderr = child
336 .take_stderr()
337 .map(|pipe| NativeOutputCapture::stderr(pipe, options.stderr_cap))
338 .transpose()
339 .map_err(|error| stop_after_failure(&mut child, RunnerPhase::Setup, error))?;
340 let (status, timed_out) = match options.timeout {
343 Some(timeout) => match child
344 .wait_exit_then_sweep(timeout)
345 .map_err(|error| stop_after_failure(&mut child, RunnerPhase::Wait, error))?
346 {
347 Some(status) => (status, false),
348 None => (terminate_runner(&mut child, RUNNER_REAP_WAIT)?, true),
349 },
350 None => loop {
351 if let Some(status) = child
352 .wait_exit_then_sweep(Duration::from_millis(100))
353 .map_err(|error| stop_after_failure(&mut child, RunnerPhase::Wait, error))?
354 {
355 break (status, false);
356 }
357 },
358 };
359 let stdout_output = stdout
360 .as_mut()
361 .map_or_else(CapturedOutput::default, |capture| {
362 capture.finish_within(options.drain_grace)
363 });
364 let stderr_output = stderr
365 .as_mut()
366 .map_or_else(CapturedOutput::default, |capture| {
367 capture.finish_within(options.drain_grace)
368 });
369 let writer_result = writer.map(StdinWriter::finish).transpose();
374 let stdout_failure = capture_failure("stdout", stdout.as_ref());
377 let stderr_failure = capture_failure("stderr", stderr.as_ref());
378 stdout_failure?;
379 stderr_failure?;
380 writer_result
381 .map_err(|error| failure(RunnerPhase::Stdin, LeaderTermination::Confirmed, error))?;
382 Ok(RunOutput {
383 status,
384 timed_out,
385 stdout: stdout_output,
386 stderr: stderr_output,
387 })
388}
389
390pub(crate) fn capture_failure(
395 stream: &str,
396 capture: Option<&NativeOutputCapture>,
397) -> io::Result<()> {
398 match capture.and_then(NativeOutputCapture::take_error) {
399 Some(error) => Err(failure(
400 if capture.is_some_and(NativeOutputCapture::cleanup_failed) { RunnerPhase::Cleanup }
401 else if stream == "stdout" { RunnerPhase::Stdout } else { RunnerPhase::Stderr },
402 LeaderTermination::Confirmed,
403 error,
404 )),
405 None if capture.is_some_and(|capture| !capture.is_complete()) => Err(failure(
406 RunnerPhase::Drain, LeaderTermination::Confirmed,
407 io::Error::new(io::ErrorKind::TimedOut,
408 format!("{stream} capture did not reach EOF within its drain bound, output is incomplete")),
409 )),
410 None => Ok(()),
411 }
412}
413
414struct WriterState {
416 cancel: AtomicBool,
417 #[cfg(windows)]
421 handle: std::sync::Mutex<Option<usize>>,
422 #[cfg(unix)]
427 pipe: std::sync::Mutex<Option<ChildStdin>>,
428}
429
430impl WriterState {
431 fn new() -> Self {
432 Self {
433 cancel: AtomicBool::new(false),
434 #[cfg(windows)]
435 handle: std::sync::Mutex::new(None),
436 #[cfg(unix)]
437 pipe: std::sync::Mutex::new(None),
438 }
439 }
440
441 #[cfg(unix)]
442 fn pipe(&self) -> std::sync::MutexGuard<'_, Option<ChildStdin>> {
443 self.pipe
444 .lock()
445 .unwrap_or_else(std::sync::PoisonError::into_inner)
446 }
447
448 #[cfg(unix)]
450 fn close_pipe(&self) {
451 drop(self.pipe().take());
452 }
453
454 fn cancelled(&self) -> bool {
455 self.cancel.load(Ordering::Acquire)
456 }
457
458 fn cancel(&self) {
461 self.cancel.store(true, Ordering::Release);
462 #[cfg(windows)]
463 {
464 let held = self
465 .handle
466 .lock()
467 .unwrap_or_else(std::sync::PoisonError::into_inner);
468 if let Some(raw) = *held {
469 unsafe { crate::native_capture::platform::cancel_all_io(raw as _) };
473 }
474 }
475 }
476}
477
478#[cfg(test)]
481static LIVE_WRITERS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
482
483#[cfg(test)]
484struct LiveWriter;
485
486#[cfg(test)]
487impl LiveWriter {
488 fn start() -> Self {
489 LIVE_WRITERS.fetch_add(1, Ordering::SeqCst);
490 Self
491 }
492}
493
494#[cfg(test)]
495impl Drop for LiveWriter {
496 fn drop(&mut self) {
497 LIVE_WRITERS.fetch_sub(1, Ordering::SeqCst);
498 }
499}
500
501struct StdinWriter {
506 state: Arc<WriterState>,
507 done: mpsc::Receiver<io::Result<()>>,
508 handle: Option<JoinHandle<()>>,
509}
510
511impl StdinWriter {
512 fn spawn(stdin: ChildStdin, bytes: Vec<u8>) -> io::Result<Self> {
513 let state = Arc::new(WriterState::new());
514 let (sender, done) = mpsc::channel();
515 let shared = Arc::clone(&state);
516 #[cfg(unix)]
517 {
518 *state.pipe() = Some(stdin);
519 }
520 #[cfg(test)]
521 let live = LiveWriter::start();
522 let handle = std::thread::Builder::new()
523 .name("rightkit-run-stdin".into())
524 .spawn(move || {
525 #[cfg(test)]
526 let _live = live;
527 #[cfg(unix)]
530 let result = {
531 let result = write_input(&bytes, &shared);
532 shared.close_pipe();
533 result
534 };
535 #[cfg(windows)]
536 let result = {
537 let mut stdin = stdin;
538 let result = write_input(&mut stdin, &bytes, &shared);
539 drop(stdin);
540 result
541 };
542 let _ = sender.send(result);
543 })?;
544 Ok(Self {
545 state,
546 done,
547 handle: Some(handle),
548 })
549 }
550
551 fn finish(mut self) -> io::Result<()> {
557 let result = match self.done.recv_timeout(STDIN_WAIT) {
558 Err(mpsc::RecvTimeoutError::Timeout) => self.cancel_and_wait(),
559 other => other,
560 };
561 self.settle(result)
562 }
563
564 fn cancel_and_wait(&self) -> Result<io::Result<()>, mpsc::RecvTimeoutError> {
566 let deadline = Instant::now() + STDIN_CANCEL_WAIT;
567 loop {
568 self.state.cancel();
569 match self.done.recv_timeout(CANCEL_RETRY) {
570 Err(mpsc::RecvTimeoutError::Timeout) if Instant::now() < deadline => {}
571 other => return other,
572 }
573 }
574 }
575
576 fn settle(&mut self, result: Result<io::Result<()>, mpsc::RecvTimeoutError>) -> io::Result<()> {
577 let handle = self.handle.take();
578 match result {
579 Ok(result) => {
580 if let Some(handle) = handle {
581 let _ = handle.join();
582 }
583 match result {
584 Err(error) if error.kind() != io::ErrorKind::BrokenPipe => Err(error),
585 _ => Ok(()),
586 }
587 }
588 Err(mpsc::RecvTimeoutError::Timeout) => {
589 #[cfg(unix)]
596 self.state.close_pipe();
597 Err(failure(
598 RunnerPhase::Cleanup,
599 LeaderTermination::Confirmed,
600 io::Error::new(
601 io::ErrorKind::TimedOut,
602 "the stdin writer did not stop within its cancellation bound",
603 ),
604 ))
605 }
606 Err(mpsc::RecvTimeoutError::Disconnected) => {
607 if let Some(handle) = handle {
608 let _ = handle.join();
609 }
610 Err(failure(
611 RunnerPhase::Cleanup,
612 LeaderTermination::Confirmed,
613 io::Error::other("the stdin writer thread panicked"),
614 ))
615 }
616 }
617 }
618}
619
620impl Drop for StdinWriter {
621 fn drop(&mut self) {
622 if self.handle.is_some() {
627 let result = self.cancel_and_wait();
628 let _ = self.settle(result);
629 }
630 }
631}
632
633#[cfg(unix)]
638fn write_input(bytes: &[u8], state: &WriterState) -> io::Result<()> {
639 use std::os::fd::AsRawFd;
640 let fd = {
641 let guard = state.pipe();
642 let Some(pipe) = guard.as_ref() else {
643 return Ok(());
644 };
645 let fd = pipe.as_raw_fd();
646 unsafe {
649 let flags = libc::fcntl(fd, libc::F_GETFL);
650 if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
651 return Err(io::Error::last_os_error());
652 }
653 }
654 fd
655 };
656 let mut written = 0;
657 while written < bytes.len() {
658 if state.cancelled() {
659 return Ok(());
660 }
661 let rest = &bytes[written..];
662 let chunk = rest.len().min(1 << 20);
663 let outcome = {
666 let guard = state.pipe();
667 let Some(pipe) = guard.as_ref() else {
668 return Ok(());
670 };
671 let count = unsafe { libc::write(pipe.as_raw_fd(), rest.as_ptr().cast(), chunk) };
674 if count >= 0 {
675 Ok(count as usize)
676 } else {
677 Err(io::Error::last_os_error())
678 }
679 };
680 match outcome {
681 Ok(count) => written += count,
682 Err(error) => match error.kind() {
683 io::ErrorKind::Interrupted => {}
684 io::ErrorKind::WouldBlock => {
685 let mut entry = libc::pollfd {
686 fd,
687 events: libc::POLLOUT,
688 revents: 0,
689 };
690 unsafe { libc::poll(&mut entry, 1, WRITE_POLL.as_millis() as libc::c_int) };
693 }
694 _ => return Err(error),
695 },
696 }
697 }
698 Ok(())
699}
700
701#[cfg(windows)]
705fn write_input(stdin: &mut ChildStdin, bytes: &[u8], state: &WriterState) -> io::Result<()> {
706 use std::{io::Write, os::windows::io::AsRawHandle};
707 let publish = |value: Option<usize>| {
708 *state
709 .handle
710 .lock()
711 .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
712 };
713 publish(Some(stdin.as_raw_handle() as usize));
714 let mut result = Ok(());
715 for chunk in bytes.chunks(WRITE_CHUNK) {
716 if state.cancelled() {
717 break;
718 }
719 if let Err(error) = stdin.write_all(chunk) {
720 result = Err(error);
721 break;
722 }
723 }
724 publish(None);
726 match result {
727 Err(_) if state.cancelled() => Ok(()),
728 other => other,
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use std::process::Stdio;
736 use std::sync::atomic::Ordering::SeqCst;
737
738 static WRITER_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
741
742 fn serial() -> std::sync::MutexGuard<'static, ()> {
743 WRITER_TESTS
744 .lock()
745 .unwrap_or_else(std::sync::PoisonError::into_inner)
746 }
747
748 struct KillOnDrop(std::process::Child);
750
751 impl Drop for KillOnDrop {
752 fn drop(&mut self) {
753 let _ = self.0.kill();
754 let _ = self.0.wait();
755 }
756 }
757
758 #[test]
759 fn defaults_hide_the_window_and_cap_both_streams() {
760 let options = RunOptions::new(Duration::from_secs(1));
761 assert!(options.no_window);
762 assert_eq!(options.stdout_cap, 1024 * 1024);
763 assert_eq!(options.stderr_cap, 1024 * 1024);
764 let options = options.stdout_cap(4).stderr_cap(8).no_window(false);
765 assert_eq!((options.stdout_cap, options.stderr_cap), (4, 8));
766 assert!(!options.no_window);
767 }
768
769 #[cfg(unix)]
770 #[test]
771 fn a_quick_program_returns_its_output_and_status() {
772 let options = RunOptions::new(Duration::from_secs(10));
773 let output = run("sh", ["-c", "printf hi; exit 3"], &options).unwrap();
774 assert!(!output.timed_out);
775 assert_eq!(output.status.code(), Some(3));
776 assert_eq!(output.stdout.bytes, b"hi".to_vec());
777 }
778
779 #[cfg(unix)]
780 #[test]
781 fn a_program_running_within_the_deadline_is_not_signalled() {
782 let options = RunOptions::new(Duration::from_secs(10));
783 let output = run("sh", ["-c", "sleep 1; printf done"], &options).unwrap();
784 assert!(!output.timed_out);
785 assert!(output.status.success());
786 assert_eq!(output.stdout.bytes, b"done".to_vec());
787 }
788
789 #[cfg(unix)]
790 #[test]
791 fn a_slow_program_is_terminated_at_the_deadline() {
792 let options = RunOptions::new(Duration::from_millis(200));
793 let output = run("sh", ["-c", "sleep 30"], &options).unwrap();
794 assert!(output.timed_out);
795 }
796
797 #[cfg(unix)]
798 #[test]
799 fn stdin_input_reaches_the_child_and_early_exit_is_not_an_error() {
800 let _serial = serial();
801 let options = RunOptions::new(Duration::from_secs(10));
802 let output = run_with_stdin("sh", ["-c", "cat"], b"hello", &options).unwrap();
803 assert!(output.status.success());
804 assert_eq!(output.stdout.bytes, b"hello".to_vec());
805 let big = vec![b'x'; 1 << 20];
808 let output = run_with_stdin("sh", ["-c", "exit 7"], &big, &options).unwrap();
809 assert_eq!(output.status.code(), Some(7));
810 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
811 }
812
813 #[test]
817 fn a_reader_that_never_reads_cannot_pin_the_writer() {
818 let _serial = serial();
819 #[cfg(unix)]
820 let mut command = {
821 let mut command = std::process::Command::new("sleep");
822 command.arg("30");
823 command
824 };
825 #[cfg(windows)]
826 let mut command = {
827 let mut command = std::process::Command::new("ping");
828 command.args(["-n", "30", "127.0.0.1"]);
829 command
830 };
831 let mut child = KillOnDrop(
832 command
833 .stdin(Stdio::piped())
834 .stdout(Stdio::null())
835 .stderr(Stdio::null())
836 .spawn()
837 .unwrap(),
838 );
839 let stdin = child.0.stdin.take().unwrap();
840 let writer = StdinWriter::spawn(stdin, vec![b'x'; 2 << 20]).unwrap();
841 std::thread::sleep(Duration::from_millis(200));
843 assert_eq!(LIVE_WRITERS.load(SeqCst), 1, "the writer should be blocked");
844 let started = std::time::Instant::now();
845 writer.finish().unwrap();
846 let elapsed = started.elapsed();
847 assert_eq!(
848 LIVE_WRITERS.load(SeqCst),
849 0,
850 "the writer thread must be gone"
851 );
852 assert!(
853 elapsed < STDIN_WAIT + STDIN_CANCEL_WAIT + Duration::from_secs(3),
854 "finish took {elapsed:?}"
855 );
856 assert!(
857 child.0.try_wait().unwrap().is_none(),
858 "the reader must have stayed alive throughout"
859 );
860 }
861
862 #[cfg(unix)]
867 #[test]
868 fn a_writer_that_never_reports_times_out_and_the_parent_closes_the_write_end() {
869 let _serial = serial();
870 let mut child = KillOnDrop(
871 std::process::Command::new("cat")
872 .stdin(Stdio::piped())
873 .stdout(Stdio::null())
874 .stderr(Stdio::null())
875 .spawn()
876 .unwrap(),
877 );
878 let state = Arc::new(WriterState::new());
879 *state.pipe() = Some(child.0.stdin.take().unwrap());
880 let (_sender, done) = mpsc::channel::<io::Result<()>>();
882 let (release, parked) = mpsc::channel::<()>();
883 let stuck = std::thread::spawn(move || {
884 let _ = parked.recv();
885 });
886 let writer = StdinWriter {
887 state: Arc::clone(&state),
888 done,
889 handle: Some(stuck),
890 };
891 let started = Instant::now();
892 let error = writer.finish().unwrap_err();
893 let elapsed = started.elapsed();
894 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
895 assert!(
896 elapsed >= STDIN_WAIT + STDIN_CANCEL_WAIT
897 && elapsed < STDIN_WAIT + STDIN_CANCEL_WAIT + Duration::from_secs(3),
898 "finish took {elapsed:?}"
899 );
900 assert!(state.cancelled(), "the cancel flag must stay set");
901 assert!(
902 state.pipe().is_none(),
903 "the parent must have closed the write end"
904 );
905 let deadline = Instant::now() + Duration::from_secs(5);
907 let status = loop {
908 if let Some(status) = child.0.try_wait().unwrap() {
909 break status;
910 }
911 assert!(Instant::now() < deadline, "the reader never saw EOF");
912 std::thread::sleep(Duration::from_millis(20));
913 };
914 assert!(status.success());
915 let _ = release.send(());
916 }
917
918 #[cfg(unix)]
921 struct Grandchild(std::path::PathBuf);
922
923 #[cfg(unix)]
924 impl Grandchild {
925 fn new(label: &str) -> Self {
926 let nanos = std::time::SystemTime::now()
927 .duration_since(std::time::UNIX_EPOCH)
928 .map_or(0, |elapsed| elapsed.as_nanos());
929 Self(std::env::temp_dir().join(format!(
930 "rk-run-stdin-{label}-{}-{nanos}.pid",
931 std::process::id()
932 )))
933 }
934
935 fn pid(&self) -> Option<i32> {
936 let pid: i32 = std::fs::read_to_string(&self.0).ok()?.trim().parse().ok()?;
937 (pid > 1).then_some(pid)
938 }
939
940 fn count_path(&self) -> std::path::PathBuf {
942 let mut path = self.0.clone().into_os_string();
943 path.push(".count");
944 path.into()
945 }
946
947 fn alive(&self) -> bool {
948 self.pid()
950 .is_some_and(|pid| unsafe { libc::kill(pid, 0) } == 0)
951 }
952 }
953
954 #[cfg(unix)]
955 impl Drop for Grandchild {
956 fn drop(&mut self) {
957 if let Some(pid) = self.pid() {
958 unsafe { libc::kill(pid, libc::SIGKILL) };
960 }
961 let _ = std::fs::remove_file(&self.0);
962 let _ = std::fs::remove_file(self.count_path());
963 }
964 }
965
966 #[cfg(unix)]
972 fn holder_script(body: &str, tail: &str) -> String {
973 format!(
974 "if command -v setsid >/dev/null 2>&1; then \
975 setsid sh -c 'echo $$ > \"$1\"; {body}' sh \"$1\" <&0 >/dev/null 2>&1 & \
976 elif command -v perl >/dev/null 2>&1; then \
977 perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' sh -c 'echo $$ > \"$1\"; {body}' sh \"$1\" <&0 >/dev/null 2>&1 & \
978 else exit 99; fi; \
979 i=0; while [ ! -s \"$1\" ] && [ $i -lt 100 ]; do sleep 0.05; i=$((i+1)); done; {tail}"
980 )
981 }
982
983 #[cfg(unix)]
984 #[test]
985 fn a_session_escaped_stdin_holder_does_not_hold_run_past_its_exit() {
986 let _serial = serial();
987 let holder = Grandchild::new("exit");
988 let script = holder_script("sleep 2; wc -c > \"$1.count\"; exec sleep 30", "exit 0");
992 let big = vec![b'x'; 2 << 20];
993 let options = RunOptions::new(Duration::from_secs(10));
994 let started = std::time::Instant::now();
995 let output = run_with_stdin(
996 "sh",
997 ["-c", &script, "sh", holder.0.to_str().unwrap()],
998 &big,
999 &options,
1000 )
1001 .unwrap();
1002 let elapsed = started.elapsed();
1003 if output.status.code() == Some(99) {
1004 eprintln!("skipped: no setsid or perl to leave the session");
1005 return;
1006 }
1007 assert!(
1008 holder.alive(),
1009 "the fixture's grandchild must still hold stdin"
1010 );
1011 assert!(!output.timed_out);
1012 assert!(output.status.success(), "{:?}", output.status);
1013 assert!(elapsed < Duration::from_secs(6), "run took {elapsed:?}");
1015 assert_eq!(
1016 LIVE_WRITERS.load(SeqCst),
1017 0,
1018 "the writer thread must be gone"
1019 );
1020 let deadline = std::time::Instant::now() + Duration::from_secs(10);
1021 let received = loop {
1022 if let Some(count) = std::fs::read_to_string(holder.count_path())
1023 .ok()
1024 .and_then(|text| text.trim().parse::<usize>().ok())
1025 {
1026 break count;
1027 }
1028 assert!(
1029 std::time::Instant::now() < deadline,
1030 "the grandchild never reached EOF: the write end is still open"
1031 );
1032 std::thread::sleep(Duration::from_millis(50));
1033 };
1034 assert!(
1035 received < big.len() / 2,
1036 "{received} bytes reached the grandchild after run returned"
1037 );
1038 }
1039
1040 #[cfg(unix)]
1041 #[test]
1042 fn a_session_escaped_stdin_holder_does_not_hold_run_past_its_timeout() {
1043 let _serial = serial();
1044 let holder = Grandchild::new("timeout");
1045 let script = holder_script("exec sleep 30", "sleep 30");
1047 let big = vec![b'x'; 2 << 20];
1048 let timeout = Duration::from_secs(1);
1049 let options = RunOptions::new(timeout);
1050 let started = std::time::Instant::now();
1051 let output = run_with_stdin(
1052 "sh",
1053 ["-c", &script, "sh", holder.0.to_str().unwrap()],
1054 &big,
1055 &options,
1056 )
1057 .unwrap();
1058 let elapsed = started.elapsed();
1059 if !holder.alive() && !output.timed_out {
1060 eprintln!("skipped: no setsid or perl to leave the session");
1061 return;
1062 }
1063 assert!(
1064 holder.alive(),
1065 "the fixture's grandchild must still hold stdin"
1066 );
1067 assert!(output.timed_out);
1068 assert!(
1069 elapsed < timeout + Duration::from_secs(6),
1070 "run took {elapsed:?}"
1071 );
1072 assert_eq!(
1073 LIVE_WRITERS.load(SeqCst),
1074 0,
1075 "the writer thread must be gone"
1076 );
1077 }
1078
1079 #[cfg(unix)]
1080 #[test]
1081 fn output_past_the_cap_is_truncated_not_blocking() {
1082 let options = RunOptions::new(Duration::from_secs(10)).stdout_cap(4);
1083 let output = run("sh", ["-c", "head -c 100000 /dev/zero"], &options).unwrap();
1084 assert_eq!(output.stdout.bytes.len(), 4);
1085 assert!(output.stdout.truncated);
1086 }
1087
1088 #[cfg(unix)]
1090 fn sh(script: &str, args: &[&str]) -> OwnedCommand {
1091 let mut command = OwnedCommand::new("sh");
1092 command.command_mut().args(["-c", script, "sh"]).args(args);
1093 command
1094 }
1095
1096 #[cfg(unix)]
1098 fn sleeper() -> OwnedCommand {
1099 let mut command = OwnedCommand::new("sleep");
1100 command.command_mut().arg("30");
1101 command
1102 }
1103
1104 #[cfg(windows)]
1105 fn sleeper() -> OwnedCommand {
1106 let mut command = OwnedCommand::new("ping");
1107 command.command_mut().args(["-n", "30", "127.0.0.1"]);
1108 command
1109 }
1110
1111 #[test]
1112 fn refused_reap_returns_bounded_cleanup_failure() {
1113 let mut child = sleeper().spawn().unwrap();
1114 crate::owned::cleanup_test_fault::arm();
1115 let started = Instant::now();
1116 let error = terminate_runner(&mut child, Duration::from_millis(20)).unwrap_err();
1117 assert!(started.elapsed() < Duration::from_secs(2));
1118 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
1119 let detail = RunnerFailure::from_io(&error).unwrap();
1120 assert_eq!(detail.phase(), RunnerPhase::Cleanup);
1121 assert_eq!(detail.termination(), LeaderTermination::Unconfirmed);
1122 }
1123
1124 fn stream_outcome(
1127 command: OwnedCommand,
1128 options: &StreamOptions,
1129 cancel_on_first_chunk: bool,
1130 ) -> (io::Result<StreamOutput>, Vec<u8>, Vec<String>) {
1131 let flag = std::cell::Cell::new(false);
1132 let mut chunks = Vec::new();
1133 let mut lines = Vec::new();
1134 let output = run_streaming(
1135 command,
1136 options,
1137 &|| flag.get(),
1138 &mut |chunk: &[u8]| {
1139 chunks.extend_from_slice(chunk);
1140 if cancel_on_first_chunk {
1141 flag.set(true);
1142 }
1143 },
1144 &mut |line: &str| lines.push(line.to_owned()),
1145 );
1146 (output, chunks, lines)
1147 }
1148
1149 fn stream(
1151 command: OwnedCommand,
1152 options: &StreamOptions,
1153 cancel_on_first_chunk: bool,
1154 ) -> (StreamOutput, Vec<u8>, Vec<String>) {
1155 let (output, chunks, lines) = stream_outcome(command, options, cancel_on_first_chunk);
1156 (
1157 output.expect("successful streaming fixture must reach EOF"),
1158 chunks,
1159 lines,
1160 )
1161 }
1162
1163 #[cfg(windows)]
1164 fn assert_incomplete_drain(error: &io::Error) {
1165 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
1166 let detail = RunnerFailure::from_io(error).expect("runner failure metadata");
1167 assert_eq!(detail.phase(), RunnerPhase::Drain);
1168 assert_eq!(detail.termination(), LeaderTermination::Confirmed);
1169 }
1170
1171 #[test]
1172 fn run_command_passes_a_caller_set_env_var_to_the_child() {
1173 #[cfg(unix)]
1174 let mut command = sh("printf %s \"$RK_RUN_COMMAND_VAR\"", &[]);
1175 #[cfg(windows)]
1176 let mut command = {
1177 let mut command = OwnedCommand::new("cmd");
1178 command
1179 .command_mut()
1180 .args(["/C", "echo %RK_RUN_COMMAND_VAR%"]);
1181 command
1182 };
1183 command
1184 .command_mut()
1185 .env("RK_RUN_COMMAND_VAR", "from-the-caller");
1186 let output = run_command(command, None, &RunOptions::new(Duration::from_secs(20))).unwrap();
1187 assert!(output.status.success());
1188 assert!(!output.timed_out);
1189 assert!(
1190 String::from_utf8_lossy(&output.stdout.bytes).contains("from-the-caller"),
1191 "{:?}",
1192 output.stdout
1193 );
1194 }
1195
1196 #[cfg(unix)]
1197 #[test]
1198 fn run_command_with_input_reaches_the_child() {
1199 let _serial = serial();
1200 let options = RunOptions::new(Duration::from_secs(10));
1201 let output = run_command(sh("cat", &[]), Some(b"hello"), &options).unwrap();
1202 assert!(output.status.success());
1203 assert_eq!(output.stdout.bytes, b"hello".to_vec());
1204 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1205 }
1206
1207 #[cfg(unix)]
1208 #[test]
1209 fn a_session_escaped_stdin_holder_does_not_hold_run_command_past_its_timeout() {
1210 let _serial = serial();
1211 let holder = Grandchild::new("command-timeout");
1212 let script = holder_script("exec sleep 30", "sleep 30");
1213 let big = vec![b'x'; 2 << 20];
1214 let timeout = Duration::from_secs(1);
1215 let options = RunOptions::new(timeout);
1216 let started = std::time::Instant::now();
1217 let output = run_command(
1218 sh(script.as_str(), &[holder.0.to_str().unwrap()]),
1219 Some(&big),
1220 &options,
1221 )
1222 .unwrap();
1223 let elapsed = started.elapsed();
1224 if !holder.alive() && !output.timed_out {
1225 eprintln!("skipped: no setsid or perl to leave the session");
1226 return;
1227 }
1228 assert!(
1229 holder.alive(),
1230 "the fixture's grandchild must still hold stdin"
1231 );
1232 assert!(output.timed_out);
1233 assert!(
1234 elapsed < timeout + Duration::from_secs(6),
1235 "run_command took {elapsed:?}"
1236 );
1237 assert_eq!(
1238 LIVE_WRITERS.load(SeqCst),
1239 0,
1240 "the writer thread must be gone"
1241 );
1242 }
1243
1244 #[cfg(unix)]
1245 #[test]
1246 fn run_streaming_forwards_stdout_chunks_in_order_including_cr() {
1247 let command = sh(
1248 "printf 'a\\r'; sleep 0.1; printf 'b\\r'; sleep 0.1; printf 'c\\n'",
1249 &[],
1250 );
1251 let (output, chunks, lines) = stream(command, &StreamOptions::new(), false);
1252 assert!(output.status.success());
1253 assert!(!output.cancelled && !output.timed_out);
1254 assert_eq!(chunks, b"a\rb\rc\n".to_vec());
1255 assert_eq!(output.stdout_tail, b"a\rb\rc\n".to_vec());
1256 assert!(lines.is_empty());
1257 assert_eq!(output.stderr_tail, "");
1258 }
1259
1260 #[cfg(unix)]
1261 #[test]
1262 fn run_streaming_splits_stderr_lines_on_cr_and_lf() {
1263 let command = sh("printf 'x\\ry\\nz\\r\\nw' >&2; exit 3", &[]);
1264 let (output, chunks, lines) = stream(command, &StreamOptions::new(), false);
1265 assert_eq!(output.status.code(), Some(3));
1266 assert!(chunks.is_empty());
1267 assert_eq!(lines, ["x", "y", "z", "w"]);
1268 assert_eq!(output.stderr_tail, "x\ny\nz\nw");
1269 }
1270
1271 #[cfg(unix)]
1272 #[test]
1273 fn run_streaming_tails_keep_the_last_bytes() {
1274 let command = sh(
1275 "head -c 100000 /dev/zero | tr '\\0' 'x'; printf END; \
1276 printf 'l1\\nl2\\nl3\\nl4\\n' >&2",
1277 &[],
1278 );
1279 let options = StreamOptions::new().stdout_tail_cap(10).stderr_tail_cap(6);
1280 let (output, chunks, lines) = stream(command, &options, false);
1281 assert!(output.status.success());
1282 assert_eq!(chunks.len(), 100_003);
1284 assert_eq!(lines, ["l1", "l2", "l3", "l4"]);
1285 assert_eq!(output.stdout_tail, b"xxxxxxxEND".to_vec());
1286 assert_eq!(output.stderr_tail, "l3\nl4");
1287 }
1288
1289 #[cfg(unix)]
1290 #[test]
1291 fn run_streaming_cancel_after_the_first_chunk_kills_the_tree_and_a_grandchild() {
1292 let _serial = serial();
1293 let holder = Grandchild::new("stream-cancel");
1294 let command = sh(
1297 "sleep 30 & echo $! > \"$1\"; printf go; sleep 30",
1298 &[holder.0.to_str().unwrap()],
1299 );
1300 let options = StreamOptions::new().poll(Duration::from_millis(50));
1301 let started = Instant::now();
1302 let (output, chunks, _lines) = stream(command, &options, true);
1303 let elapsed = started.elapsed();
1304 assert!(output.cancelled);
1305 assert!(!output.timed_out);
1306 assert!(!output.status.success());
1307 assert_eq!(chunks, b"go".to_vec());
1308 assert_eq!(output.stdout_tail, b"go".to_vec());
1309 assert!(
1310 elapsed < Duration::from_secs(4),
1311 "cancel took {elapsed:?} to take effect"
1312 );
1313 assert!(holder.pid().is_some(), "the grandchild never started");
1314 let deadline = Instant::now() + Duration::from_secs(5);
1315 while holder.alive() {
1316 assert!(
1317 Instant::now() < deadline,
1318 "the grandchild survived the kill"
1319 );
1320 std::thread::sleep(Duration::from_millis(20));
1321 }
1322 }
1323
1324 #[test]
1325 fn run_streaming_cancel_before_any_output_ends_the_call() {
1326 let started = Instant::now();
1327 let output = run_streaming(
1328 sleeper(),
1329 &StreamOptions::new(),
1330 &|| true,
1331 &mut |_chunk: &[u8]| {},
1332 &mut |_line: &str| {},
1333 )
1334 .unwrap();
1335 assert!(output.cancelled);
1336 assert!(!output.timed_out);
1337 assert!(started.elapsed() < Duration::from_secs(8));
1338 }
1339
1340 #[test]
1341 fn run_streaming_timeout_sets_timed_out() {
1342 let options = StreamOptions::new().timeout(Duration::from_millis(300));
1343 let started = Instant::now();
1344 let (output, _chunks, lines) = stream(sleeper(), &options, false);
1345 let elapsed = started.elapsed();
1346 assert!(output.timed_out);
1347 assert!(!output.cancelled);
1348 assert!(lines.is_empty());
1349 assert!(elapsed >= Duration::from_millis(300), "{elapsed:?}");
1350 assert!(elapsed < Duration::from_secs(8), "{elapsed:?}");
1351 }
1352
1353 #[cfg(unix)]
1354 #[test]
1355 fn run_streaming_with_input_round_trips_more_than_a_pipe_buffer() {
1356 let _serial = serial();
1357 let big = vec![b'x'; 1 << 20];
1358 let options = StreamOptions::new()
1359 .timeout(Duration::from_secs(30))
1360 .input(big.clone());
1361 let (output, chunks, _lines) = stream(sh("cat", &[]), &options, false);
1362 assert!(output.status.success());
1363 assert!(!output.timed_out);
1364 assert_eq!(chunks, big);
1365 assert_eq!(output.stdout_tail.len(), 64 * 1024);
1366 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1367 let options = StreamOptions::new().input(big);
1369 let (output, _chunks, _lines) = stream(sh("exit 7", &[]), &options, false);
1370 assert_eq!(output.status.code(), Some(7));
1371 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1372 }
1373
1374 #[cfg(unix)]
1375 #[test]
1376 fn a_session_escaped_stdin_holder_does_not_hold_run_streaming_past_its_timeout() {
1377 let _serial = serial();
1378 let holder = Grandchild::new("stream-timeout");
1379 let script = holder_script("exec sleep 30", "sleep 30");
1380 let timeout = Duration::from_secs(1);
1381 let options = StreamOptions::new()
1382 .timeout(timeout)
1383 .input(vec![b'x'; 2 << 20]);
1384 let started = Instant::now();
1385 let (output, _chunks, _lines) = stream(
1386 sh(script.as_str(), &[holder.0.to_str().unwrap()]),
1387 &options,
1388 false,
1389 );
1390 let elapsed = started.elapsed();
1391 if !holder.alive() && !output.timed_out {
1392 eprintln!("skipped: no setsid or perl to leave the session");
1393 return;
1394 }
1395 assert!(
1396 holder.alive(),
1397 "the fixture's grandchild must still hold stdin"
1398 );
1399 assert!(output.timed_out);
1400 assert!(
1401 elapsed < timeout + Duration::from_secs(8),
1402 "run_streaming took {elapsed:?}"
1403 );
1404 assert_eq!(
1405 LIVE_WRITERS.load(SeqCst),
1406 0,
1407 "the writer thread must be gone"
1408 );
1409 }
1410
1411 fn noisy() -> OwnedCommand {
1415 #[cfg(unix)]
1416 {
1417 sh("printf out; printf err >&2", &[])
1418 }
1419 #[cfg(windows)]
1420 {
1421 use std::os::windows::process::CommandExt;
1422 let mut command = OwnedCommand::new("cmd");
1423 command.command_mut().raw_arg("/C echo out & echo err 1>&2");
1424 command
1425 }
1426 }
1427
1428 #[test]
1429 fn a_capture_read_error_is_an_error_from_run_command() {
1430 use crate::native_capture::{inject_read_fault, Stream};
1431 let options = RunOptions::new(Duration::from_secs(20));
1432 for (stream, name) in [(Stream::Stdout, "stdout"), (Stream::Stderr, "stderr")] {
1433 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1434 let error = run_command(noisy(), None, &options)
1435 .expect_err("a stored capture error must not become an Ok with partial output");
1436 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1437 assert!(error.to_string().contains(name), "{error}");
1438 }
1439 let output = run_command(noisy(), None, &options).unwrap();
1441 assert!(output.status.success());
1442 assert!(!output.stdout.bytes.is_empty() && !output.stderr.bytes.is_empty());
1443 }
1444
1445 fn noisy_argv() -> (&'static str, Vec<&'static str>) {
1447 #[cfg(unix)]
1448 {
1449 ("sh", vec!["-c", "printf out; printf err >&2"])
1450 }
1451 #[cfg(windows)]
1452 {
1453 ("cmd", vec!["/C", "echo out & echo err 1>&2"])
1454 }
1455 }
1456
1457 #[test]
1458 fn a_capture_read_error_is_an_error_from_run_and_run_with_stdin() {
1459 use crate::native_capture::{inject_read_fault, Stream};
1460 let options = RunOptions::new(Duration::from_secs(20));
1461 let (program, args) = noisy_argv();
1462 for (stream, name) in [(Stream::Stdout, "stdout"), (Stream::Stderr, "stderr")] {
1463 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1464 let error = run(program, &args, &options).expect_err(
1465 "run: a stored capture error must not become an Ok with partial output",
1466 );
1467 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1468 assert!(error.to_string().contains(name), "{error}");
1469
1470 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1471 let error = run_with_stdin(program, &args, b"input", &options).expect_err(
1472 "run_with_stdin: a stored capture error must not become an Ok with partial output",
1473 );
1474 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1475 assert!(error.to_string().contains(name), "{error}");
1476 }
1477 let output = run(program, &args, &options).unwrap();
1479 assert!(output.status.success());
1480 assert!(!output.stdout.bytes.is_empty() && !output.stderr.bytes.is_empty());
1481 let output = run_with_stdin(program, &args, b"input", &options).unwrap();
1482 assert!(output.status.success());
1483 }
1484
1485 #[test]
1486 fn configured_capture_without_deadline_keeps_raw_first_bytes() {
1487 let options = CaptureOptions::new().stdout_cap(2).stderr_cap(2);
1488 assert_eq!(options.timeout, None);
1489 let output = run_command_capture(noisy(), None, &options).unwrap();
1490 assert!(output.status.success());
1491 assert!(!output.timed_out);
1492 assert_eq!(output.stdout.bytes, b"ou");
1493 assert_eq!(output.stderr.bytes, b"er");
1494 assert!(output.stdout.truncated && output.stderr.truncated);
1495 assert!(output.stdout.total_bytes >= 3 && output.stderr.total_bytes >= 3);
1496 }
1497
1498 #[test]
1499 fn missing_program_is_typed_not_started_for_both_runners() {
1500 let missing = std::env::current_exe()
1501 .unwrap()
1502 .with_extension("rightkit-missing-program");
1503 let error = run_command_capture(OwnedCommand::new(&missing), None, &CaptureOptions::new())
1504 .unwrap_err();
1505 let failure = RunnerFailure::from_io(&error).unwrap();
1506 assert_eq!(failure.phase(), RunnerPhase::Spawn);
1507 assert_eq!(failure.termination(), LeaderTermination::NotStarted);
1508 assert_eq!(failure.source_io_error().kind(), io::ErrorKind::NotFound);
1509 let error = run_streaming(
1510 OwnedCommand::new(&missing),
1511 &StreamOptions::new(),
1512 &|| false,
1513 &mut |_| {},
1514 &mut |_| {},
1515 )
1516 .unwrap_err();
1517 let failure = RunnerFailure::from_io(&error).unwrap();
1518 assert_eq!(failure.phase(), RunnerPhase::Spawn);
1519 assert_eq!(failure.termination(), LeaderTermination::NotStarted);
1520 }
1521
1522 #[test]
1523 fn a_cancelled_capture_without_eof_is_never_successful_output() {
1524 let mut command = sleeper();
1525 command.pipe_stdio(false);
1526 let mut child = command.spawn().unwrap();
1527 let mut stdout = NativeOutputCapture::stdout(child.take_stdout().unwrap(), 128).unwrap();
1528 let mut stderr = NativeOutputCapture::stderr(child.take_stderr().unwrap(), 128).unwrap();
1529 stdout.cancel();
1530 stderr.cancel();
1531 assert!(matches!(
1532 child
1533 .terminate_tree_bounded(Duration::from_secs(5))
1534 .unwrap(),
1535 crate::ReapOutcome::Exited(_)
1536 ));
1537 for (name, capture) in [("stdout", &stdout), ("stderr", &stderr)] {
1538 assert!(!capture.is_complete());
1539 let error = capture_failure(name, Some(capture)).unwrap_err();
1540 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
1541 assert!(error.to_string().contains(name));
1542 assert_eq!(
1543 RunnerFailure::from_io(&error).unwrap().phase(),
1544 RunnerPhase::Drain
1545 );
1546 }
1547 }
1548
1549 #[test]
1550 fn a_capture_read_error_is_an_error_from_run_streaming() {
1551 use crate::native_capture::{inject_read_fault, Stream};
1552 let options = StreamOptions::new().timeout(Duration::from_secs(20));
1553 for (stream, name) in [(Stream::Stdout, "stdout"), (Stream::Stderr, "stderr")] {
1554 inject_read_fault(stream, io::ErrorKind::PermissionDenied);
1555 let error = run_streaming(noisy(), &options, &|| false, &mut |_| {}, &mut |_| {})
1556 .expect_err("a stored capture error must not become an Ok with partial output");
1557 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
1558 assert!(error.to_string().contains(name), "{error}");
1559 let detail = RunnerFailure::from_io(&error).unwrap();
1560 assert_eq!(detail.termination(), LeaderTermination::Confirmed);
1561 assert_eq!(
1562 detail.phase(),
1563 if name == "stdout" {
1564 RunnerPhase::Stdout
1565 } else {
1566 RunnerPhase::Stderr
1567 }
1568 );
1569 assert_eq!(
1570 detail.source_io_error().kind(),
1571 io::ErrorKind::PermissionDenied
1572 );
1573 }
1574 let output = run_streaming(noisy(), &options, &|| false, &mut |_| {}, &mut |_| {}).unwrap();
1575 assert!(output.status.success());
1576 }
1577
1578 #[test]
1583 fn command_mut_after_the_piped_setup_does_not_lose_it() {
1584 let mut command = sleeper();
1585 command.pipe_stdio(true);
1586 command
1587 .command_mut()
1588 .stdin(Stdio::null())
1589 .stdout(Stdio::inherit())
1590 .stderr(Stdio::null());
1591 let mut child = command.spawn().unwrap();
1592 assert!(child.take_stdin().is_some(), "stdin must stay piped");
1593 assert!(child.take_stdout().is_some(), "stdout must stay piped");
1594 assert!(child.take_stderr().is_some(), "stderr must stay piped");
1595 }
1596
1597 #[cfg(windows)]
1600 #[test]
1601 fn command_mut_after_an_explicit_config_does_not_lose_the_piped_setup() {
1602 use crate::{WindowsSpawnConfig, WindowsStdio};
1603 let mut command = sleeper();
1604 command
1605 .inherit_handles_only([])
1606 .windows_spawn_config(WindowsSpawnConfig {
1607 stdin: WindowsStdio::Null,
1608 stdout: WindowsStdio::Null,
1609 stderr: WindowsStdio::Null,
1610 ..Default::default()
1611 });
1612 command.pipe_stdio(true);
1613 command
1614 .command_mut()
1615 .stdin(Stdio::null())
1616 .stdout(Stdio::null())
1617 .stderr(Stdio::null());
1618 let mut child = command.spawn().unwrap();
1619 assert!(child.take_stdin().is_some(), "stdin must stay piped");
1620 assert!(child.take_stdout().is_some(), "stdout must stay piped");
1621 assert!(child.take_stderr().is_some(), "stderr must stay piped");
1622 }
1623
1624 #[cfg(windows)]
1627 #[test]
1628 fn the_piped_setup_keeps_the_automatic_windows_config() {
1629 let mut command = OwnedCommand::new("sort");
1630 command.inherit_handles_only([]);
1631 command.pipe_stdio(true);
1632 let mut child = command.spawn().unwrap();
1633 assert!(child.take_stdin().is_some(), "stdin must be piped");
1634 assert!(child.take_stdout().is_some(), "stdout must be piped");
1635 assert!(child.take_stderr().is_some(), "stderr must be piped");
1636 }
1637
1638 #[cfg(windows)]
1640 fn echo_stdin_and_report_on_stderr() -> OwnedCommand {
1641 use std::os::windows::process::CommandExt;
1642 let mut command = OwnedCommand::new("cmd");
1643 command
1644 .command_mut()
1645 .raw_arg("/C findstr . & echo err 1>&2");
1646 command
1647 }
1648
1649 #[cfg(windows)]
1652 fn explicit_configs() -> Vec<crate::WindowsSpawnConfig> {
1653 use crate::{WindowsSpawnConfig, WindowsStdio};
1654 vec![
1655 WindowsSpawnConfig::default(),
1656 WindowsSpawnConfig {
1657 stdin: WindowsStdio::Null,
1658 stdout: WindowsStdio::Null,
1659 stderr: WindowsStdio::Null,
1660 ..Default::default()
1661 },
1662 ]
1663 }
1664
1665 #[cfg(windows)]
1666 #[test]
1667 fn a_configured_windows_command_gets_stdin_and_captures_both_streams_through_run_command() {
1668 let _serial = serial();
1669 for config in explicit_configs() {
1670 let mut command = echo_stdin_and_report_on_stderr();
1671 command
1672 .inherit_handles_only([])
1673 .windows_spawn_config(config);
1674 let output = run_command(
1675 command,
1676 Some(b"hello\r\n"),
1677 &RunOptions::new(Duration::from_secs(30)),
1678 )
1679 .unwrap();
1680 assert!(!output.timed_out);
1681 let stdout = String::from_utf8_lossy(&output.stdout.bytes).into_owned();
1682 let stderr = String::from_utf8_lossy(&output.stderr.bytes).into_owned();
1683 assert!(
1684 stdout.contains("hello"),
1685 "stdin was not delivered: {stdout:?}"
1686 );
1687 assert!(
1688 stderr.contains("err"),
1689 "stderr was not captured: {stderr:?}"
1690 );
1691 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1692 }
1693 }
1694
1695 #[cfg(windows)]
1696 #[test]
1697 fn a_configured_windows_command_gets_stdin_and_captures_both_streams_through_run_streaming() {
1698 let _serial = serial();
1699 for config in explicit_configs() {
1700 let mut command = echo_stdin_and_report_on_stderr();
1701 command
1702 .inherit_handles_only([])
1703 .windows_spawn_config(config);
1704 let options = StreamOptions::new()
1705 .timeout(Duration::from_secs(30))
1706 .input(b"hello\r\n".to_vec());
1707 let (output, chunks, lines) = stream(command, &options, false);
1708 assert!(!output.timed_out);
1709 let stdout = String::from_utf8_lossy(&chunks).into_owned();
1710 assert!(
1711 stdout.contains("hello"),
1712 "stdin was not delivered: {stdout:?}"
1713 );
1714 assert!(
1715 lines.iter().any(|line| line.contains("err")),
1716 "stderr was not captured: {lines:?}"
1717 );
1718 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
1719 }
1720 }
1721
1722 #[cfg(windows)]
1725 #[test]
1726 fn an_allowlisted_new_command_with_the_automatic_config_runs_through_both_runners() {
1727 let _serial = serial();
1728 let sorted = |bytes: &[u8]| String::from_utf8_lossy(bytes).replace("\r\n", "\n");
1729 let mut command = OwnedCommand::new("sort");
1730 command.inherit_handles_only([]);
1731 let output = run_command(
1732 command,
1733 Some(b"b\r\na\r\n"),
1734 &RunOptions::new(Duration::from_secs(30)),
1735 )
1736 .unwrap();
1737 assert_eq!(sorted(&output.stdout.bytes), "a\nb\n");
1738
1739 let mut command = OwnedCommand::new("sort");
1740 command.inherit_handles_only([]);
1741 let options = StreamOptions::new()
1742 .timeout(Duration::from_secs(30))
1743 .input(b"b\r\na\r\n".to_vec());
1744 let (_output, chunks, _lines) = stream(command, &options, false);
1745 assert_eq!(sorted(&chunks), "a\nb\n");
1746 }
1747
1748 #[cfg(windows)]
1770 const HELPER_ENV: &str = "RK_RUN_HELPER";
1771 #[cfg(windows)]
1774 const HELPER_ARG: &str = "rk-run-helper=";
1775 #[cfg(windows)]
1776 const BEGIN: &[u8] = b"<<RK-BEGIN>>";
1777 #[cfg(windows)]
1778 const END: &[u8] = b"<<RK-END>>";
1779 #[cfg(windows)]
1781 const GO: &[u8] = b"<<RK-GO>>";
1782 #[cfg(windows)]
1785 const DONE: &[u8] = b"<<RK-DONE>>";
1786 #[cfg(windows)]
1788 const HOLD_SECONDS: u64 = 120;
1789
1790 #[cfg(windows)]
1793 fn pattern(offset: usize, len: usize) -> Vec<u8> {
1794 (offset..offset + len)
1795 .map(|index| b'!' + ((index + index / 95) % 90) as u8)
1796 .collect()
1797 }
1798
1799 #[cfg(windows)]
1800 fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1801 haystack
1802 .windows(needle.len())
1803 .position(|window| window == needle)
1804 }
1805
1806 #[cfg(windows)]
1808 fn framed(output: &[u8]) -> &[u8] {
1809 let begin = find(output, BEGIN).expect("the begin marker is missing") + BEGIN.len();
1810 let end = begin + find(&output[begin..], END).expect("the end marker is missing");
1811 &output[begin..end]
1812 }
1813
1814 #[cfg(windows)]
1815 fn helper_spec() -> Option<String> {
1816 if let Ok(spec) = std::env::var(HELPER_ENV) {
1817 return Some(spec);
1818 }
1819 std::env::args().find_map(|arg| arg.strip_prefix(HELPER_ARG).map(str::to_owned))
1820 }
1821
1822 #[cfg(windows)]
1824 fn helper_test_name() -> String {
1825 let module = module_path!();
1826 let path = module.split_once("::").map_or(module, |(_, rest)| rest);
1827 format!("{path}::windows_runner_helper_child")
1828 }
1829
1830 #[cfg(windows)]
1832 fn helper_command(spec: &str) -> OwnedCommand {
1833 let mut command = OwnedCommand::new(std::env::current_exe().unwrap());
1834 command
1835 .command_mut()
1836 .args([
1837 "--exact",
1838 helper_test_name().as_str(),
1839 "--nocapture",
1840 "--test-threads=1",
1841 ])
1842 .env(HELPER_ENV, spec);
1843 command
1844 }
1845
1846 #[cfg(windows)]
1849 fn helper_argv(spec: &str) -> Vec<String> {
1850 vec![
1851 "--exact".to_owned(),
1852 helper_test_name(),
1853 "--nocapture".to_owned(),
1854 "--test-threads=1".to_owned(),
1855 format!("{HELPER_ARG}{spec}"),
1856 ]
1857 }
1858
1859 #[cfg(windows)]
1860 fn wait_for_file(path: &std::path::Path, limit: Duration) -> bool {
1861 let deadline = Instant::now() + limit;
1862 while !path.exists() {
1863 if Instant::now() >= deadline {
1864 return false;
1865 }
1866 std::thread::sleep(Duration::from_millis(20));
1867 }
1868 true
1869 }
1870
1871 #[cfg(windows)]
1873 fn write_atomic(path: &std::path::Path, text: &str) {
1874 let mut temporary = path.as_os_str().to_owned();
1875 temporary.push(".tmp");
1876 std::fs::write(&temporary, text).unwrap();
1877 std::fs::rename(&temporary, path).unwrap();
1878 }
1879
1880 #[cfg(windows)]
1881 fn hold_for_a_while() {
1882 for _ in 0..HOLD_SECONDS {
1883 std::thread::sleep(Duration::from_secs(1));
1884 }
1885 }
1886
1887 #[cfg(windows)]
1889 fn echo_stdin_framed() {
1890 use std::io::{Read, Write};
1891 let mut stdin = std::io::stdin();
1892 let mut stdout = std::io::stdout();
1893 stdout.write_all(BEGIN).unwrap();
1894 let mut buffer = vec![0u8; 64 * 1024];
1895 loop {
1896 let read = stdin.read(&mut buffer).unwrap();
1897 if read == 0 {
1898 break;
1899 }
1900 stdout.write_all(&buffer[..read]).unwrap();
1901 }
1902 stdout.write_all(END).unwrap();
1903 stdout.flush().unwrap();
1904 }
1905
1906 #[cfg(windows)]
1909 fn flood_stderr(total: usize) {
1910 use std::io::Write;
1911 let mut stderr = std::io::stderr();
1912 stderr.write_all(BEGIN).unwrap();
1913 let mut written = 0;
1914 while written < total {
1915 let chunk = (total - written).min(8 * 1024);
1916 stderr.write_all(&pattern(written, chunk)).unwrap();
1917 written += chunk;
1918 }
1919 let mut stdout = std::io::stdout();
1920 stdout.write_all(DONE).unwrap();
1921 stdout.flush().unwrap();
1922 }
1923
1924 #[cfg(windows)]
1927 fn flood_stderr_lines(count: usize) {
1928 use std::io::Write;
1929 let mut stderr = std::io::stderr();
1930 let mut next = 0;
1931 while next < count {
1932 let upto = (next + 1000).min(count);
1933 let mut block = String::new();
1934 for index in next..upto {
1935 block.push_str(&format!("L{index:07}\n"));
1936 }
1937 stderr.write_all(block.as_bytes()).unwrap();
1938 next = upto;
1939 }
1940 let mut stdout = std::io::stdout();
1941 stdout.write_all(DONE).unwrap();
1942 stdout.flush().unwrap();
1943 }
1944
1945 #[cfg(windows)]
1949 fn hold_then_count_stdin(
1950 ready: &std::path::Path,
1951 gate: &std::path::Path,
1952 count_file: &std::path::Path,
1953 ) {
1954 use std::io::Read;
1955 write_atomic(ready, "ready");
1956 assert!(
1957 wait_for_file(gate, Duration::from_secs(HOLD_SECONDS)),
1958 "the gate never opened"
1959 );
1960 let mut stdin = std::io::stdin();
1961 let mut buffer = vec![0u8; 64 * 1024];
1962 let mut total = 0usize;
1963 loop {
1964 match stdin.read(&mut buffer) {
1965 Ok(0) => break,
1966 Ok(read) => total += read,
1967 Err(error) if error.kind() == io::ErrorKind::BrokenPipe => break,
1968 Err(error) => panic!("reading stdin failed: {error}"),
1969 }
1970 }
1971 write_atomic(count_file, &total.to_string());
1972 hold_for_a_while();
1973 }
1974
1975 #[cfg(windows)]
1981 #[allow(clippy::zombie_processes)] fn run_leader(dir: &std::path::Path, holder: &str, breakaway: bool, then: &str) {
1983 use std::io::Write;
1984 use std::os::windows::process::CommandExt;
1985 write_atomic(&dir.join("leader.pid"), &std::process::id().to_string());
1986 let ready = dir.join("holder.ready");
1987 let holder_spec = match holder {
1988 "hold" => format!("hold|{}", ready.display()),
1989 "hold-read" => format!(
1990 "hold-read|{}|{}|{}",
1991 ready.display(),
1992 dir.join("gate").display(),
1993 dir.join("count").display()
1994 ),
1995 other => panic!("unknown holder {other}"),
1996 };
1997 let mut command = std::process::Command::new(std::env::current_exe().unwrap());
1998 command
1999 .args([
2000 "--exact",
2001 helper_test_name().as_str(),
2002 "--nocapture",
2003 "--test-threads=1",
2004 ])
2005 .env(HELPER_ENV, holder_spec)
2006 .stdin(std::process::Stdio::inherit())
2007 .stdout(std::process::Stdio::inherit())
2008 .stderr(std::process::Stdio::inherit());
2009 if breakaway {
2010 command.creation_flags(crate::CREATE_BREAKAWAY_FROM_JOB);
2011 }
2012 let child = match command.spawn() {
2013 Ok(child) => child,
2014 Err(error) => {
2015 let report = error
2016 .raw_os_error()
2017 .map_or_else(|| error.to_string(), |code| code.to_string());
2018 write_atomic(&dir.join("holder.err"), &report);
2019 return;
2020 }
2021 };
2022 if !wait_for_file(&ready, Duration::from_secs(30)) {
2023 write_atomic(&dir.join("holder.err"), "the holder never became ready");
2024 return;
2025 }
2026 write_atomic(&dir.join("holder.pid"), &child.id().to_string());
2027 drop(child);
2028 match then {
2029 "echo" => echo_stdin_framed(),
2030 "exit" => {}
2031 "block" => hold_for_a_while(),
2032 "go" => {
2033 let mut stdout = std::io::stdout();
2034 stdout.write_all(GO).unwrap();
2035 stdout.flush().unwrap();
2036 hold_for_a_while();
2037 }
2038 other => panic!("unknown leader action {other}"),
2039 }
2040 }
2041
2042 #[cfg(windows)]
2047 #[test]
2048 fn windows_runner_helper_child() {
2049 let Some(spec) = helper_spec() else {
2050 return;
2051 };
2052 let parts: Vec<&str> = spec.split('|').collect();
2053 match parts.as_slice() {
2054 ["echo"] => echo_stdin_framed(),
2055 ["exit-now"] => std::process::exit(7),
2056 ["flood", total] => flood_stderr(total.parse().unwrap()),
2057 ["flood-lines", count] => flood_stderr_lines(count.parse().unwrap()),
2058 ["hold", ready] => {
2059 write_atomic(std::path::Path::new(ready), "ready");
2060 hold_for_a_while();
2061 }
2062 ["hold-read", ready, gate, count] => hold_then_count_stdin(
2063 std::path::Path::new(ready),
2064 std::path::Path::new(gate),
2065 std::path::Path::new(count),
2066 ),
2067 ["leader", dir, holder, breakaway, then] => {
2068 run_leader(std::path::Path::new(dir), holder, *breakaway == "1", then)
2069 }
2070 other => panic!("unknown helper spec {other:?}"),
2071 }
2072 }
2073
2074 #[cfg(windows)]
2078 struct HelperFixture {
2079 dir: std::path::PathBuf,
2080 holder: Option<crate::AdoptedIdentity>,
2081 }
2082
2083 #[cfg(windows)]
2084 impl HelperFixture {
2085 fn new(label: &str) -> Self {
2086 let nanos = std::time::SystemTime::now()
2087 .duration_since(std::time::UNIX_EPOCH)
2088 .map_or(0, |elapsed| elapsed.as_nanos());
2089 let dir = std::env::temp_dir()
2090 .join(format!("rk-run-win-{label}-{}-{nanos}", std::process::id()));
2091 std::fs::create_dir_all(&dir).unwrap();
2092 Self { dir, holder: None }
2093 }
2094
2095 fn read(&self, name: &str) -> Option<String> {
2096 std::fs::read_to_string(self.dir.join(name)).ok()
2097 }
2098
2099 fn pid(&self, name: &str) -> Option<u32> {
2100 self.read(name)?.trim().parse().ok()
2101 }
2102
2103 fn leader_spec(&self, holder: &str, breakaway: bool, then: &str) -> String {
2104 format!(
2105 "leader|{}|{holder}|{}|{then}",
2106 self.dir.display(),
2107 u8::from(breakaway)
2108 )
2109 }
2110
2111 fn command(&self, holder: &str, breakaway: bool, then: &str) -> OwnedCommand {
2114 let mut command = helper_command(&self.leader_spec(holder, breakaway, then));
2115 command.windows_allow_breakaway(breakaway);
2116 command
2117 }
2118
2119 fn argv(&self, holder: &str, breakaway: bool, then: &str) -> Vec<String> {
2121 helper_argv(&self.leader_spec(holder, breakaway, then))
2122 }
2123
2124 fn holder_pid(&self) -> u32 {
2126 if let Some(error) = self.read("holder.err") {
2127 panic!(
2128 "the leader could not start its holder: {error} (5 means the job refused \
2129 CREATE_BREAKAWAY_FROM_JOB)"
2130 );
2131 }
2132 self.pid("holder.pid").expect(
2133 "the leader never reported its holder before the call ended (a very slow host?)",
2134 )
2135 }
2136
2137 fn track_holder(&mut self) -> u32 {
2139 let pid = self.holder_pid();
2140 let identity = crate::AdoptedIdentity::capture(std::num::NonZeroU32::new(pid).unwrap())
2141 .unwrap()
2142 .unwrap_or_else(|| {
2143 panic!("the holder (pid {pid}) is gone: it did not outlive the call")
2144 });
2145 assert!(
2146 identity.is_running().unwrap(),
2147 "the holder (pid {pid}) must still hold the pipes"
2148 );
2149 self.holder = Some(identity);
2150 pid
2151 }
2152 }
2153
2154 #[cfg(windows)]
2155 impl Drop for HelperFixture {
2156 fn drop(&mut self) {
2157 if let Some(identity) = self.holder.take() {
2158 let _ = identity.signal(crate::AdoptedSignal::Force);
2159 let _ = identity.wait_confirmed_gone(Duration::from_secs(5));
2160 }
2161 let _ = std::fs::remove_dir_all(&self.dir);
2162 }
2163 }
2164
2165 #[cfg(windows)]
2167 fn assert_gone(pid: u32, what: &str) {
2168 let process = crate::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
2169 let deadline = Instant::now() + Duration::from_secs(10);
2170 while process.is_running().unwrap() {
2171 assert!(
2172 Instant::now() < deadline,
2173 "{what} (pid {pid}) is still running"
2174 );
2175 std::thread::sleep(Duration::from_millis(20));
2176 }
2177 }
2178
2179 #[cfg(windows)]
2183 #[test]
2184 fn run_with_stdin_and_run_command_round_trip_one_mib_and_reap_an_in_job_holder_windows() {
2185 let _serial = serial();
2186 let input = pattern(0, 1 << 20);
2187 let options = RunOptions::new(Duration::from_secs(60)).stdout_cap(4 << 20);
2188 for entry in ["run_with_stdin", "run_command"] {
2189 let fixture = HelperFixture::new(entry);
2190 let started = Instant::now();
2191 let result = if entry == "run_with_stdin" {
2192 run_with_stdin(
2193 std::env::current_exe().unwrap(),
2194 fixture.argv("hold", false, "echo"),
2195 &input,
2196 &options,
2197 )
2198 } else {
2199 run_command(
2200 fixture.command("hold", false, "echo"),
2201 Some(&input),
2202 &options,
2203 )
2204 };
2205 let output = result.unwrap();
2206 let elapsed = started.elapsed();
2207 assert!(output.status.success(), "{entry}: {:?}", output.status);
2208 assert!(!output.timed_out, "{entry}: the call ran to its deadline");
2209 assert!(!output.stdout.truncated, "{entry}: stdout hit its cap");
2210 let echoed = framed(&output.stdout.bytes);
2211 assert!(
2212 echoed == input.as_slice(),
2213 "{entry}: echoed {} bytes, expected {}",
2214 echoed.len(),
2215 input.len()
2216 );
2217 assert!(
2218 elapsed < Duration::from_secs(30),
2219 "{entry} took {elapsed:?}"
2220 );
2221 assert_eq!(
2222 LIVE_WRITERS.load(SeqCst),
2223 0,
2224 "{entry}: the writer thread must be gone"
2225 );
2226 assert_gone(fixture.holder_pid(), "the in-job holder");
2227 }
2228 }
2229
2230 #[cfg(windows)]
2233 #[test]
2234 fn run_streaming_with_input_round_trips_one_mib_and_reaps_an_in_job_holder_windows() {
2235 let _serial = serial();
2236 let input = pattern(0, 1 << 20);
2237 let fixture = HelperFixture::new("stream-round-trip");
2238 let options = StreamOptions::new()
2239 .timeout(Duration::from_secs(60))
2240 .input(input.clone());
2241 let started = Instant::now();
2242 let command = fixture.command("hold", false, "echo");
2243 let (output, chunks, _lines) = stream(command, &options, false);
2244 let elapsed = started.elapsed();
2245 assert!(output.status.success(), "{:?}", output.status);
2246 assert!(!output.timed_out && !output.cancelled);
2247 let echoed = framed(&chunks);
2248 assert!(
2249 echoed == input.as_slice(),
2250 "echoed {} bytes, expected {}",
2251 echoed.len(),
2252 input.len()
2253 );
2254 assert_eq!(output.stdout_tail.len(), 64 * 1024);
2255 assert!(
2256 elapsed < Duration::from_secs(30),
2257 "run_streaming took {elapsed:?}"
2258 );
2259 assert_eq!(
2260 LIVE_WRITERS.load(SeqCst),
2261 0,
2262 "the writer thread must be gone"
2263 );
2264 assert_gone(fixture.holder_pid(), "the in-job holder");
2265 }
2266
2267 #[cfg(windows)]
2271 #[test]
2272 fn a_child_that_exits_without_reading_its_input_is_a_normal_early_exit_through_every_runner_windows(
2273 ) {
2274 let _serial = serial();
2275 let big = pattern(0, 1 << 20);
2276 let options = RunOptions::new(Duration::from_secs(60));
2277 let output = run_with_stdin(
2278 std::env::current_exe().unwrap(),
2279 helper_argv("exit-now"),
2280 &big,
2281 &options,
2282 )
2283 .unwrap();
2284 assert_eq!(output.status.code(), Some(7));
2285 assert!(!output.timed_out);
2286 let output = run_command(helper_command("exit-now"), Some(&big), &options).unwrap();
2287 assert_eq!(output.status.code(), Some(7));
2288 assert!(!output.timed_out);
2289 let options = StreamOptions::new()
2290 .timeout(Duration::from_secs(60))
2291 .input(big.clone());
2292 let (output, _chunks, _lines) = stream(helper_command("exit-now"), &options, false);
2293 assert_eq!(output.status.code(), Some(7));
2294 assert!(!output.timed_out && !output.cancelled);
2295 assert_eq!(
2296 LIVE_WRITERS.load(SeqCst),
2297 0,
2298 "the writer thread must be gone"
2299 );
2300 }
2301
2302 #[cfg(windows)]
2309 #[test]
2310 fn a_breakaway_stdin_holder_does_not_hold_run_command_past_its_exit_windows() {
2311 let _serial = serial();
2312 let mut fixture = HelperFixture::new("command-exit");
2313 let big = pattern(0, 2 << 20);
2314 let options = RunOptions::new(Duration::from_secs(60));
2315 let started = Instant::now();
2316 let error = run_command(
2317 fixture.command("hold-read", true, "exit"),
2318 Some(&big),
2319 &options,
2320 )
2321 .unwrap_err();
2322 let elapsed = started.elapsed();
2323 fixture.track_holder();
2324 assert_incomplete_drain(&error);
2325 assert!(
2327 elapsed < Duration::from_secs(20),
2328 "run_command took {elapsed:?}"
2329 );
2330 assert_eq!(
2331 LIVE_WRITERS.load(SeqCst),
2332 0,
2333 "the writer thread must be gone"
2334 );
2335 std::fs::write(fixture.dir.join("gate"), "open").unwrap();
2336 let deadline = Instant::now() + Duration::from_secs(30);
2337 let received = loop {
2338 let count = fixture.read("count");
2339 if let Some(count) = count.and_then(|text| text.trim().parse::<usize>().ok()) {
2340 break count;
2341 }
2342 assert!(
2343 Instant::now() < deadline,
2344 "the holder never reached the end of stdin: the write end is still open"
2345 );
2346 std::thread::sleep(Duration::from_millis(50));
2347 };
2348 assert!(
2349 received < big.len() / 2,
2350 "{received} bytes reached the holder after run_command returned"
2351 );
2352 }
2353
2354 #[cfg(windows)]
2360 #[test]
2361 fn a_breakaway_stdin_holder_does_not_hold_run_command_past_its_timeout_windows() {
2362 let _serial = serial();
2363 let mut fixture = HelperFixture::new("command-timeout");
2364 let big = pattern(0, 2 << 20);
2365 let timeout = Duration::from_secs(8);
2366 let options = RunOptions::new(timeout);
2367 let started = Instant::now();
2368 let error =
2369 run_command(fixture.command("hold", true, "block"), Some(&big), &options).unwrap_err();
2370 let elapsed = started.elapsed();
2371 fixture.track_holder();
2372 assert_incomplete_drain(&error);
2373 assert!(
2374 elapsed < timeout + Duration::from_secs(15),
2375 "run_command took {elapsed:?}"
2376 );
2377 assert_eq!(
2378 LIVE_WRITERS.load(SeqCst),
2379 0,
2380 "the writer thread must be gone"
2381 );
2382 }
2383
2384 #[cfg(windows)]
2386 #[test]
2387 fn a_breakaway_stdin_holder_does_not_hold_run_streaming_past_its_timeout_windows() {
2388 let _serial = serial();
2389 let mut fixture = HelperFixture::new("stream-timeout");
2390 let timeout = Duration::from_secs(8);
2391 let options = StreamOptions::new()
2392 .timeout(timeout)
2393 .input(pattern(0, 2 << 20));
2394 let started = Instant::now();
2395 let (outcome, _chunks, _lines) =
2396 stream_outcome(fixture.command("hold", true, "block"), &options, false);
2397 let error = outcome.unwrap_err();
2398 let elapsed = started.elapsed();
2399 fixture.track_holder();
2400 assert_incomplete_drain(&error);
2401 assert!(
2402 elapsed < timeout + Duration::from_secs(15),
2403 "run_streaming took {elapsed:?}"
2404 );
2405 assert_eq!(
2406 LIVE_WRITERS.load(SeqCst),
2407 0,
2408 "the writer thread must be gone"
2409 );
2410 }
2411
2412 #[cfg(windows)]
2416 #[test]
2417 fn run_with_stdin_contains_a_descendant_that_asks_to_break_away_windows() {
2418 let _serial = serial();
2419 let fixture = HelperFixture::new("no-breakaway");
2420 let output = run_with_stdin(
2421 std::env::current_exe().unwrap(),
2422 fixture.argv("hold", true, "exit"),
2423 b"",
2424 &RunOptions::new(Duration::from_secs(60)),
2425 )
2426 .unwrap();
2427 assert!(output.status.success(), "{:?}", output.status);
2428 assert!(!output.timed_out);
2429 assert_eq!(
2430 fixture.read("holder.err").as_deref(),
2431 Some("5"),
2432 "the job must refuse CREATE_BREAKAWAY_FROM_JOB"
2433 );
2434 assert!(fixture.pid("holder.pid").is_none());
2435 assert_eq!(LIVE_WRITERS.load(SeqCst), 0);
2436 }
2437
2438 #[cfg(windows)]
2444 #[test]
2445 fn run_streaming_cancel_after_the_first_chunk_kills_the_tree_and_a_grandchild_windows() {
2446 let fixture = HelperFixture::new("stream-cancel");
2447 let options = StreamOptions::new()
2448 .poll(Duration::from_millis(50))
2449 .timeout(Duration::from_secs(60));
2450 let cancel = std::cell::Cell::new(false);
2451 let seen_at = std::cell::Cell::new(None::<Instant>);
2452 let mut chunks = Vec::new();
2453 let output = run_streaming(
2454 fixture.command("hold", false, "go"),
2455 &options,
2456 &|| cancel.get(),
2457 &mut |chunk: &[u8]| {
2458 chunks.extend_from_slice(chunk);
2459 if !cancel.get() && find(&chunks, GO).is_some() {
2460 seen_at.set(Some(Instant::now()));
2461 cancel.set(true);
2462 }
2463 },
2464 &mut |_line: &str| {},
2465 )
2466 .unwrap();
2467 let seen = seen_at.get().expect("the leader's GO marker never arrived");
2468 assert!(output.cancelled);
2469 assert!(!output.timed_out);
2470 assert!(!output.status.success(), "{:?}", output.status);
2471 assert!(
2472 seen.elapsed() < Duration::from_secs(8),
2473 "cancel took {:?} to take effect",
2474 seen.elapsed()
2475 );
2476 let leader = fixture
2477 .pid("leader.pid")
2478 .expect("the leader recorded no pid");
2479 let holder = fixture.holder_pid();
2480 assert_ne!(leader, holder);
2481 assert_gone(leader, "the leader");
2482 assert_gone(holder, "the in-job holder");
2483 }
2484
2485 #[cfg(windows)]
2490 fn assert_flood_capped(output: &RunOutput, cap: usize, flood: usize) {
2491 assert!(
2492 !output.timed_out,
2493 "the flood held the child until the deadline"
2494 );
2495 assert!(output.status.success(), "{:?}", output.status);
2496 assert_eq!(output.stderr.bytes.len(), cap);
2497 assert!(output.stderr.truncated);
2498 assert!(
2499 output.stderr.total_bytes >= (flood + BEGIN.len()) as u64,
2500 "stderr counted {} bytes, the child wrote {}",
2501 output.stderr.total_bytes,
2502 flood + BEGIN.len()
2503 );
2504 let kept = &output.stderr.bytes;
2505 let at = find(kept, BEGIN).expect("the begin marker is not in the kept bytes");
2506 let body = &output.stderr.bytes[at + BEGIN.len()..];
2507 assert!(
2508 body == pattern(0, body.len()).as_slice(),
2509 "the kept bytes are not the first ones the child wrote"
2510 );
2511 assert!(
2512 !output.stdout.truncated,
2513 "the stderr cap must not touch stdout"
2514 );
2515 assert!(
2516 find(&output.stdout.bytes, DONE).is_some(),
2517 "the child never finished writing"
2518 );
2519 }
2520
2521 #[cfg(windows)]
2525 #[test]
2526 fn a_stderr_flood_past_the_cap_is_truncated_not_blocking_through_run_and_run_command_windows() {
2527 let cap = 64 * 1024;
2528 let flood = 4 << 20;
2529 let started = Instant::now();
2530 let output = run(
2531 std::env::current_exe().unwrap(),
2532 helper_argv(&format!("flood|{flood}")),
2533 &RunOptions::new(Duration::from_secs(60)).stderr_cap(cap),
2534 )
2535 .unwrap();
2536 assert_flood_capped(&output, cap, flood);
2537 let flood = 3 << 20;
2538 let output = run_command(
2539 helper_command(&format!("flood|{flood}")),
2540 None,
2541 &RunOptions::new(Duration::from_secs(60)),
2542 )
2543 .unwrap();
2544 assert_flood_capped(&output, 1024 * 1024, flood);
2545 assert!(
2546 started.elapsed() < Duration::from_secs(90),
2547 "the floods took {:?}",
2548 started.elapsed()
2549 );
2550 }
2551
2552 #[cfg(windows)]
2555 #[test]
2556 fn a_stderr_flood_does_not_block_run_streaming_and_keeps_only_the_tail_windows() {
2557 const LINES: usize = 400_000;
2558 let options = StreamOptions::new()
2559 .timeout(Duration::from_secs(60))
2560 .stderr_tail_cap(1024);
2561 let started = Instant::now();
2562 let (output, _chunks, lines) = stream(
2563 helper_command(&format!("flood-lines|{LINES}")),
2564 &options,
2565 false,
2566 );
2567 assert!(!output.timed_out && !output.cancelled);
2568 assert!(output.status.success(), "{:?}", output.status);
2569 let ours: Vec<&str> = lines
2570 .iter()
2571 .map(String::as_str)
2572 .filter(|line| line.len() == 8 && line.starts_with('L'))
2573 .collect();
2574 assert_eq!(ours.len(), LINES, "lines were lost or never delivered");
2575 for (index, line) in ours.iter().enumerate() {
2576 assert_eq!(*line, format!("L{index:07}"));
2577 }
2578 assert!(
2579 output.stderr_tail.len() <= 1024,
2580 "{}",
2581 output.stderr_tail.len()
2582 );
2583 assert!(
2584 output.stderr_tail.ends_with(&format!("L{:07}", LINES - 1)),
2585 "the tail does not end with the last line"
2586 );
2587 assert!(
2588 find(&output.stdout_tail, DONE).is_some(),
2589 "the child never finished writing"
2590 );
2591 assert!(
2592 started.elapsed() < Duration::from_secs(60),
2593 "run_streaming took {:?}",
2594 started.elapsed()
2595 );
2596 }
2597}