1use std::ffi::{OsStr, OsString};
50use std::fmt;
51use std::io::{Read, Write};
52use std::path::{Path, PathBuf};
53use std::process::{Child, Command, Stdio};
54use std::sync::atomic::{AtomicBool, Ordering};
55use std::sync::{Arc, Mutex};
56use std::time::{Duration, Instant};
57
58use secrecy::{ExposeSecret, SecretBox, SecretString};
59
60use super::WslError;
61
62pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(120);
68
69pub const DEFAULT_STDOUT_LIMIT: usize = 1024 * 1024;
75
76pub const DEFAULT_STDERR_LIMIT: usize = 64 * 1024;
79
80const ARGV_SCAN_LIMIT: usize = 32 * 1024;
89
90const POLL_INTERVAL: Duration = Duration::from_millis(5);
92
93#[derive(Debug, Clone, Default)]
104pub struct Cancellation(Arc<AtomicBool>);
105
106impl Cancellation {
107 #[must_use]
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn cancel(&self) {
115 self.0.store(true, Ordering::SeqCst);
116 }
117
118 #[must_use]
120 pub fn is_cancelled(&self) -> bool {
121 self.0.load(Ordering::SeqCst)
122 }
123}
124
125pub struct PipedInput {
137 bytes: SecretBox<Vec<u8>>,
138 length: usize,
139}
140
141impl PipedInput {
142 #[must_use]
144 pub fn from_bytes(bytes: Vec<u8>) -> Self {
145 let length = bytes.len();
146 Self {
147 bytes: SecretBox::new(Box::new(bytes)),
148 length,
149 }
150 }
151
152 #[must_use]
155 pub fn from_secret_text(text: &SecretString) -> Self {
156 Self::from_bytes(text.expose_secret().as_bytes().to_vec())
157 }
158
159 #[must_use]
161 pub fn len(&self) -> usize {
162 self.length
163 }
164
165 #[must_use]
167 pub fn is_empty(&self) -> bool {
168 self.length == 0
169 }
170
171 pub(crate) fn expose_bytes(&self) -> &[u8] {
180 self.bytes.expose_secret()
181 }
182}
183
184impl fmt::Debug for PipedInput {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 write!(f, "PipedInput(<redacted; {} bytes>)", self.length)
187 }
188}
189
190#[derive(Debug, Default)]
192pub enum ChildInput {
193 #[default]
195 Empty,
196 Piped(PipedInput),
198}
199
200impl ChildInput {
201 #[must_use]
203 pub fn piped(&self) -> Option<&PipedInput> {
204 match self {
205 Self::Empty => None,
206 Self::Piped(input) => Some(input),
207 }
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct OutputLimits {
218 pub stdout: usize,
220 pub stderr: usize,
222}
223
224impl Default for OutputLimits {
225 fn default() -> Self {
226 Self {
227 stdout: DEFAULT_STDOUT_LIMIT,
228 stderr: DEFAULT_STDERR_LIMIT,
229 }
230 }
231}
232
233pub struct CommandRequest {
244 program: PathBuf,
245 arguments: Vec<OsString>,
246 input: ChildInput,
247 limits: OutputLimits,
248 timeout: Duration,
249 cancellation: Option<Cancellation>,
250}
251
252impl CommandRequest {
253 #[must_use]
255 pub fn new(program: impl Into<PathBuf>) -> Self {
256 Self {
257 program: program.into(),
258 arguments: Vec::new(),
259 input: ChildInput::Empty,
260 limits: OutputLimits::default(),
261 timeout: DEFAULT_TIMEOUT,
262 cancellation: None,
263 }
264 }
265
266 #[must_use]
268 pub fn arg(mut self, argument: impl Into<OsString>) -> Self {
269 self.arguments.push(argument.into());
270 self
271 }
272
273 #[must_use]
275 pub fn args<I, S>(mut self, arguments: I) -> Self
276 where
277 I: IntoIterator<Item = S>,
278 S: Into<OsString>,
279 {
280 self.arguments.extend(arguments.into_iter().map(Into::into));
281 self
282 }
283
284 #[must_use]
286 pub fn with_input(mut self, input: ChildInput) -> Self {
287 self.input = input;
288 self
289 }
290
291 #[must_use]
293 pub fn with_timeout(mut self, timeout: Duration) -> Self {
294 self.timeout = timeout;
295 self
296 }
297
298 #[must_use]
300 pub fn with_limits(mut self, limits: OutputLimits) -> Self {
301 self.limits = limits;
302 self
303 }
304
305 #[must_use]
307 pub fn with_cancellation(mut self, cancellation: Cancellation) -> Self {
308 self.cancellation = Some(cancellation);
309 self
310 }
311
312 #[must_use]
314 pub fn program(&self) -> &Path {
315 &self.program
316 }
317
318 #[must_use]
320 pub fn arguments(&self) -> &[OsString] {
321 &self.arguments
322 }
323
324 #[must_use]
326 pub fn argument_strings(&self) -> Vec<String> {
327 self.arguments
328 .iter()
329 .map(|argument| argument.to_string_lossy().into_owned())
330 .collect()
331 }
332
333 #[must_use]
335 pub fn input(&self) -> &ChildInput {
336 &self.input
337 }
338
339 #[must_use]
341 pub fn limits(&self) -> OutputLimits {
342 self.limits
343 }
344
345 #[must_use]
347 pub fn timeout(&self) -> Duration {
348 self.timeout
349 }
350
351 #[must_use]
353 pub fn cancellation(&self) -> Option<&Cancellation> {
354 self.cancellation.as_ref()
355 }
356
357 pub fn refuse_payload_in_argv(&self) -> Result<(), WslError> {
374 let Some(payload) = self.input.piped() else {
375 return Ok(());
376 };
377 if payload.is_empty() || payload.len() > ARGV_SCAN_LIMIT {
378 return Ok(());
379 }
380 let needle = payload.expose_bytes();
381 let found_in = |value: &OsStr| {
382 let text = value.to_string_lossy();
383 contains_subslice(text.as_bytes(), needle)
384 };
385 if found_in(self.program.as_os_str()) {
386 return Err(WslError::SecretInCommandLine {
387 program: self.program.clone(),
388 location: "the program path".to_string(),
389 });
390 }
391 for (index, argument) in self.arguments.iter().enumerate() {
392 if found_in(argument) {
393 return Err(WslError::SecretInCommandLine {
394 program: self.program.clone(),
395 location: format!("argument {index}"),
396 });
397 }
398 }
399 Ok(())
400 }
401}
402
403impl fmt::Debug for CommandRequest {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 f.debug_struct("CommandRequest")
408 .field("program", &self.program)
409 .field("arguments", &self.arguments)
410 .field("input", &self.input)
411 .field("limits", &self.limits)
412 .field("timeout", &self.timeout)
413 .finish_non_exhaustive()
414 }
415}
416
417fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
419 if needle.is_empty() || needle.len() > haystack.len() {
420 return false;
421 }
422 haystack
423 .windows(needle.len())
424 .any(|window| window == needle)
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum Completion {
434 Exited,
436 TimedOut,
438 Cancelled,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct CommandOutput {
445 completion: Completion,
446 exit_code: Option<i32>,
447 stdout: Vec<u8>,
448 stderr: Vec<u8>,
449 stdout_truncated: bool,
450 stderr_truncated: bool,
451}
452
453impl CommandOutput {
454 #[must_use]
456 pub fn exited(exit_code: i32, stdout: impl Into<Vec<u8>>, stderr: impl Into<Vec<u8>>) -> Self {
457 Self {
458 completion: Completion::Exited,
459 exit_code: Some(exit_code),
460 stdout: stdout.into(),
461 stderr: stderr.into(),
462 stdout_truncated: false,
463 stderr_truncated: false,
464 }
465 }
466
467 #[must_use]
469 pub fn timed_out() -> Self {
470 Self {
471 completion: Completion::TimedOut,
472 exit_code: None,
473 stdout: Vec::new(),
474 stderr: Vec::new(),
475 stdout_truncated: false,
476 stderr_truncated: false,
477 }
478 }
479
480 #[must_use]
482 pub fn with_truncation(mut self, stdout: bool, stderr: bool) -> Self {
483 self.stdout_truncated = stdout;
484 self.stderr_truncated = stderr;
485 self
486 }
487
488 #[must_use]
490 pub fn completion(&self) -> Completion {
491 self.completion
492 }
493
494 #[must_use]
499 pub fn exit_code(&self) -> Option<i32> {
500 self.exit_code
501 }
502
503 #[must_use]
505 pub fn success(&self) -> bool {
506 self.completion == Completion::Exited && self.exit_code == Some(0)
507 }
508
509 #[must_use]
511 pub fn stdout(&self) -> &[u8] {
512 &self.stdout
513 }
514
515 #[must_use]
517 pub fn stderr(&self) -> &[u8] {
518 &self.stderr
519 }
520
521 #[must_use]
523 pub fn stdout_truncated(&self) -> bool {
524 self.stdout_truncated
525 }
526
527 #[must_use]
529 pub fn stderr_truncated(&self) -> bool {
530 self.stderr_truncated
531 }
532
533 #[must_use]
539 pub fn stdout_text(&self) -> String {
540 super::discovery::decode_console_output(&self.stdout)
541 .into_text()
542 .trim()
543 .to_string()
544 }
545
546 #[must_use]
548 pub fn stderr_text(&self) -> String {
549 super::discovery::decode_console_output(&self.stderr)
550 .into_text()
551 .trim()
552 .to_string()
553 }
554
555 #[must_use]
557 pub fn diagnostic(&self) -> String {
558 let stderr = self.stderr_text();
559 if !stderr.is_empty() {
560 return stderr;
561 }
562 let stdout = self.stdout_text();
563 if !stdout.is_empty() {
564 return stdout;
565 }
566 match self.completion {
567 Completion::Exited => match self.exit_code {
568 Some(code) => format!("it exited with status {code} and said nothing"),
569 None => "it was terminated and said nothing".to_string(),
570 },
571 Completion::TimedOut => "it did not finish before its deadline".to_string(),
572 Completion::Cancelled => "it was cancelled".to_string(),
573 }
574 }
575}
576
577pub trait CommandRunner: fmt::Debug + Send + Sync {
587 fn run(&self, request: &CommandRequest) -> Result<CommandOutput, WslError>;
598}
599
600#[derive(Debug, Clone, Copy, Default)]
606pub struct HostCommandRunner;
607
608impl CommandRunner for HostCommandRunner {
609 fn run(&self, request: &CommandRequest) -> Result<CommandOutput, WslError> {
610 request.refuse_payload_in_argv()?;
611
612 let mut command = Command::new(request.program());
615 command
616 .args(request.arguments())
617 .stdin(match request.input() {
618 ChildInput::Empty => Stdio::null(),
619 ChildInput::Piped(_) => Stdio::piped(),
620 })
621 .stdout(Stdio::piped())
622 .stderr(Stdio::piped());
623
624 let mut child = command.spawn().map_err(|source| WslError::Spawn {
625 program: request.program().to_path_buf(),
626 source,
627 })?;
628
629 let stdin = child.stdin.take();
630 let stdout = child
631 .stdout
632 .take()
633 .expect("stdout was piped when the child was configured");
634 let stderr = child
635 .stderr
636 .take()
637 .expect("stderr was piped when the child was configured");
638 let limits = request.limits();
639
640 let (waited, out, err) = std::thread::scope(|scope| {
644 let writer = scope.spawn(move || write_input(stdin, request.input()));
645 let out = scope.spawn(move || read_bounded(stdout, limits.stdout));
646 let err = scope.spawn(move || read_bounded(stderr, limits.stderr));
647 let waited = wait_for(&mut child, request.timeout(), request.cancellation());
648 drop(writer.join());
653 (
654 waited,
655 out.join().unwrap_or_else(|_| (Vec::new(), false)),
656 err.join().unwrap_or_else(|_| (Vec::new(), false)),
657 )
658 });
659
660 let (completion, exit_code) = waited.map_err(|source| WslError::ChildControl {
661 program: request.program().to_path_buf(),
662 source,
663 })?;
664
665 Ok(CommandOutput {
666 completion,
667 exit_code,
668 stdout: out.0,
669 stderr: err.0,
670 stdout_truncated: out.1,
671 stderr_truncated: err.1,
672 })
673 }
674}
675
676fn write_input(stdin: Option<std::process::ChildStdin>, input: &ChildInput) -> std::io::Result<()> {
678 let Some(mut pipe) = stdin else {
679 return Ok(());
680 };
681 if let Some(payload) = input.piped() {
682 pipe.write_all(payload.expose_bytes())?;
683 pipe.flush()?;
684 }
685 drop(pipe);
686 Ok(())
687}
688
689fn read_bounded(mut source: impl Read, limit: usize) -> (Vec<u8>, bool) {
695 let mut kept: Vec<u8> = Vec::new();
696 let mut truncated = false;
697 let mut buffer = [0_u8; 8192];
698 loop {
699 match source.read(&mut buffer) {
700 Ok(0) => break,
701 Ok(read) => {
702 let room = limit.saturating_sub(kept.len());
703 if room > 0 {
704 kept.extend_from_slice(&buffer[..read.min(room)]);
705 }
706 if read > room {
707 truncated = true;
708 }
709 }
710 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
711 Err(_) => break,
712 }
713 }
714 (kept, truncated)
715}
716
717fn wait_for(
719 child: &mut Child,
720 timeout: Duration,
721 cancellation: Option<&Cancellation>,
722) -> std::io::Result<(Completion, Option<i32>)> {
723 let deadline = Instant::now().checked_add(timeout);
724 loop {
725 if let Some(status) = child.try_wait()? {
726 return Ok((Completion::Exited, status.code()));
727 }
728 if cancellation.is_some_and(Cancellation::is_cancelled) {
729 child.kill()?;
730 let status = child.wait()?;
731 return Ok((Completion::Cancelled, status.code()));
732 }
733 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
734 child.kill()?;
735 let status = child.wait()?;
736 return Ok((Completion::TimedOut, status.code()));
737 }
738 std::thread::sleep(POLL_INTERVAL);
739 }
740}
741
742#[derive(Debug, Clone, PartialEq, Eq)]
753pub struct RecordedRequest {
754 pub program: PathBuf,
756 pub arguments: Vec<String>,
758 pub stdin: Vec<u8>,
760 pub timeout: Duration,
762}
763
764impl RecordedRequest {
765 #[must_use]
767 pub fn command_line(&self) -> String {
768 let mut line = self.program.to_string_lossy().into_owned();
769 for argument in &self.arguments {
770 line.push(' ');
771 line.push_str(argument);
772 }
773 line
774 }
775}
776
777#[derive(Debug, Default)]
784pub struct ScriptedRunner {
785 rules: Mutex<Vec<Rule>>,
786 recorded: Mutex<Vec<RecordedRequest>>,
787 default_response: Mutex<Option<CommandOutput>>,
788}
789
790#[derive(Debug)]
791struct Rule {
792 contains: String,
793 responses: Vec<CommandOutput>,
794 used: usize,
795}
796
797impl ScriptedRunner {
798 #[must_use]
800 pub fn new() -> Self {
801 Self::default()
802 }
803
804 #[must_use]
807 pub fn always(self, contains: &str, response: CommandOutput) -> Self {
808 self.push_rule(contains, vec![response]);
809 self
810 }
811
812 #[must_use]
815 pub fn sequence(self, contains: &str, responses: Vec<CommandOutput>) -> Self {
816 self.push_rule(contains, responses);
817 self
818 }
819
820 #[must_use]
822 pub fn otherwise(self, response: CommandOutput) -> Self {
823 *self
824 .default_response
825 .lock()
826 .expect("the scripted runner's response is not shared across a panic") = Some(response);
827 self
828 }
829
830 fn push_rule(&self, contains: &str, responses: Vec<CommandOutput>) {
831 self.rules
832 .lock()
833 .expect("the scripted runner's rules are not shared across a panic")
834 .push(Rule {
835 contains: contains.to_string(),
836 responses,
837 used: 0,
838 });
839 }
840
841 #[must_use]
843 pub fn recorded(&self) -> Vec<RecordedRequest> {
844 self.recorded
845 .lock()
846 .expect("the scripted runner's log is not shared across a panic")
847 .clone()
848 }
849
850 #[must_use]
852 pub fn call_count(&self) -> usize {
853 self.recorded
854 .lock()
855 .expect("the scripted runner's log is not shared across a panic")
856 .len()
857 }
858
859 #[must_use]
861 pub fn command_lines(&self) -> Vec<String> {
862 self.recorded()
863 .iter()
864 .map(RecordedRequest::command_line)
865 .collect()
866 }
867
868 #[must_use]
874 pub fn piped_input(&self) -> Vec<u8> {
875 let mut all = Vec::new();
876 for request in self.recorded() {
877 all.extend_from_slice(&request.stdin);
878 }
879 all
880 }
881}
882
883impl CommandRunner for ScriptedRunner {
884 fn run(&self, request: &CommandRequest) -> Result<CommandOutput, WslError> {
885 request.refuse_payload_in_argv()?;
886 let recorded = RecordedRequest {
887 program: request.program().to_path_buf(),
888 arguments: request.argument_strings(),
889 stdin: request
890 .input()
891 .piped()
892 .map(|input| input.expose_bytes().to_vec())
893 .unwrap_or_default(),
894 timeout: request.timeout(),
895 };
896 let line = recorded.command_line();
897 self.recorded
898 .lock()
899 .expect("the scripted runner's log is not shared across a panic")
900 .push(recorded);
901
902 let mut rules = self
903 .rules
904 .lock()
905 .expect("the scripted runner's rules are not shared across a panic");
906 for rule in rules.iter_mut() {
907 if line.contains(&rule.contains) {
908 let index = rule.used.min(rule.responses.len().saturating_sub(1));
909 rule.used += 1;
910 if let Some(response) = rule.responses.get(index) {
911 return Ok(response.clone());
912 }
913 }
914 }
915 drop(rules);
916
917 Ok(self
918 .default_response
919 .lock()
920 .expect("the scripted runner's response is not shared across a panic")
921 .clone()
922 .unwrap_or_else(|| CommandOutput::exited(0, Vec::new(), Vec::new())))
923 }
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929
930 fn canary() -> SecretString {
931 SecretString::from(format!("{}{}", "ghu_", "a1WslFixtureNotARealCredential00"))
932 }
933
934 #[test]
935 fn a_piped_payload_never_appears_in_debug_output() {
936 let secret = canary();
937 let request = CommandRequest::new("wsl.exe")
938 .arg("--distribution")
939 .arg("Ubuntu")
940 .with_input(ChildInput::Piped(PipedInput::from_secret_text(&secret)));
941
942 let printed = format!("{request:?}");
943 assert!(
944 !printed.contains(secret.expose_secret()),
945 "the payload reached Debug output: {printed}"
946 );
947 assert!(
948 printed.contains("<redacted; 36 bytes>"),
949 "the redacted form should still say how much there was: {printed}"
950 );
951 }
952
953 #[test]
954 fn a_payload_that_is_also_an_argument_refuses_to_launch() {
955 let secret = canary();
956 let request = CommandRequest::new("wsl.exe")
957 .arg("--exec")
958 .arg(format!("--token={}", secret.expose_secret()))
959 .with_input(ChildInput::Piped(PipedInput::from_secret_text(&secret)));
960
961 let error = request
962 .refuse_payload_in_argv()
963 .expect_err("the payload is in argument 1");
964 assert!(
965 matches!(&error, WslError::SecretInCommandLine { location, .. } if location == "argument 1"),
966 "unexpected error: {error:?}"
967 );
968 assert!(!error.to_string().contains(secret.expose_secret()));
970 }
971
972 #[test]
973 fn a_payload_that_is_only_on_stdin_is_allowed() {
974 let secret = canary();
975 let request = CommandRequest::new("wsl.exe")
976 .arg("--distribution")
977 .arg("Ubuntu")
978 .with_input(ChildInput::Piped(PipedInput::from_secret_text(&secret)));
979 request
980 .refuse_payload_in_argv()
981 .expect("stdin is the supported channel");
982 }
983
984 #[test]
985 fn a_payload_too_large_for_a_command_line_is_not_scanned() {
986 let request = CommandRequest::new("wsl.exe")
989 .arg("--exec")
990 .with_input(ChildInput::Piped(PipedInput::from_bytes(vec![
991 b'x';
992 ARGV_SCAN_LIMIT
993 + 1
994 ])));
995 request.refuse_payload_in_argv().expect("not scanned");
996 }
997
998 #[test]
999 fn arguments_are_kept_verbatim_and_never_joined() {
1000 let request = CommandRequest::new("wsl.exe")
1001 .arg("--distribution")
1002 .arg("Ubuntu & shutdown /s")
1003 .arg("--exec");
1004 assert_eq!(
1005 request.argument_strings(),
1006 vec![
1007 "--distribution".to_string(),
1008 "Ubuntu & shutdown /s".to_string(),
1009 "--exec".to_string(),
1010 ]
1011 );
1012 }
1013
1014 #[test]
1015 fn a_scripted_runner_answers_in_rule_order_and_records_stdin() {
1016 let secret = canary();
1017 let runner = ScriptedRunner::new()
1018 .always(
1019 "--version",
1020 CommandOutput::exited(0, "runner-manager 0.4.0", ""),
1021 )
1022 .otherwise(CommandOutput::exited(1, "", "no rule"));
1023
1024 let versioned = runner
1025 .run(&CommandRequest::new("wsl.exe").arg("--version"))
1026 .expect("scripted");
1027 assert_eq!(versioned.stdout_text(), "runner-manager 0.4.0");
1028
1029 let other = runner
1030 .run(
1031 &CommandRequest::new("wsl.exe")
1032 .arg("--exec")
1033 .with_input(ChildInput::Piped(PipedInput::from_secret_text(&secret))),
1034 )
1035 .expect("scripted");
1036 assert_eq!(other.exit_code(), Some(1));
1037
1038 assert_eq!(runner.call_count(), 2);
1039 assert_eq!(runner.piped_input(), secret.expose_secret().as_bytes());
1040 assert!(
1041 runner
1042 .command_lines()
1043 .iter()
1044 .all(|line| !line.contains(secret.expose_secret())),
1045 "the canary must not be in any recorded command line"
1046 );
1047 }
1048
1049 #[test]
1050 fn a_sequence_rule_advances_and_then_repeats_its_last_answer() {
1051 let runner = ScriptedRunner::new().sequence(
1052 "probe",
1053 vec![
1054 CommandOutput::exited(1, "", "not yet"),
1055 CommandOutput::exited(0, "ready", ""),
1056 ],
1057 );
1058 let first = runner.run(&CommandRequest::new("probe")).expect("scripted");
1059 let second = runner.run(&CommandRequest::new("probe")).expect("scripted");
1060 let third = runner.run(&CommandRequest::new("probe")).expect("scripted");
1061 assert_eq!(first.exit_code(), Some(1));
1062 assert_eq!(second.stdout_text(), "ready");
1063 assert_eq!(third.stdout_text(), "ready");
1064 }
1065
1066 #[test]
1067 fn output_is_bounded_but_the_stream_is_still_drained() {
1068 let (kept, truncated) = read_bounded(&b"0123456789"[..], 4);
1069 assert_eq!(kept, b"0123");
1070 assert!(truncated);
1071
1072 let (kept, truncated) = read_bounded(&b"012"[..], 4);
1073 assert_eq!(kept, b"012");
1074 assert!(!truncated);
1075 }
1076
1077 #[test]
1078 fn a_diagnostic_prefers_stderr_and_never_invents_one() {
1079 let output = CommandOutput::exited(2, "some stdout", "the real reason");
1080 assert_eq!(output.diagnostic(), "the real reason");
1081
1082 let output = CommandOutput::exited(2, "some stdout", "");
1083 assert_eq!(output.diagnostic(), "some stdout");
1084
1085 let output = CommandOutput::exited(2, "", "");
1086 assert_eq!(
1087 output.diagnostic(),
1088 "it exited with status 2 and said nothing"
1089 );
1090
1091 assert_eq!(
1092 CommandOutput::timed_out().diagnostic(),
1093 "it did not finish before its deadline"
1094 );
1095 }
1096
1097 #[test]
1098 fn cancellation_is_shared_by_every_clone() {
1099 let cancellation = Cancellation::new();
1100 let clone = cancellation.clone();
1101 assert!(!clone.is_cancelled());
1102 cancellation.cancel();
1103 assert!(clone.is_cancelled());
1104 }
1105
1106 fn this_test_binary() -> PathBuf {
1114 std::env::current_exe().expect("a test binary knows its own path")
1115 }
1116
1117 #[test]
1118 fn the_host_runner_captures_output_and_an_exit_code() {
1119 let output = HostCommandRunner
1120 .run(
1121 &CommandRequest::new(this_test_binary())
1122 .arg("--list")
1123 .with_timeout(Duration::from_secs(60)),
1124 )
1125 .expect("this binary can run itself");
1126 assert_eq!(output.completion(), Completion::Exited);
1127 assert_eq!(output.exit_code(), Some(0));
1128 assert!(
1129 output.stdout_text().contains("test"),
1130 "`--list` should name at least one test: {}",
1131 output.stdout_text()
1132 );
1133 }
1134
1135 #[test]
1136 fn the_host_runner_reports_a_program_that_is_not_there() {
1137 let error = HostCommandRunner
1138 .run(&CommandRequest::new(
1139 "runner-manager-a1-no-such-program-exists",
1140 ))
1141 .expect_err("there is no such program");
1142 assert!(matches!(error, WslError::Spawn { .. }), "{error:?}");
1143 }
1144
1145 #[test]
1146 fn the_host_runner_bounds_what_it_keeps() {
1147 let output = HostCommandRunner
1148 .run(
1149 &CommandRequest::new(this_test_binary())
1150 .arg("--list")
1151 .with_limits(OutputLimits {
1152 stdout: 8,
1153 stderr: 8,
1154 })
1155 .with_timeout(Duration::from_secs(60)),
1156 )
1157 .expect("this binary can run itself");
1158 assert!(output.stdout().len() <= 8);
1159 assert!(output.stdout_truncated());
1160 }
1161
1162 #[test]
1163 fn the_host_runner_kills_a_child_that_outlives_its_deadline() {
1164 let output = HostCommandRunner
1169 .run(
1170 &CommandRequest::new(this_test_binary())
1171 .arg("--exact")
1172 .arg("wsl::exec::tests::a_child_that_never_finishes")
1173 .arg("--ignored")
1174 .arg("--nocapture")
1175 .with_timeout(Duration::from_millis(300)),
1176 )
1177 .expect("this binary can run itself");
1178 assert_eq!(output.completion(), Completion::TimedOut);
1179 }
1180
1181 #[test]
1182 fn the_host_runner_kills_a_cancelled_child() {
1183 let cancellation = Cancellation::new();
1184 let flag = cancellation.clone();
1185 std::thread::spawn(move || {
1186 std::thread::sleep(Duration::from_millis(200));
1187 flag.cancel();
1188 });
1189 let output = HostCommandRunner
1190 .run(
1191 &CommandRequest::new(this_test_binary())
1192 .arg("--exact")
1193 .arg("wsl::exec::tests::a_child_that_never_finishes")
1194 .arg("--ignored")
1195 .arg("--nocapture")
1196 .with_timeout(Duration::from_secs(60))
1197 .with_cancellation(cancellation),
1198 )
1199 .expect("this binary can run itself");
1200 assert_eq!(output.completion(), Completion::Cancelled);
1201 }
1202
1203 #[test]
1204 fn the_host_runner_writes_stdin_and_the_child_reads_it() {
1205 let payload = b"a1-wsl-stdin-round-trip\n".to_vec();
1207 let output = HostCommandRunner
1208 .run(
1209 &CommandRequest::new(this_test_binary())
1210 .arg("--exact")
1211 .arg("wsl::exec::tests::a_child_that_echoes_its_stdin")
1212 .arg("--ignored")
1213 .arg("--nocapture")
1214 .with_input(ChildInput::Piped(PipedInput::from_bytes(payload)))
1215 .with_timeout(Duration::from_secs(60)),
1216 )
1217 .expect("this binary can run itself");
1218 assert!(
1219 output.stdout_text().contains("a1-wsl-stdin-round-trip"),
1220 "the child did not see the payload: {}",
1221 output.stdout_text()
1222 );
1223 }
1224
1225 #[test]
1232 #[ignore = "a helper child process, selected by name by the tests above"]
1233 fn a_child_that_never_finishes() {
1234 std::thread::sleep(Duration::from_secs(30));
1235 }
1236
1237 #[test]
1239 #[ignore = "a helper child process, selected by name by the test above"]
1240 fn a_child_that_echoes_its_stdin() {
1241 let mut text = String::new();
1242 std::io::Read::read_to_string(&mut std::io::stdin(), &mut text)
1243 .expect("the parent writes and closes the pipe");
1244 println!("{text}");
1245 }
1246}