diff --git a/Cargo.lock b/Cargo.lock index c0fce330..7862ec24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3207,6 +3207,14 @@ dependencies = [ "zeroize", ] +[[package]] +name = "radicle-windows" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.17", + "windows 0.62.2", +] + [[package]] name = "rand" version = "0.8.5" @@ -4798,6 +4806,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -4813,13 +4842,37 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -4831,6 +4884,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -4842,12 +4906,33 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -4857,16 +4942,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4950,6 +5053,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5107,7 +5219,7 @@ dependencies = [ "log", "rand 0.8.5", "sync-ptr", - "windows", + "windows 0.58.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7c0ac0cc..4e8f380b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ radicle-signals = { version = "0.11", path = "crates/radicle-signals" } radicle-ssh = { version = "0.10", path = "crates/radicle-ssh", default-features = false } radicle-systemd = { version = "0.11", path = "crates/radicle-systemd" } radicle-term = { version = "0.16", path = "crates/radicle-term" } +radicle-windows = { version = "0.1", path = "crates/radicle-windows" } schemars = { version = "1.0.4", default-features = false } serde = { version = "1.0", default-features = false } serde_json = "1.0" diff --git a/crates/radicle-windows/Cargo.toml b/crates/radicle-windows/Cargo.toml new file mode 100644 index 00000000..1397c78a --- /dev/null +++ b/crates/radicle-windows/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "radicle-windows" +description = "Radicle integration with Windows" +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +version = "0.1.0" +rust-version.workspace = true + +[target.'cfg(windows)'.dependencies] +thiserror = { workspace = true } + +[target.'cfg(windows)'.dependencies.windows] +version = "0.62" +features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_JobObjects", +] + +[package.metadata.docs.rs] +default-target = "x86_64-pc-windows-msvc" diff --git a/crates/radicle-windows/src/jobs.rs b/crates/radicle-windows/src/jobs.rs new file mode 100644 index 00000000..82ba3863 --- /dev/null +++ b/crates/radicle-windows/src/jobs.rs @@ -0,0 +1,153 @@ +use std::io; +use std::os::windows::io::AsRawHandle as _; + +use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{CloseHandle, HANDLE}, + System::JobObjects::{AssignProcessToJobObject, CreateJobObjectW, TerminateJobObject}, + }, +}; + +use thiserror::Error; + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error("Failed to create job: {0}")] + Create(io::Error), + #[error("Failed to assign job: {0}")] + Assign(io::Error), + #[error("Failed to terminate job: {0}")] + Terminate(io::Error), +} + +impl From for io::Error { + fn from(value: Error) -> Self { + use Error::*; + match value { + Create(error) | Assign(error) | Terminate(error) => error, + } + } +} + +/// Wraps a handle to a job object. +/// See also . +#[derive(Debug)] +#[repr(transparent)] +pub struct Job { + handle: HANDLE, +} + +impl Job { + /// Create a new, empty, job object. + /// See also . + pub fn new() -> Result { + #[allow(non_snake_case)] + // To match the naming in the documentation of `CreateJobObjectW` for clarity. + let lpJobAttributes = None; + + #[allow(non_snake_case)] + // To match the naming in the documentation of `CreateJobObjectW` for clarity. + let lpName = PCWSTR::null(); + + // SAFETY: We argue that calling `CrateJobObjectW` is safe based on + // its documentation (see documentation of this function): + // Both parameters take their `null`/empty value. + // For `lpJobAttributes`, this is immediate as we use `None`, and the + // wrapper in the `windows` crate ensures safety. + // For `lpName`, we pass `name`, which was initialized to + // `PCWSTR::null()` (see above) which we rely on to obtain a sane + // `null` value. + // For the case of `null`/empty arguments, the documentation of + // `CreateJobObjectW` does not specify any further preconditions. + let result = unsafe { CreateJobObjectW(lpJobAttributes, lpName) }; + + match result { + Ok(handle) => Ok(Self { handle }), + Err(e) => Err(Error::Create(e.into())), + } + } + + /// Assign a process to the job object. + /// See also . + pub fn assign(&self, child: &std::process::Child) -> Result<(), Error> { + #[allow(non_snake_case)] + // To match the naming in the documentation of `AssignProcessToJobObject` for clarity. + let hJob = self.handle; + + #[allow(non_snake_case)] + // To match the naming in the documentation of `AssignProcessToJobObject` for clarity. + let hProcess = HANDLE(child.as_raw_handle()); + + // SAFETY: We argue that calling `AssignProcessToJobObject` is safe based on + // its documentation (see documentation of this function): + // First, we argue that the argument + // For `hJob`, consider that its value is the same as `self.handle` (see above), + // and that `self.handle` is only assigned in `Self::new` via a call to `CreateJobObjectW`. + // For `hProcess`, we pass the raw handle of the child process we were given. + // Thus, we rely on `impl std::os::windows::io::AsRawHandle for std::process::Child` + // for its value to be a valid handle to a process. + // Note that the documentation of `AssignProcessToJobObject` specifies further preconditions + // on the arguments, especially concerning: + // - Job Object Security (see ) + // - Process Security and Access Rights (see ) + // We assume that violations of these preconditions will be reflected in + // the return value of `AssignProcessToJobObject`, and will not result in safety violations. + let result = unsafe { AssignProcessToJobObject(hJob, hProcess) }; + + result.map_err(|e| Error::Assign(e.into())) + } + + /// Terminate a job object. + /// See also . + pub fn terminate(self, exit_code: u32) -> Result<(), Error> { + #[allow(non_snake_case)] + // To match the naming in the documentation of `TerminateJobObject` for clarity. + let hJob = self.handle; + + #[allow(non_snake_case)] + // To match the naming in the documentation of `TerminateJobObject` for clarity. + let uExitCode = exit_code; + + // SAFETY: We argue that calling `TerminateJobObject` is safe based on + // its documentation (see documentation of this function): + // For `hJob`, consider that its value is the same as `self.handle`, + // and that `self.handle` is only assigned in `Self::new` via a call to `CreateJobObjectW`. + // For `uExitCode`, there are no preconditions to satisfy. + // Note that the documentation of `TerminateJobObject` specifies further preconditions + // on the arguments, especially concerning: + // - Job Object Security (see ) + // - Process Security and Access Rights (see ) + // We assume that violations of these preconditions will be reflected in + // the return value of `AssignProcessToJobObject`, and will not result in safety violations. + let result = unsafe { TerminateJobObject(hJob, uExitCode) }; + + result.map_err(|e| Error::Terminate(e.into())) + } + + /// Convenience method to create a new job and assign a child process to it. + /// See also [`Job::new`] and [`Job::assign`]. + pub fn for_child(child: &std::process::Child) -> Result { + let job = Self::new()?; + job.assign(child)?; + Ok(job) + } +} + +impl Drop for Job { + /// Close the handle to the job object. + /// See also . + fn drop(&mut self) { + #[allow(non_snake_case)] + // To match the naming in the documentation of `CloseHandle` for clarity. + let hObject = self.handle; + + // SAFETY: We argue that calling `CloseHandle` is safe based on + // its documentation (see documentation of this function): + // For `hObject`, consider that its value is the same as `self.handle`, + // and that `self.handle` is only assigned in `Self::new` via a call to `CreateJobObjectW`, + // thus is a valid handle to a job object. + let _ = unsafe { CloseHandle(hObject) }; + } +} diff --git a/crates/radicle-windows/src/lib.rs b/crates/radicle-windows/src/lib.rs new file mode 100644 index 00000000..ca26c063 --- /dev/null +++ b/crates/radicle-windows/src/lib.rs @@ -0,0 +1,4 @@ +//! Library for interaction with Windows, specialized for Radicle. + +#[cfg(windows)] +pub mod jobs;