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}