Skip to main content

cargo_util/
process_builder.rs

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/// Metrics gathered while running a process
19///
20/// Returned by [`ProcessBuilder::exec_with_streaming`] whether or not it succeeded.
21#[derive(Clone, Copy, Debug, Default)]
22pub struct ExecStats {
23    /// Peak resident set size of the process, in bytes.
24    pub peak_rss: Option<u64>,
25}
26
27/// A builder object for an external process, similar to [`std::process::Command`].
28#[derive(Clone, Debug)]
29pub struct ProcessBuilder {
30    /// The program to execute.
31    program: OsString,
32    /// Best-effort replacement for arg0
33    arg0: Option<OsString>,
34    /// A list of arguments to pass to the program.
35    args: Vec<OsString>,
36    /// Any environment variables that should be set for the program.
37    env: BTreeMap<String, Option<OsString>>,
38    /// The directory to run the program from.
39    cwd: Option<OsString>,
40    /// A list of wrappers that wrap the original program when calling
41    /// [`ProcessBuilder::wrapped`]. The last one is the outermost one.
42    wrappers: Vec<OsString>,
43    /// The `make` jobserver. See the [jobserver crate] for
44    /// more information.
45    ///
46    /// [jobserver crate]: https://docs.rs/jobserver/
47    jobserver: Option<Client>,
48    /// `true` to include environment variable in display.
49    display_env_vars: bool,
50    /// `true` to retry with an argfile if hitting "command line too big" error.
51    /// See [`ProcessBuilder::retry_with_argfile`] for more information.
52    retry_with_argfile: bool,
53    /// Data to write to stdin.
54    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    /// Creates a new [`ProcessBuilder`] with the given executable path.
88    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    /// (chainable) Sets the executable for the process.
106    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    /// (chainable) Overrides `arg0` for this program.
112    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    /// (chainable) Adds `arg` to the args list.
118    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    /// (chainable) Adds multiple `args` to the args list.
124    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    /// (chainable) Replaces the args list with the given `args`.
131    pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
132        if let Some(program) = self.wrappers.pop() {
133            // User intend to replace all args, so we
134            // - use the outermost wrapper as the main program, and
135            // - cleanup other inner wrappers.
136            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    /// (chainable) Sets the current working directory of the process.
144    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    /// (chainable) Sets an environment variable for the process.
150    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    /// (chainable) Unsets an environment variable for the process.
157    pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
158        self.env.insert(key.to_string(), None);
159        self
160    }
161
162    /// (chainable) Configure the process's stdout handle
163    ///
164    /// Only applies when used with [`Self::status`] and [`Self::exec`]
165    pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
166        self.stdout = Some(cfg.into());
167        self
168    }
169
170    /// (chainable) Configure the process's stderr handle
171    ///
172    /// Only applies when used with [`Self::status`] and [`Self::exec`]
173    pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
174        self.stderr = Some(cfg.into());
175        self
176    }
177
178    /// Gets the executable name.
179    pub fn get_program(&self) -> &OsString {
180        self.wrappers.last().unwrap_or(&self.program)
181    }
182
183    /// Gets the program arg0.
184    pub fn get_arg0(&self) -> Option<&OsStr> {
185        self.arg0.as_deref()
186    }
187
188    /// Gets the program arguments.
189    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) // Skip the main `program
196    }
197
198    /// Gets the current working directory for the process.
199    pub fn get_cwd(&self) -> Option<&Path> {
200        self.cwd.as_ref().map(Path::new)
201    }
202
203    /// Gets an environment variable as the process will see it (will inherit from environment
204    /// unless explicitly unset).
205    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    /// Gets all environment variables explicitly set or unset for the process (not inherited
214    /// vars).
215    pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
216        &self.env
217    }
218
219    /// Sets the `make` jobserver. See the [jobserver crate][jobserver_docs] for
220    /// more information.
221    ///
222    /// [jobserver_docs]: https://docs.rs/jobserver/latest/jobserver/
223    pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
224        self.jobserver = Some(jobserver.clone());
225        self
226    }
227
228    /// Enables environment variable display.
229    pub fn display_env_vars(&mut self) -> &mut Self {
230        self.display_env_vars = true;
231        self
232    }
233
234    /// Enables retrying with an argfile if hitting "command line too big" error
235    ///
236    /// This is primarily for the `@path` arg of rustc and rustdoc, which treat
237    /// each line as an command-line argument, so `LF` and `CRLF` bytes are not
238    /// valid as an argument for argfile at this moment.
239    /// For example, `RUSTDOCFLAGS="--crate-version foo\nbar" cargo doc` is
240    /// valid when invoking from command-line but not from argfile.
241    ///
242    /// To sum up, the limitations of the argfile are:
243    ///
244    /// - Must be valid UTF-8 encoded.
245    /// - Must not contain any newlines in each argument.
246    ///
247    /// Ref:
248    ///
249    /// - <https://doc.rust-lang.org/rustdoc/command-line-arguments.html#path-load-command-line-flags-from-a-path>
250    /// - <https://doc.rust-lang.org/rustc/command-line-arguments.html#path-load-command-line-flags-from-a-path>
251    pub fn retry_with_argfile(&mut self, enabled: bool) -> &mut Self {
252        self.retry_with_argfile = enabled;
253        self
254    }
255
256    /// Sets a value that will be written to stdin of the process on launch.
257    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    /// Like [`Command::status`] but with a better error message.
267    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    /// Runs the process, waiting for completion, and mapping non-success exit codes to an error.
300    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    /// Replaces the current process with the target process.
315    ///
316    /// On Unix, this executes the process using the Unix syscall `execvp`, which will block
317    /// this process, and will only return if there is an error.
318    ///
319    /// On Windows this isn't technically possible. Instead we emulate it to the best of our
320    /// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler.
321    /// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C
322    /// handling to the application at hand, which will either terminate or handle it itself.
323    /// According to Microsoft's documentation at
324    /// <https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>.
325    /// the Ctrl-C signal is sent to all processes attached to a terminal, which should
326    /// include our child process. If the child terminates then we'll reap them in Cargo
327    /// pretty quickly, and if the child handles the signal then we won't terminate
328    /// (and we shouldn't!) until the process itself later exits.
329    pub fn exec_replace(&self) -> Result<()> {
330        imp::exec_replace(self)
331    }
332
333    /// Like [`Command::output`] but with a better error message.
334    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    /// Executes the process, returning the stdio output, or an error if non-zero exit status.
364    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    /// Executes a command, passing each line of stdout and stderr to the supplied callbacks, which
379    /// can mutate the string data.
380    ///
381    /// If any invocations of these function return an error, it will be propagated.
382    ///
383    /// If `capture_output` is true, then all the output will also be buffered
384    /// and stored in the returned `Output` object. If it is false, no caching
385    /// is done, and the callbacks are solely responsible for handling the
386    /// output.
387    ///
388    /// The returned [`ExecStats`] are populated only when `capture_rusage` is
389    /// set, and are returned even on failure (e.g. an OOM-killed process).
390    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            // The process may have failed to spawn.
479            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    /// Builds the command with an `@<path>` argfile that contains all the
513    /// arguments. This is primarily served for rustc/rustdoc command family.
514    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    /// Builds a command from `ProcessBuilder` for everything but not `args`.
552    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    /// Converts `ProcessBuilder` into a `std::process::Command`, and handles
584    /// the jobserver, if present.
585    ///
586    /// Note that this method doesn't take argfile fallback into account. The
587    /// caller should handle it by themselves.
588    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    /// Wraps an existing command with the provided wrapper, if it is present and valid.
597    ///
598    /// # Examples
599    ///
600    /// ```rust
601    /// use cargo_util::ProcessBuilder;
602    /// // Running this would execute `rustc`
603    /// let cmd = ProcessBuilder::new("rustc");
604    ///
605    /// // Running this will execute `sccache rustc`
606    /// let cmd = cmd.wrapped(Some("sccache"));
607    /// ```
608    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
636/// Forces the command to use `@path` argfile.
637///
638/// You should set `__CARGO_TEST_FORCE_ARGFILE` to enable this.
639fn debug_force_argfile(retry_enabled: bool) -> bool {
640    retry_enabled && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok()
641}
642
643/// Creates new pipes for stderr, stdout, and optionally stdin.
644fn 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    /// Waits for `child` to exit, returning its exit status and the peak rss (in bytes).
702    ///
703    /// Use `wait4` to read `rusage.ru_maxrss` (the kernel's exact high-water mark),
704    /// instead of the plain `waitpid` that [`std::process::Child::wait`] performs,
705    /// which discards the `rusage`.
706    #[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        // SAFETY: All rusage fields are safe to be initialized to zeroes (structs with i64 fields).
713        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
714        loop {
715            // SAFETY: Pid is valid, usage and status are initialized.
716            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        // `ru_maxrss` is in kilobytes on Linux / FreeBSD and bytes on the Darwin family. Refs:
731        // Linux: https://man7.org/linux/man-pages/man2/getrusage.2.html
732        // Darwin: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getrusage.2.html
733        // FreeBSD: https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+14.0-RELEASE&query=getrusage&sektion=2
734        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        // We already reaped the child via `wait4`; `Child`'s `Drop` on Unix does
740        // not wait, so simply dropping it here does not double-reap.
741        Ok((ExitStatus::from_raw(status), Some(peak)))
742    }
743
744    /// Regular wait logic since Solaris/Illumos do not support `wait4`.
745    #[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        // Do nothing; let the child process handle it.
765        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        // Just execute the process as normal.
776        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        // SAFETY: All PROCESS_MEMORY_COUNTERS fields are safe to zero-initialize (plain integers).
794        let mut counters: PROCESS_MEMORY_COUNTERS = unsafe { std::mem::zeroed() };
795        counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
796        // SAFETY: The handle is valid (owned by the not-yet-dropped `child`), `counters` is
797        // initialized, and `cb` is set to its size as `GetProcessMemoryInfo` requires.
798        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}