Repository: astral-sh/uv
File: crates/uv-windows/src/spawn.rs (Rust)

Code before:
use crate::{Job, install_ctrl_handler};

/// Process creation flag to prevent creating a console window.
const CREATE_NO_WINDOW: u32 = 0x0800_0000;

/// Spawns a child process using Job Objects to ensure proper cleanup.
///
/// When the parent process is terminated (e.g., by Task Scheduler), the child process
/// will also be terminated because it's associated with a Job Object that has
/// `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` set.
///
/// This function does not return on success - it calls `std::process::exit` with the
/// child's exit code.
#[allow(unsafe_code)]
pub fn spawn_child(cmd: &mut Command, hide_console: bool) -> std::io::Result<Infallible> {
    cmd.stdin(std::process::Stdio::inherit());

    if hide_console {
        cmd.creation_flags(CREATE_NO_WINDOW);
    }

    let child = cmd.spawn()?;

    // SAFETY: `into_raw_handle()` returns a valid process handle that we own.
    // We are responsible for closing it, which we do at the end of this function.
    let child_handle = HANDLE(child.into_raw_handle());

    let job = Job::new().map_err(|e| std::io::Error::other(e.to_string()))?;

    // SAFETY: child_handle is a valid process handle from spawn().
    unsafe { job.assign_process(child_handle) }
        .map_err(|e| std::io::Error::other(e.to_string()))?;

    // Ignore control-C/control-Break/logout/etc.; the same event will be delivered
    // to the child, so we let them decide whether to exit or not.
    let _ = install_ctrl_handler();

    // SAFETY: child_handle is a valid process handle. INFINITE means wait forever.
    let wait_result = unsafe { WaitForSingleObject(child_handle, INFINITE) };
    if wait_result != WAIT_OBJECT_0 {
        return Err(std::io::Error::other(format!(
            "WaitForSingleObject failed with result: {wait_result:?}"
        )));
    }

    // SAFETY: child_handle is valid and exit_code points to valid memory.
    let mut exit_code = 0u32;
    unsafe { GetExitCodeProcess(child_handle, &raw mut exit_code) }
        .map_err(|e| std::io::Error::other(format!("Failed to get exit code: {e}")))?;

    // SAFETY: child_handle is valid and we're done using it.

Review comment (suggestion):
Rather than turning `Child` into a raw handle here. We should keep it as `Child` and just use `as_raw_handle()` where we need to. `job` should hold onto the handle as a "reference". 

The problem is that right now if `Job::new` errors then we leak the handle.

The safety warning would just need to note the fact that anything which holds references to this handle (e.g. `Job`) will need to be dropped before child (which is guaranteed by it being instantiated after child).

There's a way to make this even more robust by tying the lifetimes together (probably cleanest with some kind of scope guard). But I am not sure it's worth it for the 2 places we call this function.

Code after:
};

use crate::job::JobError;
use crate::{Job, install_ctrl_handler};

/// A child process supervised by a [`Job`] object.
///
/// The [`BorrowedHandle`] ties the lifetime of the job to the child process,
/// ensuring the job cannot outlive the child's process handle.
struct SupervisedChild<'a> {
    _job: Job,
    handle: BorrowedHandle<'a>,
}

#[allow(unsafe_code)]
impl<'a> SupervisedChild<'a> {
    /// Creates a new supervised child by assigning it to a job object.
    fn new(child: &'a Child) -> Result<Self, JobError> {
        let job = Job::new()?;
        let handle = child.as_handle();
        // SAFETY: The borrowed handle is valid for `'a` because we hold a
        // reference to `child`.
        unsafe { job.assign_process(HANDLE(handle.as_raw_handle())) }?;
        Ok(Self { _job: job, handle })
    }

    /// Returns the raw Windows `HANDLE` for the child process.
    fn raw_handle(&self) -> HANDLE {
        HANDLE(self.handle.as_raw_handle())
    }
}

/// Spawns a child process using Job Objects to ensure proper cleanup.
///
/// When the parent process is terminated (e.g., by Task Scheduler), the child process
/// will also be terminated because it's associated with a Job Object that has
/// `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` set.
///
/// This function does not return on success - it calls `std::process::exit` with the
/// child's exit code.
#[allow(unsafe_code)]
pub fn spawn_child(cmd: &mut Command, hide_console: bool) -> std::io::Result<Infallible> {
    cmd.stdin(std::process::Stdio::inherit());

    if hide_console {
        cmd.creation_flags(CREATE_NO_WINDOW.0);
    }

    let child = cmd.spawn()?;

    let supervised =