1use crate::process_error::ProcessError;
2use crate::read2;
3
4use anyhow::{Context, Result};
5use jobserver::Client;
6use shell_escape::escape;
7use tempfile::NamedTempFile;
8
9use std::collections::BTreeMap;
10use std::env;
11use std::ffi::{OsStr, OsString};
12use std::fmt;
13use std::io::{self, Write};
14use std::iter::once;
15use std::path::Path;
16use std::process::{Command, ExitStatus, Output};
17
18#[derive(Clone, Copy, Debug, Default)]
22pub struct ExecStats {
23 pub peak_rss: Option<u64>,
25}
26
27#[derive(Clone, Debug)]
29pub struct ProcessBuilder {
30 program: OsString,
32 arg0: Option<OsString>,
34 args: Vec<OsString>,
36 env: BTreeMap<String, Option<OsString>>,
38 cwd: Option<OsString>,
40 wrappers: Vec<OsString>,
43 jobserver: Option<Client>,
48 display_env_vars: bool,
50 retry_with_argfile: bool,
53 stdin: Option<Vec<u8>>,
55 stdout: Option<Stdio>,
56 stderr: Option<Stdio>,
57}
58
59impl fmt::Display for ProcessBuilder {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "`")?;
62
63 if self.display_env_vars {
64 for (key, val) in self.env.iter() {
65 if let Some(val) = val {
66 let val = escape(val.to_string_lossy());
67 if cfg!(windows) {
68 write!(f, "set {}={}&& ", key, val)?;
69 } else {
70 write!(f, "{}={} ", key, val)?;
71 }
72 }
73 }
74 }
75
76 write!(f, "{}", self.get_program().to_string_lossy())?;
77
78 for arg in self.get_args() {
79 write!(f, " {}", escape(arg.to_string_lossy()))?;
80 }
81
82 write!(f, "`")
83 }
84}
85
86impl ProcessBuilder {
87 pub fn new<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder {
89 ProcessBuilder {
90 program: cmd.as_ref().to_os_string(),
91 arg0: None,
92 args: Vec::new(),
93 cwd: None,
94 env: BTreeMap::new(),
95 wrappers: Vec::new(),
96 jobserver: None,
97 display_env_vars: false,
98 retry_with_argfile: false,
99 stdin: None,
100 stdout: None,
101 stderr: None,
102 }
103 }
104
105 pub fn program<T: AsRef<OsStr>>(&mut self, program: T) -> &mut ProcessBuilder {
107 self.program = program.as_ref().to_os_string();
108 self
109 }
110
111 pub fn arg0<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
113 self.arg0 = Some(arg.as_ref().to_os_string());
114 self
115 }
116
117 pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
119 self.args.push(arg.as_ref().to_os_string());
120 self
121 }
122
123 pub fn args<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
125 self.args
126 .extend(args.iter().map(|t| t.as_ref().to_os_string()));
127 self
128 }
129
130 pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
132 if let Some(program) = self.wrappers.pop() {
133 self.program = program;
137 self.wrappers = Vec::new();
138 }
139 self.args = args.iter().map(|t| t.as_ref().to_os_string()).collect();
140 self
141 }
142
143 pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut ProcessBuilder {
145 self.cwd = Some(path.as_ref().to_os_string());
146 self
147 }
148
149 pub fn env<T: AsRef<OsStr>>(&mut self, key: &str, val: T) -> &mut ProcessBuilder {
151 self.env
152 .insert(key.to_string(), Some(val.as_ref().to_os_string()));
153 self
154 }
155
156 pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
158 self.env.insert(key.to_string(), None);
159 self
160 }
161
162 pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
166 self.stdout = Some(cfg.into());
167 self
168 }
169
170 pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
174 self.stderr = Some(cfg.into());
175 self
176 }
177
178 pub fn get_program(&self) -> &OsString {
180 self.wrappers.last().unwrap_or(&self.program)
181 }
182
183 pub fn get_arg0(&self) -> Option<&OsStr> {
185 self.arg0.as_deref()
186 }
187
188 pub fn get_args(&self) -> impl Iterator<Item = &OsString> {
190 self.wrappers
191 .iter()
192 .rev()
193 .chain(once(&self.program))
194 .chain(self.args.iter())
195 .skip(1) }
197
198 pub fn get_cwd(&self) -> Option<&Path> {
200 self.cwd.as_ref().map(Path::new)
201 }
202
203 pub fn get_env(&self, var: &str) -> Option<OsString> {
206 self.env
207 .get(var)
208 .cloned()
209 .or_else(|| Some(env::var_os(var)))
210 .and_then(|s| s)
211 }
212
213 pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
216 &self.env
217 }
218
219 pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
224 self.jobserver = Some(jobserver.clone());
225 self
226 }
227
228 pub fn display_env_vars(&mut self) -> &mut Self {
230 self.display_env_vars = true;
231 self
232 }
233
234 pub fn retry_with_argfile(&mut self, enabled: bool) -> &mut Self {
252 self.retry_with_argfile = enabled;
253 self
254 }
255
256 pub fn stdin<T: Into<Vec<u8>>>(&mut self, stdin: T) -> &mut Self {
258 self.stdin = Some(stdin.into());
259 self
260 }
261
262 fn should_retry_with_argfile(&self, err: &io::Error) -> bool {
263 self.retry_with_argfile && imp::command_line_too_big(err)
264 }
265
266 pub fn status(&self) -> Result<ExitStatus> {
268 self._status()
269 .with_context(|| ProcessError::could_not_execute(self))
270 }
271
272 fn _status(&self) -> io::Result<ExitStatus> {
273 if !debug_force_argfile(self.retry_with_argfile) {
274 let mut cmd = self.build_command();
275 if let Some(stdout) = &self.stdout {
276 cmd.stdout(stdout.to_std());
277 }
278 if let Some(stderr) = &self.stderr {
279 cmd.stderr(stderr.to_std());
280 }
281 match cmd.spawn() {
282 Err(ref e) if self.should_retry_with_argfile(e) => {}
283 Err(e) => return Err(e),
284 Ok(mut child) => return child.wait(),
285 }
286 }
287 let (mut cmd, argfile) = self.build_command_with_argfile()?;
288 if let Some(stdout) = &self.stdout {
289 cmd.stdout(stdout.to_std());
290 }
291 if let Some(stderr) = &self.stderr {
292 cmd.stderr(stderr.to_std());
293 }
294 let status = cmd.spawn()?.wait();
295 close_tempfile_and_log_error(argfile);
296 status
297 }
298
299 pub fn exec(&self) -> Result<()> {
301 let exit = self.status()?;
302 if exit.success() {
303 Ok(())
304 } else {
305 Err(ProcessError::new(
306 &format!("process didn't exit successfully: {}", self),
307 Some(exit),
308 None,
309 )
310 .into())
311 }
312 }
313
314 pub fn exec_replace(&self) -> Result<()> {
330 imp::exec_replace(self)
331 }
332
333 pub fn output(&self) -> Result<Output> {
335 self._output()
336 .with_context(|| ProcessError::could_not_execute(self))
337 }
338
339 fn _output(&self) -> io::Result<Output> {
340 if !debug_force_argfile(self.retry_with_argfile) {
341 let mut cmd = self.build_command();
342 match piped(&mut cmd, self.stdin.is_some()).spawn() {
343 Err(ref e) if self.should_retry_with_argfile(e) => {}
344 Err(e) => return Err(e),
345 Ok(mut child) => {
346 if let Some(stdin) = &self.stdin {
347 child.stdin.take().unwrap().write_all(stdin)?;
348 }
349 return child.wait_with_output();
350 }
351 }
352 }
353 let (mut cmd, argfile) = self.build_command_with_argfile()?;
354 let mut child = piped(&mut cmd, self.stdin.is_some()).spawn()?;
355 if let Some(stdin) = &self.stdin {
356 child.stdin.take().unwrap().write_all(stdin)?;
357 }
358 let output = child.wait_with_output();
359 close_tempfile_and_log_error(argfile);
360 output
361 }
362
363 pub fn exec_with_output(&self) -> Result<Output> {
365 let output = self.output()?;
366 if output.status.success() {
367 Ok(output)
368 } else {
369 Err(ProcessError::new(
370 &format!("process didn't exit successfully: {}", self),
371 Some(output.status),
372 Some(&output),
373 )
374 .into())
375 }
376 }
377
378 pub fn exec_with_streaming(
391 &self,
392 on_stdout_line: &mut dyn FnMut(&str) -> Result<()>,
393 on_stderr_line: &mut dyn FnMut(&str) -> Result<()>,
394 capture_output: bool,
395 capture_rusage: bool,
396 ) -> (ExecStats, Result<Output>) {
397 let mut stdout = Vec::new();
398 let mut stderr = Vec::new();
399
400 let mut callback_error = None;
401 let mut stdout_pos = 0;
402 let mut stderr_pos = 0;
403
404 let spawn = |mut cmd| {
405 if !debug_force_argfile(self.retry_with_argfile) {
406 match piped(&mut cmd, false).spawn() {
407 Err(ref e) if self.should_retry_with_argfile(e) => {}
408 Err(e) => return Err(e),
409 Ok(child) => return Ok((child, None)),
410 }
411 }
412 let (mut cmd, argfile) = self.build_command_with_argfile()?;
413 Ok((piped(&mut cmd, false).spawn()?, Some(argfile)))
414 };
415
416 let wait_result = (|| {
417 let cmd = self.build_command();
418 let (mut child, argfile) = spawn(cmd)?;
419 let out = child.stdout.take().unwrap();
420 let err = child.stderr.take().unwrap();
421 read2(out, err, &mut |is_out, data, eof| {
422 let pos = if is_out {
423 &mut stdout_pos
424 } else {
425 &mut stderr_pos
426 };
427 let idx = if eof {
428 data.len()
429 } else {
430 match data[*pos..].iter().rposition(|b| *b == b'\n') {
431 Some(i) => *pos + i + 1,
432 None => {
433 *pos = data.len();
434 return;
435 }
436 }
437 };
438
439 let new_lines = &data[..idx];
440
441 for line in String::from_utf8_lossy(new_lines).lines() {
442 if callback_error.is_some() {
443 break;
444 }
445 let callback_result = if is_out {
446 on_stdout_line(line)
447 } else {
448 on_stderr_line(line)
449 };
450 if let Err(e) = callback_result {
451 callback_error = Some(e);
452 break;
453 }
454 }
455
456 if capture_output {
457 let dst = if is_out { &mut stdout } else { &mut stderr };
458 dst.extend(new_lines);
459 }
460
461 data.drain(..idx);
462 *pos = 0;
463 })?;
464 let status = if capture_rusage {
465 imp::wait_with_peak_rss(child)
466 } else {
467 child.wait().map(|status| (status, None))
468 };
469 if let Some(argfile) = argfile {
470 close_tempfile_and_log_error(argfile);
471 }
472 status
473 })()
474 .with_context(|| ProcessError::could_not_execute(self));
475
476 let (status, peak_rss) = match wait_result {
477 Ok((status, peak_rss)) => (status, peak_rss),
478 Err(e) => return (ExecStats::default(), Err(e)),
480 };
481
482 let output = Output {
483 status,
484 stdout,
485 stderr,
486 };
487
488 let result = {
489 let to_print = if capture_output { Some(&output) } else { None };
490 if let Some(e) = callback_error {
491 let cx = ProcessError::new(
492 &format!("failed to parse process output: {}", self),
493 Some(output.status),
494 to_print,
495 );
496 Err(anyhow::Error::new(cx).context(e))
497 } else if !output.status.success() {
498 Err(anyhow::Error::new(ProcessError::new(
499 &format!("process didn't exit successfully: {}", self),
500 Some(output.status),
501 to_print,
502 )))
503 } else {
504 Ok(())
505 }
506 };
507
508 let stats = ExecStats { peak_rss };
509 (stats, result.map(|()| output))
510 }
511
512 fn build_command_with_argfile(&self) -> io::Result<(Command, NamedTempFile)> {
515 use std::io::Write as _;
516
517 let mut tmp = tempfile::Builder::new()
518 .prefix("cargo-argfile.")
519 .tempfile()?;
520
521 let mut arg = OsString::from("@");
522 arg.push(tmp.path());
523 let mut cmd = self.build_command_without_args();
524 cmd.arg(arg);
525 tracing::debug!("created argfile at {} for {self}", tmp.path().display());
526
527 let cap = self.get_args().map(|arg| arg.len() + 1).sum::<usize>();
528 let mut buf = Vec::with_capacity(cap);
529 for arg in &self.args {
530 let arg = arg.to_str().ok_or_else(|| {
531 io::Error::new(
532 io::ErrorKind::Other,
533 format!(
534 "argument for argfile contains invalid UTF-8 characters: `{}`",
535 arg.to_string_lossy()
536 ),
537 )
538 })?;
539 if arg.contains('\n') {
540 return Err(io::Error::new(
541 io::ErrorKind::Other,
542 format!("argument for argfile contains newlines: `{arg}`"),
543 ));
544 }
545 writeln!(buf, "{arg}")?;
546 }
547 tmp.write_all(&mut buf)?;
548 Ok((cmd, tmp))
549 }
550
551 fn build_command_without_args(&self) -> Command {
553 let mut command = {
554 let mut iter = self.wrappers.iter().rev().chain(once(&self.program));
555 let mut cmd = Command::new(iter.next().expect("at least one `program` exists"));
556 cmd.args(iter);
557 cmd
558 };
559 #[cfg(unix)]
560 if let Some(arg0) = self.get_arg0() {
561 use std::os::unix::process::CommandExt as _;
562 command.arg0(arg0);
563 }
564 if let Some(cwd) = self.get_cwd() {
565 command.current_dir(cwd);
566 }
567 for (k, v) in &self.env {
568 match *v {
569 Some(ref v) => {
570 command.env(k, v);
571 }
572 None => {
573 command.env_remove(k);
574 }
575 }
576 }
577 if let Some(ref c) = self.jobserver {
578 c.configure(&mut command);
579 }
580 command
581 }
582
583 pub fn build_command(&self) -> Command {
589 let mut command = self.build_command_without_args();
590 for arg in &self.args {
591 command.arg(arg);
592 }
593 command
594 }
595
596 pub fn wrapped(mut self, wrapper: Option<impl AsRef<OsStr>>) -> Self {
609 if let Some(wrapper) = wrapper.as_ref() {
610 let wrapper = wrapper.as_ref();
611 if !wrapper.is_empty() {
612 self.wrappers.push(wrapper.to_os_string());
613 }
614 }
615 self
616 }
617}
618
619#[derive(Clone, Debug)]
620pub enum Stdio {
621 Piped,
622 Inherit,
623 Null,
624}
625
626impl Stdio {
627 fn to_std(&self) -> std::process::Stdio {
628 match self {
629 Self::Piped => std::process::Stdio::piped(),
630 Self::Inherit => std::process::Stdio::inherit(),
631 Self::Null => std::process::Stdio::null(),
632 }
633 }
634}
635
636fn debug_force_argfile(retry_enabled: bool) -> bool {
640 retry_enabled && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok()
641}
642
643fn piped(cmd: &mut Command, pipe_stdin: bool) -> &mut Command {
645 use std::process::Stdio;
646
647 cmd.stdout(Stdio::piped())
648 .stderr(Stdio::piped())
649 .stdin(if pipe_stdin {
650 Stdio::piped()
651 } else {
652 Stdio::null()
653 })
654}
655
656fn close_tempfile_and_log_error(file: NamedTempFile) {
657 file.close().unwrap_or_else(|e| {
658 tracing::warn!("failed to close temporary file: {e}");
659 });
660}
661
662#[cfg(unix)]
663mod imp {
664 use super::{ProcessBuilder, ProcessError, close_tempfile_and_log_error, debug_force_argfile};
665 use anyhow::Result;
666 use std::io;
667 use std::os::unix::process::{CommandExt, ExitStatusExt};
668 use std::process::ExitStatus;
669
670 pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
671 let mut error;
672 let mut file = None;
673 if debug_force_argfile(process_builder.retry_with_argfile) {
674 let (mut command, argfile) = process_builder.build_command_with_argfile()?;
675 file = Some(argfile);
676 error = command.exec()
677 } else {
678 let mut command = process_builder.build_command();
679 error = command.exec();
680 if process_builder.should_retry_with_argfile(&error) {
681 let (mut command, argfile) = process_builder.build_command_with_argfile()?;
682 file = Some(argfile);
683 error = command.exec()
684 }
685 }
686 if let Some(file) = file {
687 close_tempfile_and_log_error(file);
688 }
689
690 Err(anyhow::Error::from(error).context(ProcessError::new(
691 &format!("could not execute process {}", process_builder),
692 None,
693 None,
694 )))
695 }
696
697 pub fn command_line_too_big(err: &io::Error) -> bool {
698 err.raw_os_error() == Some(libc::E2BIG)
699 }
700
701 #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
707 pub fn wait_with_peak_rss(
708 mut child: std::process::Child,
709 ) -> io::Result<(ExitStatus, Option<u64>)> {
710 let pid = child.id() as libc::pid_t;
711 let mut status: libc::c_int = 0;
712 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
714 loop {
715 let ret = unsafe { libc::wait4(pid, &mut status, 0, &mut usage) };
717 if ret == -1 {
718 let err = io::Error::last_os_error();
719 if err.kind() == io::ErrorKind::Interrupted {
720 tracing::debug!("Process `{pid}` was interrupted");
721 continue;
722 }
723 tracing::error!(
724 "Unexpected error while waiting for process `{pid}` to finish. {err}"
725 );
726 return child.wait().map(|status| (status, None));
727 }
728 break;
729 }
730 let peak = if cfg!(target_os = "macos") {
735 usage.ru_maxrss.max(0) as u64
736 } else {
737 (usage.ru_maxrss.max(0) as u64).saturating_mul(1024)
738 };
739 Ok((ExitStatus::from_raw(status), Some(peak)))
742 }
743
744 #[cfg(any(target_os = "solaris", target_os = "illumos"))]
746 pub fn wait_with_peak_rss(
747 mut child: std::process::Child,
748 ) -> io::Result<(ExitStatus, Option<u64>)> {
749 child.wait().map(|status| (status, None))
750 }
751}
752
753#[cfg(windows)]
754mod imp {
755 use super::{ProcessBuilder, ProcessError};
756 use anyhow::Result;
757 use std::io;
758 use std::process::ExitStatus;
759 use windows_sys::Win32::Foundation::{FALSE, TRUE};
760 use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
761 use windows_sys::core::BOOL;
762
763 unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL {
764 TRUE
766 }
767
768 pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
769 unsafe {
770 if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE {
771 return Err(ProcessError::new("Could not set Ctrl-C handler.", None, None).into());
772 }
773 }
774
775 process_builder.exec()
777 }
778
779 pub fn command_line_too_big(err: &io::Error) -> bool {
780 use windows_sys::Win32::Foundation::ERROR_FILENAME_EXCED_RANGE;
781 err.raw_os_error() == Some(ERROR_FILENAME_EXCED_RANGE as i32)
782 }
783
784 pub fn wait_with_peak_rss(
785 mut child: std::process::Child,
786 ) -> io::Result<(ExitStatus, Option<u64>)> {
787 use std::os::windows::io::AsRawHandle;
788 use windows_sys::Win32::System::ProcessStatus::{
789 GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS,
790 };
791
792 let status = child.wait()?;
793 let mut counters: PROCESS_MEMORY_COUNTERS = unsafe { std::mem::zeroed() };
795 counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
796 if unsafe { GetProcessMemoryInfo(child.as_raw_handle() as _, &mut counters, counters.cb) }
799 != 0
800 {
801 Ok((status, Some(counters.PeakWorkingSetSize as u64)))
802 } else {
803 Ok((status, None))
804 }
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use super::ProcessBuilder;
811 use std::fs;
812
813 #[test]
814 fn argfile_build_succeeds() {
815 let mut cmd = ProcessBuilder::new("echo");
816 cmd.args(["foo", "bar"].as_slice());
817 let (cmd, argfile) = cmd.build_command_with_argfile().unwrap();
818
819 assert_eq!(cmd.get_program(), "echo");
820 let cmd_args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
821 assert_eq!(cmd_args.len(), 1);
822 assert!(cmd_args[0].starts_with("@"));
823 assert!(cmd_args[0].contains("cargo-argfile."));
824
825 let buf = fs::read_to_string(argfile.path()).unwrap();
826 assert_eq!(buf, "foo\nbar\n");
827 }
828
829 #[test]
830 fn argfile_build_fails_if_arg_contains_newline() {
831 let mut cmd = ProcessBuilder::new("echo");
832 cmd.arg("foo\n");
833 let err = cmd.build_command_with_argfile().unwrap_err();
834 assert_eq!(
835 err.to_string(),
836 "argument for argfile contains newlines: `foo\n`"
837 );
838 }
839
840 #[test]
841 fn argfile_build_fails_if_arg_contains_invalid_utf8() {
842 let mut cmd = ProcessBuilder::new("echo");
843
844 #[cfg(windows)]
845 let invalid_arg = {
846 use std::os::windows::prelude::*;
847 std::ffi::OsString::from_wide(&[0x0066, 0x006f, 0xD800, 0x006f])
848 };
849
850 #[cfg(unix)]
851 let invalid_arg = {
852 use std::os::unix::ffi::OsStrExt;
853 std::ffi::OsStr::from_bytes(&[0x66, 0x6f, 0x80, 0x6f]).to_os_string()
854 };
855
856 cmd.arg(invalid_arg);
857 let err = cmd.build_command_with_argfile().unwrap_err();
858 assert_eq!(
859 err.to_string(),
860 "argument for argfile contains invalid UTF-8 characters: `fo�o`"
861 );
862 }
863}