1use crate::error::RunnerError;
2use std::ffi::OsStr;
3use std::process::Stdio;
4use std::time::Duration;
5
6use super::{CommandSpec, ProcessOutput, ProcessRunner};
7
8#[derive(Debug, Clone, Default)]
47pub struct WslRunner {
48 pub distro: Option<String>,
50}
51
52impl WslRunner {
53 #[must_use]
63 pub const fn new() -> Self {
64 Self { distro: None }
65 }
66
67 #[must_use]
81 pub fn with_distro(distro: impl Into<String>) -> Self {
82 Self {
83 distro: Some(distro.into()),
84 }
85 }
86
87 fn validate_argument(arg: &OsStr) -> Result<(), RunnerError> {
108 let arg_bytes = arg.as_encoded_bytes();
110 if arg_bytes.contains(&0) {
111 return Err(RunnerError::WslExecutionFailed {
112 reason: "Argument contains null byte which is not allowed".to_string(),
113 });
114 }
115 Ok(())
116 }
117
118 fn build_wsl_command(&self, cmd: &CommandSpec) -> Result<CommandSpec, RunnerError> {
138 Self::validate_argument(&cmd.program)?;
140 for arg in &cmd.args {
141 Self::validate_argument(arg)?;
142 }
143
144 let mut wsl_cmd = CommandSpec::new("wsl");
147
148 if let Some(ref distro) = self.distro {
150 wsl_cmd = wsl_cmd.arg("-d").arg(distro);
151 }
152
153 wsl_cmd = wsl_cmd.arg("--exec");
157
158 wsl_cmd = wsl_cmd.arg(&cmd.program);
160
161 for arg in &cmd.args {
164 wsl_cmd = wsl_cmd.arg(arg);
165 }
166
167 if let Some(ref cwd) = cmd.cwd {
169 wsl_cmd = wsl_cmd.cwd(cwd);
170 }
171
172 if let Some(ref env) = cmd.env {
174 for (key, value) in env {
175 wsl_cmd = wsl_cmd.env(key, value);
176 }
177 }
178
179 Ok(wsl_cmd)
180 }
181}
182
183impl ProcessRunner for WslRunner {
184 fn run(&self, cmd: &CommandSpec, timeout: Duration) -> Result<ProcessOutput, RunnerError> {
213 if !cfg!(target_os = "windows") {
215 return Err(RunnerError::WslNotAvailable {
216 reason: "WSL is only available on Windows".to_string(),
217 });
218 }
219
220 use std::sync::mpsc;
221 use std::thread;
222
223 let wsl_cmd = self.build_wsl_command(cmd)?;
225
226 let mut command = wsl_cmd.to_command();
228 command
229 .stdin(Stdio::null())
230 .stdout(Stdio::piped())
231 .stderr(Stdio::piped());
232
233 let child = command
235 .spawn()
236 .map_err(|e| RunnerError::WslExecutionFailed {
237 reason: format!(
238 "Failed to spawn WSL process for '{}': {}",
239 cmd.program.to_string_lossy(),
240 e
241 ),
242 })?;
243
244 let child_id = child.id();
246
247 let (tx, rx) = mpsc::channel();
249
250 let handle = thread::spawn(move || {
252 let output = child.wait_with_output();
253 let _ = tx.send(output);
254 });
255
256 match rx.recv_timeout(timeout) {
258 Ok(output_result) => {
259 let _ = handle.join();
261
262 let output = output_result.map_err(|e| RunnerError::WslExecutionFailed {
263 reason: format!("Failed to wait for WSL process: {e}"),
264 })?;
265
266 Ok(ProcessOutput::new(
267 output.stdout,
268 output.stderr,
269 output.status.code(),
270 false,
271 ))
272 }
273 Err(mpsc::RecvTimeoutError::Timeout) => {
274 Self::terminate_wsl_process(child_id);
276
277 let _ = handle.join();
279
280 Err(RunnerError::Timeout {
281 timeout_seconds: timeout.as_secs(),
282 })
283 }
284 Err(mpsc::RecvTimeoutError::Disconnected) => {
285 Err(RunnerError::WslExecutionFailed {
287 reason: "WSL process monitoring thread terminated unexpectedly".to_string(),
288 })
289 }
290 }
291 }
292}
293
294impl WslRunner {
295 fn terminate_wsl_process(pid: u32) {
300 #[cfg(windows)]
301 {
302 use windows::Win32::Foundation::CloseHandle;
303 use windows::Win32::System::Threading::{
304 OpenProcess, PROCESS_TERMINATE, TerminateProcess,
305 };
306
307 unsafe {
308 if let Ok(handle) = OpenProcess(PROCESS_TERMINATE, false, pid) {
309 let _ = TerminateProcess(handle, 1);
310 let _ = CloseHandle(handle);
311 }
312 }
313 }
314
315 #[cfg(not(windows))]
316 {
317 let _ = pid;
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use proptest::prelude::*;
327 use std::ffi::OsString;
328 use std::path::PathBuf;
329
330 #[test]
335 fn test_wsl_runner_new() {
336 let runner = WslRunner::new();
337 assert!(runner.distro.is_none());
338 }
339
340 #[test]
341 fn test_wsl_runner_with_distro() {
342 let runner = WslRunner::with_distro("Ubuntu-22.04");
343 assert_eq!(runner.distro, Some("Ubuntu-22.04".to_string()));
344 }
345
346 #[test]
347 fn test_wsl_runner_default() {
348 let runner = WslRunner::default();
349 assert!(runner.distro.is_none());
350 }
351
352 #[test]
353 fn test_wsl_runner_clone() {
354 let runner = WslRunner::with_distro("Ubuntu");
355 let cloned = runner.clone();
356 assert_eq!(cloned.distro, runner.distro);
357 }
358
359 #[test]
360 fn test_wsl_runner_implements_process_runner() {
361 fn assert_process_runner<T: ProcessRunner>(_: &T) {}
363
364 let runner = WslRunner::new();
365 assert_process_runner(&runner);
366 }
367
368 #[test]
369 fn test_wsl_runner_build_command_basic() {
370 let runner = WslRunner::new();
371 let cmd = CommandSpec::new("echo").arg("hello").arg("world");
372
373 let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
374
375 assert_eq!(wsl_cmd.program, OsString::from("wsl"));
377 assert_eq!(wsl_cmd.args.len(), 4);
378 assert_eq!(wsl_cmd.args[0], OsString::from("--exec"));
379 assert_eq!(wsl_cmd.args[1], OsString::from("echo"));
380 assert_eq!(wsl_cmd.args[2], OsString::from("hello"));
381 assert_eq!(wsl_cmd.args[3], OsString::from("world"));
382 }
383
384 #[test]
385 fn test_wsl_runner_build_command_with_distro() {
386 let runner = WslRunner::with_distro("Ubuntu-22.04");
387 let cmd = CommandSpec::new("echo").arg("test");
388
389 let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
390
391 assert_eq!(wsl_cmd.program, OsString::from("wsl"));
393 assert_eq!(wsl_cmd.args.len(), 5);
394 assert_eq!(wsl_cmd.args[0], OsString::from("-d"));
395 assert_eq!(wsl_cmd.args[1], OsString::from("Ubuntu-22.04"));
396 assert_eq!(wsl_cmd.args[2], OsString::from("--exec"));
397 assert_eq!(wsl_cmd.args[3], OsString::from("echo"));
398 assert_eq!(wsl_cmd.args[4], OsString::from("test"));
399 }
400
401 #[test]
402 fn test_wsl_runner_build_command_preserves_cwd() {
403 let runner = WslRunner::new();
404 let cmd = CommandSpec::new("ls").cwd("/home/user");
405
406 let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
407
408 assert_eq!(wsl_cmd.cwd, Some(PathBuf::from("/home/user")));
409 }
410
411 #[test]
412 fn test_wsl_runner_build_command_preserves_env() {
413 let runner = WslRunner::new();
414 let cmd = CommandSpec::new("env").env("MY_VAR", "my_value");
415
416 let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
417
418 let env = wsl_cmd.env.as_ref().unwrap();
419 assert_eq!(
420 env.get(&OsString::from("MY_VAR")),
421 Some(&OsString::from("my_value"))
422 );
423 }
424
425 #[test]
426 fn test_wsl_runner_build_command_shell_metacharacters_preserved() {
427 let runner = WslRunner::new();
430 let cmd = CommandSpec::new("echo")
431 .arg("$(whoami)")
432 .arg("`id`")
433 .arg("${HOME}")
434 .arg("$PATH")
435 .arg("arg;with;semicolons")
436 .arg("arg|with|pipes")
437 .arg("arg&with&ersands");
438
439 let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
440
441 assert_eq!(wsl_cmd.args[2], OsString::from("$(whoami)"));
444 assert_eq!(wsl_cmd.args[3], OsString::from("`id`"));
445 assert_eq!(wsl_cmd.args[4], OsString::from("${HOME}"));
446 assert_eq!(wsl_cmd.args[5], OsString::from("$PATH"));
447 assert_eq!(wsl_cmd.args[6], OsString::from("arg;with;semicolons"));
448 assert_eq!(wsl_cmd.args[7], OsString::from("arg|with|pipes"));
449 assert_eq!(wsl_cmd.args[8], OsString::from("arg&with&ersands"));
450 }
451
452 #[test]
453 fn test_wsl_runner_validate_argument_rejects_null_bytes() {
454 let arg_with_null = OsString::from("hello\0world");
456 let result = WslRunner::validate_argument(&arg_with_null);
457
458 assert!(result.is_err());
459 match result {
460 Err(RunnerError::WslExecutionFailed { reason }) => {
461 assert!(reason.contains("null byte"));
462 }
463 _ => panic!("Expected WslExecutionFailed error"),
464 }
465 }
466
467 #[test]
468 fn test_wsl_runner_validate_argument_accepts_valid_args() {
469 let valid_args = [
471 "simple",
472 "with spaces",
473 "with-dashes",
474 "with_underscores",
475 "with.dots",
476 "/path/to/file",
477 "C:\\Windows\\Path",
478 "unicode: 日本語",
479 "emoji: 🎉",
480 "--flag=value",
481 "-v",
482 "$(not-expanded)",
483 "`backticks`",
484 "${variable}",
485 ];
486
487 for arg in valid_args {
488 let os_arg = OsString::from(arg);
489 let result = WslRunner::validate_argument(&os_arg);
490 assert!(result.is_ok(), "Argument '{}' should be valid", arg);
491 }
492 }
493
494 #[test]
495 fn test_wsl_runner_build_command_rejects_null_in_program() {
496 let runner = WslRunner::new();
497 let cmd = CommandSpec::new("echo\0bad");
498
499 let result = runner.build_wsl_command(&cmd);
500
501 assert!(result.is_err());
502 match result {
503 Err(RunnerError::WslExecutionFailed { reason }) => {
504 assert!(reason.contains("null byte"));
505 }
506 _ => panic!("Expected WslExecutionFailed error"),
507 }
508 }
509
510 #[test]
511 fn test_wsl_runner_build_command_rejects_null_in_args() {
512 let runner = WslRunner::new();
513 let cmd = CommandSpec::new("echo")
514 .arg("valid")
515 .arg("has\0null")
516 .arg("also valid");
517
518 let result = runner.build_wsl_command(&cmd);
519
520 assert!(result.is_err());
521 match result {
522 Err(RunnerError::WslExecutionFailed { reason }) => {
523 assert!(reason.contains("null byte"));
524 }
525 _ => panic!("Expected WslExecutionFailed error"),
526 }
527 }
528
529 #[cfg(not(target_os = "windows"))]
530 #[test]
531 fn test_wsl_runner_returns_error_on_non_windows() {
532 let runner = WslRunner::new();
534 let cmd = CommandSpec::new("echo").arg("test");
535
536 let result = runner.run(&cmd, Duration::from_secs(10));
537
538 assert!(result.is_err());
539 match result {
540 Err(RunnerError::WslNotAvailable { reason }) => {
541 assert!(reason.contains("only available on Windows"));
542 }
543 _ => panic!("Expected WslNotAvailable error"),
544 }
545 }
546
547 #[test]
548 fn test_wsl_runner_no_string_concatenation() {
549 let runner = WslRunner::with_distro("TestDistro");
552 let cmd = CommandSpec::new("program")
553 .arg("arg1")
554 .arg("arg2 with spaces")
555 .arg("arg3;semicolon");
556
557 let wsl_cmd = runner.build_wsl_command(&cmd).unwrap();
558
559 assert_eq!(wsl_cmd.args.len(), 7);
565
566 assert_eq!(wsl_cmd.args[4], OsString::from("arg1"));
568 assert_eq!(wsl_cmd.args[5], OsString::from("arg2 with spaces"));
569 assert_eq!(wsl_cmd.args[6], OsString::from("arg3;semicolon"));
570 }
571
572 #[test]
573 fn test_wsl_runner_command_construction() {
574 let runner = WslRunner::new();
578 let cmd = CommandSpec::new("echo").arg("hello").arg("world");
579
580 let wsl_cmd = runner
581 .build_wsl_command(&cmd)
582 .expect("Failed to build WSL command");
583
584 assert_eq!(wsl_cmd.program, OsString::from("wsl"));
586
587 let args: Vec<String> = wsl_cmd
589 .args
590 .iter()
591 .map(|s| s.to_string_lossy().to_string())
592 .collect();
593
594 assert_eq!(args[0], "--exec");
595 assert_eq!(args[1], "echo");
596 assert_eq!(args[2], "hello");
597 assert_eq!(args[3], "world");
598
599 for arg in &args {
601 assert!(!arg.contains("sh -c"));
602 assert!(!arg.contains("cmd /C"));
603 }
604 }
605
606 #[test]
607 fn test_wsl_runner_with_distro_command_construction() {
608 let runner = WslRunner::with_distro("Ubuntu-22.04");
609 let cmd = CommandSpec::new("ls").arg("-la");
610
611 let wsl_cmd = runner
612 .build_wsl_command(&cmd)
613 .expect("Failed to build WSL command");
614
615 let args: Vec<String> = wsl_cmd
616 .args
617 .iter()
618 .map(|s| s.to_string_lossy().to_string())
619 .collect();
620
621 assert_eq!(args[0], "-d");
623 assert_eq!(args[1], "Ubuntu-22.04");
624 assert_eq!(args[2], "--exec");
625 assert_eq!(args[3], "ls");
626 assert_eq!(args[4], "-la");
627 }
628
629 #[test]
630 fn test_wsl_runner_argument_validation() {
631 let runner = WslRunner::new();
633
634 let cmd = CommandSpec::new("echo").arg("hello\0world");
636
637 let result = runner.build_wsl_command(&cmd);
638 assert!(result.is_err());
639
640 if let Err(RunnerError::WslExecutionFailed { reason }) = result {
641 assert!(reason.contains("null byte"));
642 } else {
643 panic!("Expected WslExecutionFailed error");
644 }
645 }
646
647 proptest! {
648 #![proptest_config(ProptestConfig::with_cases(100))]
649 #[test]
650 fn test_wsl_runner_safety_property(
651 program in any::<String>(),
652 args in prop::collection::vec(any::<String>(), 0..10),
653 distro in prop::option::of(any::<String>())
654 ) {
655 let mut runner = WslRunner::new();
659 if let Some(ref d) = distro {
660 runner = WslRunner::with_distro(d.clone());
661 }
662
663 let mut cmd = CommandSpec::new(&program);
664 for arg in &args {
665 cmd = cmd.arg(arg);
666 }
667
668 let result = runner.build_wsl_command(&cmd);
669
670 let has_null = program.contains('\0') || args.iter().any(|a| a.contains('\0'));
672
673 if has_null {
674 prop_assert!(result.is_err());
676 } else {
677 prop_assert!(result.is_ok());
679 let wsl_cmd = result.unwrap();
680
681 prop_assert_eq!(wsl_cmd.program, OsString::from("wsl"));
683
684 let mut expected_args_len = 1; let mut arg_idx = 0;
686
687 if let Some(ref d) = runner.distro {
689 prop_assert_eq!(&wsl_cmd.args[arg_idx], &OsString::from("-d"));
690 prop_assert_eq!(&wsl_cmd.args[arg_idx+1], &OsString::from(d));
691 arg_idx += 2;
692 expected_args_len += 2;
693 }
694
695 prop_assert_eq!(&wsl_cmd.args[arg_idx], &OsString::from("--exec"));
697 arg_idx += 1;
698
699 prop_assert_eq!(&wsl_cmd.args[arg_idx], &OsString::from(&program));
701 arg_idx += 1;
702 expected_args_len += 1;
703
704 for (i, arg) in args.iter().enumerate() {
706 prop_assert_eq!(&wsl_cmd.args[arg_idx + i], &OsString::from(arg));
707 }
708 expected_args_len += args.len();
709
710 prop_assert_eq!(wsl_cmd.args.len(), expected_args_len);
711 }
712 }
713 }
714}