1use std::collections::VecDeque;
19use std::ffi::{OsStr, OsString};
20use std::io::{self, Cursor, ErrorKind, Read, Write};
21use std::path::{Path, PathBuf};
22use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus, Stdio};
23use std::sync::{Arc, Mutex};
24use thiserror::Error;
25
26use crate::environment::Environment;
27
28#[derive(Debug, Error)]
30#[error("command handle is not backed by a live OS process")]
31pub struct CommandHandleError;
32
33#[derive(Debug, Clone, Default)]
35pub struct CommandEnvironment {
36 pub inherit_process: bool,
38 pub set: Vec<(OsString, OsString)>,
40 pub remove: Vec<OsString>,
42}
43
44impl CommandEnvironment {
45 #[must_use]
47 pub fn inherit_process_only() -> Self {
48 Self {
49 inherit_process: true,
50 set: Vec::new(),
51 remove: Vec::new(),
52 }
53 }
54
55 #[must_use]
60 pub fn from_repository_environment(env: &Environment, extra: &[(String, String)]) -> Self {
61 let mut set = env.subprocess_environment();
62 for (k, v) in extra {
63 let key = OsString::from(k.as_str());
64 let val = OsString::from(v.as_str());
65 if let Some(slot) = set.iter_mut().find(|(ek, _)| ek == &key) {
66 slot.1 = val;
67 } else {
68 set.push((key, val));
69 }
70 }
71 Self {
72 inherit_process: false,
73 set,
74 remove: Vec::new(),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum CommandStdio {
82 #[default]
83 Inherit,
85 Null,
87 Pipe,
89 ParentStderr,
91}
92
93#[derive(Debug, Clone, Default)]
95pub enum CommandStdin {
96 #[default]
97 Inherit,
99 Null,
101 Pipe(Vec<u8>),
103}
104
105#[derive(Debug, Clone)]
107pub enum ShellInvocation {
108 DashC {
110 script: String,
112 argv0: Option<String>,
114 args: Vec<OsString>,
116 },
117 ScriptPath {
119 script_path: PathBuf,
121 args: Vec<OsString>,
123 },
124}
125
126#[derive(Debug, Clone)]
128pub struct CommandSpec {
129 pub program: PathBuf,
136 pub args: Vec<OsString>,
138 pub shell: Option<ShellInvocation>,
140 pub cwd: Option<PathBuf>,
142 pub env: CommandEnvironment,
144 pub stdin: CommandStdin,
146 pub stdout: CommandStdio,
148 pub stderr: CommandStdio,
150}
151
152impl CommandSpec {
153 #[must_use]
155 pub fn sh_dash_c_filter(script: impl Into<String>) -> Self {
156 Self {
157 program: PathBuf::from("sh"),
158 args: Vec::new(),
159 shell: Some(ShellInvocation::DashC {
160 script: script.into(),
161 argv0: None,
162 args: Vec::new(),
163 }),
164 cwd: None,
165 env: CommandEnvironment::inherit_process_only(),
166 stdin: CommandStdin::Pipe(Vec::new()),
167 stdout: CommandStdio::Pipe,
168 stderr: CommandStdio::Inherit,
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct CommandExit {
176 pub code: Option<i32>,
178 pub success: bool,
180}
181
182impl From<ExitStatus> for CommandExit {
183 fn from(status: ExitStatus) -> Self {
184 Self {
185 code: status.code(),
186 success: status.success(),
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct CommandOutput {
194 pub status: CommandExit,
196 pub stdout: Vec<u8>,
198 pub stderr: Vec<u8>,
200}
201
202enum RunningCommandInner {
203 System {
204 child: Child,
205 stdin: Option<ChildStdin>,
206 stdout: Option<ChildStdout>,
207 stderr: Option<ChildStderr>,
208 kill_process_group: bool,
209 },
210 Recording {
211 state: Arc<Mutex<RecordingState>>,
212 stdin: Option<RecordingStdinBuffer>,
213 },
214}
215
216struct RecordingStdinBuffer(Arc<Mutex<Vec<u8>>>);
217
218impl Write for RecordingStdinBuffer {
219 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
220 self.0.lock().unwrap_or_else(|e| e.into_inner()).write(buf)
221 }
222
223 fn flush(&mut self) -> io::Result<()> {
224 self.0.lock().unwrap_or_else(|e| e.into_inner()).flush()
225 }
226}
227
228struct RecordingState {
229 stdout: Option<Cursor<Vec<u8>>>,
230 stderr: Vec<u8>,
231 exit: CommandExit,
232}
233
234pub struct RunningCommand {
236 inner: RunningCommandInner,
237}
238
239pub(crate) enum ConcurrentStdinFeed<'a> {
241 Bytes(&'a [u8]),
243 File(std::fs::File),
245}
246
247impl RunningCommand {
248 #[must_use]
250 pub fn from_system(
251 child: Child,
252 stdin: Option<ChildStdin>,
253 stdout: Option<ChildStdout>,
254 stderr: Option<ChildStderr>,
255 kill_process_group: bool,
256 ) -> Self {
257 Self {
258 inner: RunningCommandInner::System {
259 child,
260 stdin,
261 stdout,
262 stderr,
263 kill_process_group,
264 },
265 }
266 }
267
268 #[must_use]
270 pub fn from_recording(
271 exit: CommandExit,
272 stdout: Option<Vec<u8>>,
273 stderr: Vec<u8>,
274 piped_stdin: bool,
275 ) -> Self {
276 Self {
277 inner: RunningCommandInner::Recording {
278 state: Arc::new(Mutex::new(RecordingState {
279 stdout: stdout.map(Cursor::new),
280 stderr,
281 exit,
282 })),
283 stdin: piped_stdin.then(|| RecordingStdinBuffer(Arc::new(Mutex::new(Vec::new())))),
284 },
285 }
286 }
287
288 pub fn stdin_mut(&mut self) -> Option<&mut dyn Write> {
290 match &mut self.inner {
291 RunningCommandInner::System { stdin, .. } => {
292 stdin.as_mut().map(|s| s as &mut dyn Write)
293 }
294 RunningCommandInner::Recording { stdin, .. } => {
295 stdin.as_mut().map(|s| s as &mut dyn Write)
296 }
297 }
298 }
299
300 pub fn close_stdin(&mut self) {
302 match &mut self.inner {
303 RunningCommandInner::System { stdin, .. } => *stdin = None,
304 RunningCommandInner::Recording { stdin, .. } => *stdin = None,
305 }
306 }
307
308 #[must_use]
310 pub fn child_pid(&self) -> Option<u32> {
311 match &self.inner {
312 RunningCommandInner::System { child, .. } => Some(child.id()),
313 RunningCommandInner::Recording { .. } => None,
314 }
315 }
316
317 #[must_use]
319 pub(crate) fn kill_process_group(&self) -> bool {
320 match &self.inner {
321 RunningCommandInner::System {
322 kill_process_group, ..
323 } => *kill_process_group,
324 RunningCommandInner::Recording { .. } => false,
325 }
326 }
327
328 pub fn try_wait(&mut self) -> io::Result<Option<CommandExit>> {
334 match &mut self.inner {
335 RunningCommandInner::System { child, .. } => {
336 child.try_wait().map(|opt| opt.map(Into::into))
337 }
338 RunningCommandInner::Recording { state, .. } => {
339 let guard = state.lock().unwrap_or_else(|e| e.into_inner());
340 Ok(Some(guard.exit))
341 }
342 }
343 }
344
345 pub fn kill(&mut self) -> io::Result<()> {
351 match &mut self.inner {
352 RunningCommandInner::System { child, .. } => child.kill(),
353 RunningCommandInner::Recording { .. } => Ok(()),
354 }
355 }
356 pub fn wait(self) -> io::Result<CommandExit> {
362 match self.inner {
363 RunningCommandInner::System { mut child, .. } => child.wait().map(Into::into),
364 RunningCommandInner::Recording { state, .. } => {
365 let guard = state.lock().unwrap_or_else(|e| e.into_inner());
366 Ok(guard.exit)
367 }
368 }
369 }
370
371 pub(crate) fn feed_stdin_and_wait_with_output(
382 self,
383 stdin_feed: Option<ConcurrentStdinFeed<'_>>,
384 ) -> io::Result<CommandOutput> {
385 match self.inner {
386 RunningCommandInner::System {
387 mut child,
388 stdin,
389 stdout,
390 stderr,
391 kill_process_group: _,
392 } => std::thread::scope(|scope| {
393 let drain = scope.spawn(move || drain_child_pipes(stdout, stderr));
394
395 if let Some(mut stdin_pipe) = stdin {
396 if let Some(feed) = stdin_feed {
397 let _ = scope.spawn(move || {
398 write_stdin_feed(feed, &mut stdin_pipe);
399 drop(stdin_pipe);
400 });
401 } else {
402 drop(stdin_pipe);
403 }
404 }
405
406 let (stdout_bytes, stderr_bytes) =
407 drain.join().unwrap_or(Ok((Vec::new(), Vec::new())))?;
408 let status = child.wait()?.into();
409 Ok(CommandOutput {
410 status,
411 stdout: stdout_bytes,
412 stderr: stderr_bytes,
413 })
414 }),
415 RunningCommandInner::Recording { state, mut stdin } => {
416 if let (Some(stdin_mut), Some(feed)) = (stdin.as_mut(), stdin_feed) {
417 write_stdin_feed(feed, stdin_mut);
418 }
419 drop(stdin);
420 let mut guard = state.lock().unwrap_or_else(|e| e.into_inner());
421 let mut stdout_bytes = Vec::new();
422 if let Some(cursor) = &mut guard.stdout {
423 let _ = cursor.read_to_end(&mut stdout_bytes);
424 }
425 Ok(CommandOutput {
426 status: guard.exit,
427 stdout: stdout_bytes,
428 stderr: guard.stderr.clone(),
429 })
430 }
431 }
432 }
433
434 pub fn wait_with_output(self) -> io::Result<CommandOutput> {
440 match self.inner {
441 RunningCommandInner::System {
442 mut child,
443 stdout,
444 stderr,
445 ..
446 } => {
447 let (stdout_bytes, stderr_bytes) = drain_child_pipes(stdout, stderr)?;
448 let status = child.wait()?.into();
449 Ok(CommandOutput {
450 status,
451 stdout: stdout_bytes,
452 stderr: stderr_bytes,
453 })
454 }
455 RunningCommandInner::Recording { state, .. } => {
456 let mut guard = state.lock().unwrap_or_else(|e| e.into_inner());
457 let mut stdout_bytes = Vec::new();
458 if let Some(cursor) = &mut guard.stdout {
459 let _ = cursor.read_to_end(&mut stdout_bytes);
460 }
461 Ok(CommandOutput {
462 status: guard.exit,
463 stdout: stdout_bytes,
464 stderr: guard.stderr.clone(),
465 })
466 }
467 }
468 }
469
470 #[expect(clippy::type_complexity)]
476 pub fn into_system_parts(
477 self,
478 ) -> Result<
479 (
480 Child,
481 Option<ChildStdin>,
482 Option<ChildStdout>,
483 Option<ChildStderr>,
484 ),
485 CommandHandleError,
486 > {
487 match self.inner {
488 RunningCommandInner::System {
489 child,
490 stdin,
491 stdout,
492 stderr,
493 kill_process_group: _,
494 } => Ok((child, stdin, stdout, stderr)),
495 RunningCommandInner::Recording { .. } => Err(CommandHandleError),
496 }
497 }
498}
499
500fn write_stdin_feed(feed: ConcurrentStdinFeed<'_>, stdin: &mut dyn Write) {
501 let write_result = match feed {
502 ConcurrentStdinFeed::Bytes(data) => stdin.write_all(data).map(|_| ()),
503 ConcurrentStdinFeed::File(mut file) => io::copy(&mut file, stdin).map(|_| ()),
504 };
505 if let Err(e) = write_result {
506 if e.kind() != ErrorKind::BrokenPipe {
507 let _ = e;
508 }
509 }
510}
511
512fn drain_child_pipes(
513 stdout: Option<ChildStdout>,
514 stderr: Option<ChildStderr>,
515) -> io::Result<(Vec<u8>, Vec<u8>)> {
516 std::thread::scope(|scope| {
517 let out_task = stdout.map(|mut out| {
518 scope.spawn(move || {
519 let mut buf = Vec::new();
520 let _ = out.read_to_end(&mut buf);
521 buf
522 })
523 });
524 let err_task = stderr.map(|mut err| {
525 scope.spawn(move || {
526 let mut buf = Vec::new();
527 let _ = err.read_to_end(&mut buf);
528 buf
529 })
530 });
531 let stdout_bytes = out_task
532 .map(|h| h.join().unwrap_or_default())
533 .unwrap_or_default();
534 let stderr_bytes = err_task
535 .map(|h| h.join().unwrap_or_default())
536 .unwrap_or_default();
537 Ok((stdout_bytes, stderr_bytes))
538 })
539}
540
541pub trait CommandRunner: Send + Sync {
543 fn spawn(&self, spec: &CommandSpec) -> io::Result<RunningCommand>;
549
550 fn spawn_server_receive_hook(&self, spec: &CommandSpec) -> io::Result<RunningCommand> {
554 self.spawn(spec)
555 }
556}
557
558#[derive(Debug, Clone, Default)]
560pub struct SystemCommandRunner;
561
562impl SystemCommandRunner {
563 fn build_command(spec: &CommandSpec) -> Command {
565 let program = if spec.program.as_os_str() == SHELL_NAME {
566 shell_program(&spec.env)
567 } else {
568 spec.program.clone()
569 };
570 let mut cmd = match &spec.shell {
571 None => {
572 let mut c = Command::new(&program); c.args(&spec.args);
574 c
575 }
576 Some(ShellInvocation::DashC {
577 script,
578 argv0,
579 args,
580 }) => {
581 let mut c = Command::new(&program); c.arg("-c").arg(script);
583 if let Some(name) = argv0 {
584 c.arg(name);
585 }
586 c.args(args);
587 c
588 }
589 Some(ShellInvocation::ScriptPath { script_path, args }) => {
590 let mut c = Command::new(&program); c.arg(script_path);
592 c.args(args);
593 c
594 }
595 };
596
597 if let Some(cwd) = &spec.cwd {
598 cmd.current_dir(cwd);
599 }
600
601 apply_env(&mut cmd, &spec.env);
602 apply_stdio(&mut cmd, spec);
603 cmd
604 }
605}
606
607fn apply_env(cmd: &mut Command, env: &CommandEnvironment) {
608 if !env.inherit_process {
609 cmd.env_clear();
610 }
611 for key in &env.remove {
612 cmd.env_remove(key);
613 }
614 for (k, v) in &env.set {
615 cmd.env(k, v);
616 }
617}
618
619fn apply_stdio(cmd: &mut Command, spec: &CommandSpec) {
620 cmd.stdin(match spec.stdin {
621 CommandStdin::Inherit => Stdio::inherit(),
622 CommandStdin::Null => Stdio::null(),
623 CommandStdin::Pipe(_) => Stdio::piped(),
624 });
625 cmd.stdout(child_stdio(spec.stdout));
626 cmd.stderr(child_stdio(spec.stderr));
627}
628
629fn child_stdio(mode: CommandStdio) -> Stdio {
630 match mode {
631 CommandStdio::Inherit => Stdio::inherit(),
632 CommandStdio::Null => Stdio::null(),
633 CommandStdio::Pipe => Stdio::piped(),
634 CommandStdio::ParentStderr => std::io::stderr().into(),
635 }
636}
637
638const SHELL_NAME: &str = "sh";
640
641pub(crate) fn child_path_var(env: &CommandEnvironment) -> Option<OsString> {
644 env.set
645 .iter()
646 .rev()
647 .find(|(k, _)| is_path_var(k))
648 .map(|(_, v)| v.clone())
649 .or_else(|| {
650 env.inherit_process
651 .then(|| std::env::var_os("PATH"))
653 .flatten()
654 })
655}
656
657pub(crate) fn shell_program(env: &CommandEnvironment) -> PathBuf {
662 shell_interpreter(env).program
663}
664
665pub(crate) fn shell_interpreter(env: &CommandEnvironment) -> ScriptInterpreter {
667 if cfg!(windows) {
668 resolve_script_interpreter(SHELL_NAME, child_path_var(env).as_deref())
669 } else {
670 ScriptInterpreter::bare("/bin/sh")
671 }
672}
673
674fn find_program_in_dirs(name: &str, dirs: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
676 let suffix = std::env::consts::EXE_SUFFIX;
677 dirs.into_iter()
678 .filter(|dir| !dir.as_os_str().is_empty())
679 .find_map(|dir| {
680 let with_suffix = (!suffix.is_empty() && !name.ends_with(suffix))
681 .then(|| dir.join(format!("{name}{suffix}")));
682 with_suffix
683 .into_iter()
684 .chain(std::iter::once(dir.join(name)))
685 .find(|candidate| candidate.is_file())
686 })
687}
688
689fn find_program_in_path(name: &str, path_var: &OsStr) -> Option<PathBuf> {
691 find_program_in_dirs(name, std::env::split_paths(path_var))
692}
693
694fn is_wsl_launcher_dir(dir: &Path) -> bool {
697 dir.file_name().and_then(|n| n.to_str()).is_some_and(|n| {
698 n.eq_ignore_ascii_case("System32") || n.eq_ignore_ascii_case("WindowsApps")
699 })
700}
701
702#[derive(Debug, Clone, PartialEq, Eq)]
704pub(crate) struct ScriptInterpreter {
705 pub(crate) program: PathBuf,
706 pub(crate) git_root: Option<PathBuf>,
708}
709
710impl ScriptInterpreter {
711 pub(crate) fn bare(program: impl Into<PathBuf>) -> Self {
712 Self {
713 program: program.into(),
714 git_root: None,
715 }
716 }
717}
718
719pub(crate) fn resolve_script_interpreter(
726 name: &str,
727 path_var: Option<&OsStr>,
728) -> ScriptInterpreter {
729 let Some(path_var) = path_var else {
730 return ScriptInterpreter::bare(name);
731 };
732 let in_git_tree = || {
733 let git = find_program_in_path("git", path_var)?;
734 git.parent()?.ancestors().skip(1).take(2).find_map(|root| {
735 let program =
736 find_program_in_dirs(name, [root.join("usr").join("bin"), root.join("bin")])?;
737 Some(ScriptInterpreter {
738 program,
739 git_root: Some(root.to_path_buf()),
740 })
741 })
742 };
743 in_git_tree()
744 .or_else(|| {
745 find_program_in_dirs(
746 name,
747 std::env::split_paths(path_var).filter(|dir| !is_wsl_launcher_dir(dir)),
748 )
749 .map(ScriptInterpreter::bare)
750 })
751 .unwrap_or_else(|| ScriptInterpreter::bare(name))
752}
753
754pub(crate) fn git_for_windows_tool_dirs(root: &Path) -> Vec<PathBuf> {
759 const MINGW_PREFIXES: [&str; 4] = ["mingw64", "clangarm64", "ucrt64", "mingw32"];
760 let mingw_bin = MINGW_PREFIXES
761 .iter()
762 .map(|prefix| root.join(prefix).join("bin"))
763 .find(|dir| dir.is_dir());
764 mingw_bin
765 .into_iter()
766 .chain(std::iter::once(root.join("usr").join("bin")))
767 .filter(|dir| dir.is_dir())
768 .collect()
769}
770
771pub(crate) fn prepend_to_child_path(env: &mut CommandEnvironment, dirs: &[PathBuf]) {
776 if dirs.is_empty() {
777 return;
778 }
779 let Ok(mut path) = std::env::join_paths(dirs) else {
780 return;
781 };
782 if let Some(current) = child_path_var(env).filter(|p| !p.is_empty()) {
783 path.push(if cfg!(windows) { ";" } else { ":" });
784 path.push(current);
785 }
786 match env.set.iter_mut().rev().find(|(k, _)| is_path_var(k)) {
787 Some((_, value)) => *value = path,
788 None => env.set.push((OsString::from("PATH"), path)),
789 }
790}
791
792fn is_path_var(key: &OsStr) -> bool {
793 if cfg!(windows) {
794 key.eq_ignore_ascii_case("PATH")
795 } else {
796 key == "PATH"
797 }
798}
799
800impl SystemCommandRunner {
801 fn spawn_with_process_group(
802 &self,
803 spec: &CommandSpec,
804 isolate_process_group: bool,
805 ) -> io::Result<RunningCommand> {
806 let mut cmd = Self::build_command(spec);
807 if isolate_process_group {
808 #[cfg(unix)]
809 {
810 use std::os::unix::process::CommandExt;
811 unsafe {
812 cmd.pre_exec(|| {
813 if libc::setpgid(0, 0) != 0 {
815 return Err(io::Error::last_os_error());
816 }
817 Ok(())
818 });
819 }
820 }
821 }
822 let kill_process_group = isolate_process_group;
823 let mut child = cmd.spawn()?;
824
825 let mut stdin = child.stdin.take();
826 if let CommandStdin::Pipe(ref bytes) = spec.stdin {
827 if let Some(ref mut stdin_handle) = stdin {
828 if !bytes.is_empty() {
829 let _ = stdin_handle.write_all(bytes);
830 stdin = None;
832 }
833 }
834 }
835
836 let stdout = child.stdout.take();
837 let stderr = child.stderr.take();
838 Ok(RunningCommand::from_system(
839 child,
840 stdin,
841 stdout,
842 stderr,
843 kill_process_group,
844 ))
845 }
846}
847
848impl CommandRunner for SystemCommandRunner {
849 fn spawn(&self, spec: &CommandSpec) -> io::Result<RunningCommand> {
850 self.spawn_with_process_group(spec, false)
851 }
852
853 fn spawn_server_receive_hook(&self, spec: &CommandSpec) -> io::Result<RunningCommand> {
854 self.spawn_with_process_group(spec, true)
855 }
856}
857
858pub(crate) fn force_kill_process_pid(pid: u32, kill_process_group: bool) {
860 #[cfg(unix)]
861 {
862 if let Ok(pid_t) = libc::pid_t::try_from(pid) {
863 unsafe {
865 if kill_process_group {
866 let pgid = libc::getpgid(pid_t);
867 if pgid > 0 {
868 libc::kill(-pgid, libc::SIGKILL);
869 return;
870 }
871 }
872 libc::kill(pid_t, libc::SIGKILL);
873 }
874 }
875 }
876 #[cfg(windows)]
877 {
878 let mut args = vec![
879 OsString::from("/F"),
880 OsString::from("/PID"),
881 OsString::from(pid.to_string()),
882 ];
883 if kill_process_group {
884 args.push(OsString::from("/T"));
885 }
886 let spec = CommandSpec {
887 program: PathBuf::from("taskkill"),
888 args,
889 shell: None,
890 cwd: None,
891 env: CommandEnvironment::inherit_process_only(),
892 stdin: CommandStdin::Null,
893 stdout: CommandStdio::Null,
894 stderr: CommandStdio::Null,
895 };
896 let _ = SystemCommandRunner
897 .spawn(&spec)
898 .and_then(|child| child.wait().map(|_| ()));
899 }
900 #[cfg(not(any(unix, windows)))]
901 {
902 let _ = (pid, kill_process_group);
903 }
904}
905
906#[must_use]
908pub fn system_command_runner() -> Arc<dyn CommandRunner> {
909 Arc::new(SystemCommandRunner)
910}
911
912#[derive(Debug, Clone)]
914pub enum RecordedResponse {
915 Success,
917 Exit(i32),
919 Output {
921 stdout: Vec<u8>,
923 code: i32,
925 },
926}
927
928#[derive(Debug, Default)]
930pub struct RecordingRunner {
931 pub recorded: Mutex<Vec<CommandSpec>>,
933 stdin_payloads: Mutex<Vec<Arc<Mutex<Vec<u8>>>>>,
934 responses: Mutex<VecDeque<RecordedResponse>>,
935}
936
937impl RecordingRunner {
938 #[must_use]
940 pub fn always_success() -> Arc<Self> {
941 Arc::new(Self::default())
942 }
943
944 pub fn push_response(&self, response: RecordedResponse) {
946 self.responses
947 .lock()
948 .unwrap_or_else(|e| e.into_inner())
949 .push_back(response);
950 }
951
952 #[must_use]
954 pub fn specs(&self) -> Vec<CommandSpec> {
955 self.recorded
956 .lock()
957 .unwrap_or_else(|e| e.into_inner())
958 .clone()
959 }
960
961 #[must_use]
963 pub fn stdin_payloads(&self) -> Vec<Vec<u8>> {
964 self.stdin_payloads
965 .lock()
966 .unwrap_or_else(|e| e.into_inner())
967 .iter()
968 .map(|buf| buf.lock().unwrap_or_else(|e| e.into_inner()).clone())
969 .collect()
970 }
971}
972
973impl CommandRunner for RecordingRunner {
974 fn spawn(&self, spec: &CommandSpec) -> io::Result<RunningCommand> {
975 self.recorded
976 .lock()
977 .unwrap_or_else(|e| e.into_inner())
978 .push(spec.clone());
979
980 let response = self
981 .responses
982 .lock()
983 .unwrap_or_else(|e| e.into_inner())
984 .pop_front()
985 .unwrap_or(RecordedResponse::Success);
986
987 let piped_stdin = matches!(spec.stdin, CommandStdin::Pipe(_));
988
989 let (stdout_bytes, exit) = match response {
990 RecordedResponse::Success => (
991 None,
992 CommandExit {
993 code: Some(0),
994 success: true,
995 },
996 ),
997 RecordedResponse::Exit(code) => (
998 None,
999 CommandExit {
1000 code: Some(code),
1001 success: code == 0,
1002 },
1003 ),
1004 RecordedResponse::Output { stdout, code } => (
1005 Some(stdout),
1006 CommandExit {
1007 code: Some(code),
1008 success: code == 0,
1009 },
1010 ),
1011 };
1012
1013 let stdout_cursor = if spec.stdout == CommandStdio::Pipe {
1014 Some(Cursor::new(stdout_bytes.unwrap_or_default()))
1015 } else {
1016 None
1017 };
1018
1019 Ok(RunningCommand {
1020 inner: RunningCommandInner::Recording {
1021 state: Arc::new(Mutex::new(RecordingState {
1022 stdout: stdout_cursor,
1023 stderr: Vec::new(),
1024 exit,
1025 })),
1026 stdin: piped_stdin.then(|| {
1027 let buf = Arc::new(Mutex::new(Vec::new()));
1028 self.stdin_payloads
1029 .lock()
1030 .unwrap_or_else(|e| e.into_inner())
1031 .push(buf.clone());
1032 RecordingStdinBuffer(buf)
1033 }),
1034 },
1035 })
1036 }
1037}
1038
1039pub fn run_sh_dash_c(
1041 runner: &dyn CommandRunner,
1042 script: &str,
1043 cwd: Option<&Path>,
1044 stdin: CommandStdin,
1045 stderr: CommandStdio,
1046) -> io::Result<CommandOutput> {
1047 let mut spec = CommandSpec::sh_dash_c_filter(script);
1048 spec.cwd = cwd.map(Path::to_path_buf);
1049 spec.stdin = stdin;
1050 spec.stderr = stderr;
1051 runner.spawn(&spec)?.wait_with_output()
1052}
1053
1054pub fn run_program(
1056 runner: &dyn CommandRunner,
1057 program: &Path,
1058 args: &[OsString],
1059 cwd: Option<&Path>,
1060 stdin: CommandStdin,
1061 stderr: CommandStdio,
1062) -> io::Result<CommandOutput> {
1063 let spec = CommandSpec {
1064 program: program.to_path_buf(),
1065 args: args.to_vec(),
1066 shell: None,
1067 cwd: cwd.map(Path::to_path_buf),
1068 env: CommandEnvironment::inherit_process_only(),
1069 stdin,
1070 stdout: CommandStdio::Pipe,
1071 stderr,
1072 };
1073 runner.spawn(&spec)?.wait_with_output()
1074}
1075
1076pub mod hooks {
1078 pub use crate::hooks::*;
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use super::*;
1084 #[cfg(unix)]
1085 use std::os::unix::fs::PermissionsExt;
1086
1087 fn touch(path: &Path) {
1088 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
1089 std::fs::write(path, b"").expect("write");
1090 }
1091
1092 fn exe(name: &str) -> String {
1093 format!("{name}{}", std::env::consts::EXE_SUFFIX)
1094 }
1095
1096 #[test]
1097 fn find_program_in_path_searches_entries_in_order() {
1098 let tmp = tempfile::tempdir().expect("tempdir");
1099 let first = tmp.path().join("first");
1100 let second = tmp.path().join("second");
1101 std::fs::create_dir_all(&first).expect("mkdir");
1102 touch(&second.join(exe("sh")));
1103 touch(&tmp.path().join("third").join(exe("sh")));
1104 let path_var = std::env::join_paths([
1105 PathBuf::new(),
1106 first,
1107 second.clone(),
1108 tmp.path().join("third"),
1109 ])
1110 .expect("join");
1111
1112 assert_eq!(
1113 find_program_in_path("sh", &path_var),
1114 Some(second.join(exe("sh")))
1115 );
1116 assert_eq!(find_program_in_path("bash", &path_var), None);
1117 assert_eq!(find_program_in_path("sh", OsStr::new("")), None);
1118 }
1119
1120 #[test]
1121 fn resolve_script_interpreter_prefers_path() {
1122 let tmp = tempfile::tempdir().expect("tempdir");
1123 let bin = tmp.path().join("bin");
1124 touch(&bin.join(exe("bash")));
1125 let path_var = bin.clone().into_os_string();
1126 assert_eq!(
1127 resolve_script_interpreter("bash", Some(&path_var)).program,
1128 bin.join(exe("bash"))
1129 );
1130 }
1131
1132 #[test]
1133 fn resolve_script_interpreter_finds_git_for_windows_shell() {
1134 for git_dir in [&["cmd"][..], &["mingw64", "bin"][..]] {
1135 let tmp = tempfile::tempdir().expect("tempdir");
1136 let root = tmp.path().join("Git");
1137 let git_bin = git_dir.iter().fold(root.clone(), |p, c| p.join(c));
1138 touch(&git_bin.join(exe("git")));
1139 touch(&root.join("usr").join("bin").join(exe("sh")));
1140 let path_var = git_bin.into_os_string();
1141 assert_eq!(
1142 resolve_script_interpreter("sh", Some(&path_var)).program,
1143 root.join("usr").join("bin").join(exe("sh")),
1144 "git under {git_dir:?}"
1145 );
1146 }
1147 }
1148
1149 #[test]
1150 fn resolve_script_interpreter_prefers_git_tree_over_earlier_path_entries() {
1151 let tmp = tempfile::tempdir().expect("tempdir");
1152 let other = tmp.path().join("other-bin");
1153 touch(&other.join(exe("bash")));
1154 let root = tmp.path().join("Git");
1155 touch(&root.join("cmd").join(exe("git")));
1156 touch(&root.join("usr").join("bin").join(exe("bash")));
1157 let path_var = std::env::join_paths([other, root.join("cmd")]).expect("join");
1158 assert_eq!(
1159 resolve_script_interpreter("bash", Some(&path_var)).program,
1160 root.join("usr").join("bin").join(exe("bash"))
1161 );
1162 }
1163
1164 #[test]
1165 fn resolve_script_interpreter_skips_wsl_launchers() {
1166 let tmp = tempfile::tempdir().expect("tempdir");
1167 let system32 = tmp.path().join("Windows").join("System32");
1168 let apps = tmp.path().join("WindowsApps");
1169 let msys = tmp.path().join("msys64").join("usr").join("bin");
1170 touch(&system32.join(exe("bash")));
1171 touch(&apps.join(exe("bash")));
1172 touch(&msys.join(exe("bash")));
1173 let path_var =
1174 std::env::join_paths([system32.clone(), apps.clone(), msys.clone()]).expect("join");
1175 assert_eq!(
1176 resolve_script_interpreter("bash", Some(&path_var)).program,
1177 msys.join(exe("bash"))
1178 );
1179 let only_wsl = std::env::join_paths([system32, apps]).expect("join");
1180 assert_eq!(
1181 resolve_script_interpreter("bash", Some(&only_wsl)).program,
1182 PathBuf::from("bash")
1183 );
1184 }
1185
1186 #[test]
1187 fn resolve_script_interpreter_falls_back_to_bare_name() {
1188 let tmp = tempfile::tempdir().expect("tempdir");
1189 let path_var = tmp.path().as_os_str().to_os_string();
1190 assert_eq!(
1191 resolve_script_interpreter("sh", Some(&path_var)).program,
1192 PathBuf::from("sh")
1193 );
1194 assert_eq!(
1195 resolve_script_interpreter("sh", None),
1196 ScriptInterpreter::bare("sh")
1197 );
1198 }
1199
1200 #[test]
1201 fn child_path_var_uses_last_explicit_path() {
1202 let mut env = CommandEnvironment::default();
1203 assert_eq!(child_path_var(&env), None);
1204 env.set.push(("PATH".into(), "/first".into()));
1205 env.set.push(("HOME".into(), "/home/u".into()));
1206 env.set.push(("PATH".into(), "/second".into()));
1207 assert_eq!(child_path_var(&env), Some(OsString::from("/second")));
1208 }
1209
1210 #[cfg(windows)]
1211 #[test]
1212 fn child_path_var_ignores_case_on_windows() {
1213 let mut env = CommandEnvironment::default();
1214 env.set.push(("Path".into(), r"C:\Git\cmd".into()));
1215 assert_eq!(child_path_var(&env), Some(OsString::from(r"C:\Git\cmd")));
1216 }
1217
1218 #[test]
1219 fn child_path_var_falls_back_to_process_path_only_when_inheriting() {
1220 let inherit = CommandEnvironment::inherit_process_only();
1221 assert_eq!(child_path_var(&inherit), std::env::var_os("PATH"));
1222 assert_eq!(child_path_var(&CommandEnvironment::default()), None);
1223 }
1224
1225 #[cfg(unix)]
1226 #[test]
1227 fn shell_program_is_bin_sh_on_unix() {
1228 let mut env = CommandEnvironment::default();
1229 env.set.push(("PATH".into(), "/nonexistent".into()));
1230 assert_eq!(shell_program(&env), PathBuf::from("/bin/sh"));
1231 }
1232
1233 #[cfg(windows)]
1234 #[test]
1235 fn shell_program_finds_git_for_windows_shell_from_cmd_dir() {
1236 let tmp = tempfile::tempdir().expect("tempdir");
1237 let root = tmp.path().join("Git");
1238 touch(&root.join("cmd").join(exe("git")));
1239 touch(&root.join("usr").join("bin").join(exe("sh")));
1240 let mut env = CommandEnvironment::default();
1241 env.set
1242 .push(("Path".into(), root.join("cmd").into_os_string()));
1243 assert_eq!(
1244 shell_program(&env),
1245 root.join("usr").join("bin").join(exe("sh"))
1246 );
1247 }
1248
1249 #[test]
1250 fn resolve_script_interpreter_reports_the_git_for_windows_root() {
1251 let tmp = tempfile::tempdir().expect("tempdir");
1252 let root = tmp.path().join("Git");
1253 touch(&root.join("cmd").join(exe("git")));
1254 touch(&root.join("usr").join("bin").join(exe("sh")));
1255 let path_var = root.join("cmd").into_os_string();
1256 assert_eq!(
1257 resolve_script_interpreter("sh", Some(&path_var)).git_root,
1258 Some(root)
1259 );
1260
1261 let other = tmp.path().join("other-bin");
1262 touch(&other.join(exe("bash")));
1263 let path_var = other.clone().into_os_string();
1264 assert_eq!(
1265 resolve_script_interpreter("bash", Some(&path_var)),
1266 ScriptInterpreter::bare(other.join(exe("bash")))
1267 );
1268 }
1269
1270 #[test]
1271 fn git_for_windows_tool_dirs_lists_mingw_then_usr_bin() {
1272 let tmp = tempfile::tempdir().expect("tempdir");
1273 let root = tmp.path().join("Git");
1274 assert!(git_for_windows_tool_dirs(&root).is_empty());
1275
1276 touch(&root.join("usr").join("bin").join(exe("grep")));
1277 assert_eq!(
1278 git_for_windows_tool_dirs(&root),
1279 vec![root.join("usr").join("bin")]
1280 );
1281
1282 touch(&root.join("clangarm64").join("bin").join(exe("git")));
1283 touch(&root.join("mingw64").join("bin").join(exe("git")));
1284 assert_eq!(
1285 git_for_windows_tool_dirs(&root),
1286 vec![
1287 root.join("mingw64").join("bin"),
1288 root.join("usr").join("bin")
1289 ]
1290 );
1291 }
1292
1293 #[test]
1294 fn prepend_to_child_path_puts_dirs_first() {
1295 let sep = if cfg!(windows) { ";" } else { ":" };
1296 let first = PathBuf::from("first");
1297 let second = PathBuf::from("second");
1298 let mut env = CommandEnvironment::default();
1299 env.set.push(("HOME".into(), "home".into()));
1300 env.set.push(("PATH".into(), "inherited".into()));
1301 prepend_to_child_path(&mut env, &[first.clone(), second.clone()]);
1302 assert_eq!(
1303 child_path_var(&env),
1304 Some(OsString::from(format!("first{sep}second{sep}inherited")))
1305 );
1306 assert_eq!(env.set.len(), 2);
1307 assert_eq!(env.set[0].1, OsString::from("home"));
1308
1309 let mut empty_path = CommandEnvironment::default();
1310 empty_path.set.push(("PATH".into(), "".into()));
1311 prepend_to_child_path(&mut empty_path, std::slice::from_ref(&first));
1312 assert_eq!(child_path_var(&empty_path), Some(OsString::from("first")));
1313
1314 let mut no_path = CommandEnvironment::default();
1315 prepend_to_child_path(&mut no_path, std::slice::from_ref(&first));
1316 assert_eq!(
1317 no_path.set,
1318 vec![(OsString::from("PATH"), OsString::from("first"))]
1319 );
1320
1321 let mut unchanged = CommandEnvironment::default();
1322 unchanged.set.push(("PATH".into(), "inherited".into()));
1323 prepend_to_child_path(&mut unchanged, &[]);
1324 assert_eq!(
1325 child_path_var(&unchanged),
1326 Some(OsString::from("inherited"))
1327 );
1328 }
1329
1330 #[test]
1331 fn prepend_to_child_path_extends_the_inherited_process_path() {
1332 let mut env = CommandEnvironment::inherit_process_only();
1333 let inherited = child_path_var(&env);
1334 prepend_to_child_path(&mut env, &[PathBuf::from("first")]);
1335 let mut expected = OsString::from("first");
1336 if let Some(inherited) = inherited.filter(|p| !p.is_empty()) {
1337 expected.push(if cfg!(windows) { ";" } else { ":" });
1338 expected.push(inherited);
1339 }
1340 assert_eq!(child_path_var(&env), Some(expected));
1341 }
1342
1343 #[cfg(unix)]
1344 #[test]
1345 fn system_runner_runs_bare_sh_without_sh_on_path() {
1346 let mut spec = CommandSpec::sh_dash_c_filter("printf ok");
1347 spec.env = CommandEnvironment {
1348 inherit_process: false,
1349 set: vec![("PATH".into(), "/nonexistent".into())],
1350 remove: Vec::new(),
1351 };
1352 let out = SystemCommandRunner
1353 .spawn(&spec)
1354 .expect("spawn")
1355 .wait_with_output()
1356 .expect("wait");
1357 assert!(out.status.success);
1358 assert_eq!(out.stdout, b"ok");
1359 }
1360
1361 #[cfg(unix)]
1362 #[test]
1363 fn system_runner_runs_sh_dash_c_script_with_argument() {
1364 let tmp = tempfile::tempdir().expect("tempdir");
1365 let helper = tmp.path().join("h.sh");
1366 std::fs::write(
1367 &helper,
1368 "#!/bin/sh\nif [ \"$1\" = get ]; then echo username=alice; fi\n",
1369 )
1370 .expect("write");
1371 let mut perms = std::fs::metadata(&helper).expect("meta").permissions();
1372 perms.set_mode(0o755);
1373 std::fs::set_permissions(&helper, perms).expect("chmod");
1374
1375 let script = format!("{} get", helper.display());
1376 let spec = CommandSpec {
1377 program: "sh".into(),
1378 args: Vec::new(),
1379 shell: Some(ShellInvocation::DashC {
1380 script,
1381 argv0: None,
1382 args: Vec::new(),
1383 }),
1384 cwd: None,
1385 env: CommandEnvironment::inherit_process_only(),
1386 stdin: CommandStdin::Null,
1387 stdout: CommandStdio::Pipe,
1388 stderr: CommandStdio::Inherit,
1389 };
1390 let out = SystemCommandRunner
1391 .spawn(&spec)
1392 .expect("spawn")
1393 .wait_with_output()
1394 .expect("wait");
1395 assert!(out.status.success);
1396 assert!(
1397 out.stdout.starts_with(b"username=alice"),
1398 "runner stdout={:?} stderr={:?} status={:?}",
1399 out.stdout,
1400 out.stderr,
1401 out.status
1402 );
1403
1404 let direct = std::process::Command::new("sh")
1405 .arg("-c")
1406 .arg(format!("{} get", helper.display()))
1407 .output()
1408 .expect("direct");
1409 assert!(
1410 direct.stdout.starts_with(b"username=alice"),
1411 "direct stdout={:?}",
1412 direct.stdout
1413 );
1414 }
1415
1416 #[test]
1417 fn feed_stdin_and_wait_with_output_avoids_hook_pipe_deadlock() {
1418 let script = "while read line; do printf 'checking %s\\n' \"$line\" >&2; done";
1419 let stdin_lines: String = (1..=3000)
1420 .map(|i| format!("1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/b{i}\n"))
1421 .collect();
1422 let spec = CommandSpec {
1423 program: "sh".into(),
1424 args: Vec::new(),
1425 shell: Some(ShellInvocation::DashC {
1426 script: script.into(),
1427 argv0: None,
1428 args: Vec::new(),
1429 }),
1430 cwd: None,
1431 env: CommandEnvironment::inherit_process_only(),
1432 stdin: CommandStdin::Pipe(Vec::new()),
1433 stdout: CommandStdio::Pipe,
1434 stderr: CommandStdio::Pipe,
1435 };
1436 let started = std::time::Instant::now();
1437 let out = SystemCommandRunner
1438 .spawn(&spec)
1439 .expect("spawn")
1440 .feed_stdin_and_wait_with_output(Some(ConcurrentStdinFeed::Bytes(
1441 stdin_lines.as_bytes(),
1442 )))
1443 .expect("wait");
1444 assert!(
1445 started.elapsed() < std::time::Duration::from_secs(5),
1446 "hook I/O took {:?}",
1447 started.elapsed()
1448 );
1449 assert!(out.status.success);
1450 assert!(out.stderr.len() > 64 * 1024);
1451 assert!(out.stderr.starts_with(b"checking "));
1452 }
1453
1454 #[test]
1455 fn feed_stdin_and_wait_with_output_treats_broken_pipe_as_early_exit() {
1456 let script = "exit 1";
1457 let payload = vec![b'x'; 128 * 1024];
1458 let spec = CommandSpec {
1459 program: "sh".into(),
1460 args: Vec::new(),
1461 shell: Some(ShellInvocation::DashC {
1462 script: script.into(),
1463 argv0: None,
1464 args: Vec::new(),
1465 }),
1466 cwd: None,
1467 env: CommandEnvironment::inherit_process_only(),
1468 stdin: CommandStdin::Pipe(Vec::new()),
1469 stdout: CommandStdio::Pipe,
1470 stderr: CommandStdio::Pipe,
1471 };
1472 let out = SystemCommandRunner
1473 .spawn(&spec)
1474 .expect("spawn")
1475 .feed_stdin_and_wait_with_output(Some(ConcurrentStdinFeed::Bytes(&payload)))
1476 .expect("wait");
1477 assert!(!out.status.success);
1478 assert_eq!(out.status.code, Some(1));
1479 }
1480
1481 #[test]
1482 fn wait_with_output_drains_stderr_and_stdout_concurrently() {
1483 let script = "dd if=/dev/zero bs=1024 count=1024 >&2; printf ok";
1484 let spec = CommandSpec {
1485 program: "sh".into(),
1486 args: Vec::new(),
1487 shell: Some(ShellInvocation::DashC {
1488 script: script.into(),
1489 argv0: None,
1490 args: Vec::new(),
1491 }),
1492 cwd: None,
1493 env: CommandEnvironment::inherit_process_only(),
1494 stdin: CommandStdin::Null,
1495 stdout: CommandStdio::Pipe,
1496 stderr: CommandStdio::Pipe,
1497 };
1498 let out = SystemCommandRunner
1499 .spawn(&spec)
1500 .expect("spawn")
1501 .wait_with_output()
1502 .expect("wait");
1503 assert!(out.status.success);
1504 assert_eq!(out.stdout, b"ok");
1505 assert!(out.stderr.len() >= 1024);
1506 }
1507
1508 #[test]
1509 fn recording_runner_captures_spec_and_exit() {
1510 let runner = RecordingRunner::always_success();
1511 runner.push_response(RecordedResponse::Output {
1512 stdout: b"ok".to_vec(),
1513 code: 0,
1514 });
1515 let mut spec = CommandSpec::sh_dash_c_filter("echo hi");
1516 spec.stdin = CommandStdin::Pipe(b"data".to_vec());
1517 let handle = runner.spawn(&spec).expect("spawn");
1518 let out = handle.wait_with_output().expect("wait");
1519 assert!(out.status.success);
1520 assert_eq!(out.stdout, b"ok");
1521 let recorded = runner.specs();
1522 assert_eq!(recorded.len(), 1);
1523 match &recorded[0].stdin {
1524 CommandStdin::Pipe(b) => assert_eq!(b, b"data"),
1525 _ => panic!("expected pipe stdin"),
1526 }
1527 }
1528}