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