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