cross_exec/
lib.rs

1use std::{
2    io,
3    process::{self, Command},
4};
5
6
7/// This trait being unreachable from outside the crate
8/// prevents outside implementations of our extension traits.
9/// This allows adding more trait methods in the future.
10pub(crate) trait Sealed {}
11
12/// Allows extension traits within this crate.
13impl Sealed for Command {}
14
15/// Extensions to the [`process::Command`] builder.
16///
17/// This trait is sealed: it cannot be implemented outside `cross_exec`. This is so that future additional methods are not breaking changes.
18#[allow(private_bounds)]
19pub trait CommandExt: Sealed {
20    ///
21    /// On Unix, this will call [`std::os::unix::process::CommandExt::exec`].
22    ///
23    /// On Windows, this will:
24    ///
25    /// 1. Set a `Ctrl+C` and friends handler that lets the child process handle them.
26    /// 2. Run the command with [`Command::status`]. If it fails to start, return the error.
27    /// 3. Call [`process::exit`] with the exit code of the child process.
28    ///
29    /// On success this function will not return, and otherwise it will return an error indicating why the exec (or another part of the setup of the [`Command`]) failed.
30    ///
31    /// `cross_exec` not returning has the same implications as calling [`process::exit`] – no destructors on the current stack or any other thread’s stack will be run. Therefore, it is recommended to only call `cross_exec` at a point where it is fine to not run any destructors.
32    ///
33    /// This function, unlike `spawn`, will not fork the process to create a new child. Like spawn, however, the default behavior for the stdio descriptors will be to inherit them from the current process.
34    ///
35    /// # Notes
36    ///
37    /// The process may be in a "broken state" if this function returns in error. For example the working directory, environment variables, signal handling settings, various user/group information, or aspects of stdio file descriptors may have changed. If a "transactional spawn" is required to gracefully handle errors it is recommended to use the cross-platform spawn instead.
38    #[must_use]
39    fn cross_exec(&mut self) -> io::Error;
40}
41
42impl CommandExt for Command {
43    #[allow(unreachable_code)]
44    fn cross_exec(&mut self) -> io::Error {
45        #[cfg(unix)]
46        {
47            use std::os::unix::process::CommandExt;
48            return self.exec();
49        }
50
51        #[cfg(windows)]
52        {
53            use std::os::windows::process::CommandExt;
54            use windows::{Win32::System::Console::SetConsoleCtrlHandler, core::BOOL};
55
56            // Ignore Ctrl+C and friends so that the child process can handle them.
57            unsafe extern "system" fn ignore_all(_: u32) -> BOOL {
58                true.into()
59            }
60            let res = unsafe { SetConsoleCtrlHandler(Some(ignore_all), true.into()) };
61            if let Err(e) = res {
62                return io::Error::new(
63                    io::ErrorKind::Other,
64                    format!("failed to set Ctrl+C handler: {}", e),
65                );
66            }
67        }
68
69        // No ? because return type isn't Result or Option,
70        // it's only the E part of Result<T, E>.
71        let res = self.status();
72        let status = match res {
73            Ok(s) => s,
74            Err(e) => return e,
75        };
76        process::exit(status.code().unwrap_or(128));
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use std::error::Error;
84
85    #[test]
86    fn test_example_cargo_wrapper() -> Result<(), Box<dyn Error>> {
87        let expected = {
88            let output = Command::new("cargo").arg("--version").output()?;
89            let stdout = String::from_utf8(output.stdout)?.replace("\r\n", "\n");
90            format!("Hello from before cross_exec!\n{}", stdout)
91        };
92        let actual = {
93            let output = Command::new("cargo")
94                .args(&["run", "--example", "cargo-wrapper", "--", "--version"])
95                .output()?;
96            String::from_utf8(output.stdout)?.replace("\r\n", "\n")
97        };
98        assert_eq!(expected, actual);
99        Ok(())
100    }
101}