Skip to main content

cargo/util/
job.rs

1//! Job management (mostly for windows)
2//!
3//! Most of the time when you're running cargo you expect Ctrl-C to actually
4//! terminate the entire tree of processes in play, not just the one at the top
5//! (cargo). This currently works "by default" on Unix platforms because Ctrl-C
6//! actually sends a signal to the *process group* rather than the parent
7//! process, so everything will get torn down. On Windows, however, this does
8//! not happen and Ctrl-C just kills cargo.
9//!
10//! To achieve the same semantics on Windows we use Job Objects to ensure that
11//! processes in the job die at the same time. Job objects have a mode of operation
12//! where when all handles to the object are closed it causes all child
13//! processes associated with the object to be terminated immediately.
14//! Conveniently, whenever a process in the job object spawns a new process the
15//! child will be associated with the job object as well, unless it is created
16//! with CREATE_BREAKAWAY_FROM_JOB. This means if we add ourselves to the job
17//! object we create then all processes remaining in the job will get torn down!
18
19pub use self::imp::Setup;
20
21pub fn setup() -> Option<Setup> {
22    unsafe { imp::setup() }
23}
24
25#[cfg(unix)]
26mod imp {
27    use std::env;
28
29    pub type Setup = ();
30
31    pub unsafe fn setup() -> Option<()> {
32        // There's a test case for the behavior of
33        // when-cargo-is-killed-subprocesses-are-also-killed, but that requires
34        // one cargo spawned to become its own session leader, so we do that
35        // here.
36        #[expect(
37            clippy::disallowed_methods,
38            reason = "testing only, no reason for config support"
39        )]
40        if env::var("__CARGO_TEST_SETSID_PLEASE_DONT_USE_ELSEWHERE").is_ok() {
41            // SAFETY: I'm unaware of any safety requirements for this function.
42            unsafe {
43                libc::setsid();
44            }
45        }
46        Some(())
47    }
48}
49
50#[cfg(windows)]
51mod imp {
52    use std::io;
53    use std::mem;
54    use std::ptr;
55    use std::ptr::addr_of;
56
57    use tracing::info;
58
59    use windows_sys::Win32::Foundation::CloseHandle;
60    use windows_sys::Win32::Foundation::HANDLE;
61    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
62    use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
63    use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
64    use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_BREAKAWAY_OK;
65    use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
66    use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
67    use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
68    use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
69    use windows_sys::Win32::System::Threading::GetCurrentProcess;
70
71    pub struct Setup {
72        job: Handle,
73    }
74
75    pub struct Handle {
76        inner: HANDLE,
77    }
78
79    fn last_err() -> io::Error {
80        io::Error::last_os_error()
81    }
82
83    pub unsafe fn setup() -> Option<Setup> {
84        // Creates a new job object for us to use and then adds ourselves to it.
85        // Note that all errors are basically ignored in this function,
86        // intentionally. Job objects are "relatively new" in Windows,
87        // particularly the ability to support nested job objects. Older
88        // Windows installs don't support this ability. We probably don't want
89        // to force Cargo to abort in this situation or force others to *not*
90        // use job objects, so we instead just ignore errors and assume that
91        // we're otherwise part of someone else's job object in this case.
92
93        let job = unsafe { CreateJobObjectW(ptr::null_mut(), ptr::null()) };
94        if job == INVALID_HANDLE_VALUE {
95            return None;
96        }
97        let job = Handle { inner: job };
98
99        // Indicate that when all handles to the job object are gone that all
100        // processes in the object should be killed. Note that this includes our
101        // entire process tree by default because we've added ourselves and
102        // our children will reside in the job once we spawn a process.
103        let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
104        info = unsafe { mem::zeroed() };
105        info.BasicLimitInformation.LimitFlags =
106            JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
107        let r = unsafe {
108            SetInformationJobObject(
109                job.inner,
110                JobObjectExtendedLimitInformation,
111                addr_of!(info) as *const _,
112                mem::size_of_val(&info) as u32,
113            )
114        };
115        if r == 0 {
116            return None;
117        }
118
119        // Assign our process to this job object, meaning that our children will
120        // now live or die based on our existence unless they break away from the job.
121        let me = unsafe { GetCurrentProcess() };
122        let r = unsafe { AssignProcessToJobObject(job.inner, me) };
123        if r == 0 {
124            return None;
125        }
126
127        Some(Setup { job })
128    }
129
130    impl Drop for Setup {
131        fn drop(&mut self) {
132            // On normal exits (not ctrl-c), we don't want to kill any child
133            // processes. The destructor here configures our job object to
134            // **not** kill everything on close, then closes the job object.
135            unsafe {
136                let info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
137                info = mem::zeroed();
138                let r = SetInformationJobObject(
139                    self.job.inner,
140                    JobObjectExtendedLimitInformation,
141                    addr_of!(info) as *const _,
142                    mem::size_of_val(&info) as u32,
143                );
144                if r == 0 {
145                    info!("failed to configure job object to defaults: {}", last_err());
146                }
147            }
148        }
149    }
150
151    impl Drop for Handle {
152        fn drop(&mut self) {
153            unsafe {
154                CloseHandle(self.inner);
155            }
156        }
157    }
158}