From 03bbe52417f131ec721a7c1cc57185019f0298a3 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Thu, 12 Feb 2026 19:14:58 +0100 Subject: [PATCH] signals: Add support for Windows --- CHANGELOG.md | 2 ++ Cargo.lock | 1 + Cargo.toml | 1 + crates/radicle-node/src/main.rs | 9 ----- crates/radicle-node/src/runtime.rs | 1 - crates/radicle-signals/Cargo.toml | 11 +++++- crates/radicle-signals/src/lib.rs | 17 +++++++++ crates/radicle-signals/src/unix.rs | 5 +-- crates/radicle-signals/src/windows.rs | 50 +++++++++++++++++++++++++++ crates/radicle-windows/Cargo.toml | 2 +- 10 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 crates/radicle-signals/src/windows.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fe6e6f..b813a7bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This is fixed by using `winsplit` on Windows instead. - On Windows, zombie `git-upload-pack` processes are now prevented by using the "Job" API of the operating system to group child processes and their children. +- On Windows, signal handling was not supported. The `radicle-node` executable + will now respect signal handling on Windows. ## Deprecations diff --git a/Cargo.lock b/Cargo.lock index d3c3461a..d571a626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3151,6 +3151,7 @@ dependencies = [ "crossbeam-channel", "libc", "signals_receipts", + "windows 0.62.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b5604abb..449d8de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ sqlite = "0.32.0" tempfile = "3.3.0" thiserror = { version = "2", default-features = false } uds_windows = "1.1.0" +windows = "0.62" winpipe = "0.1.1" winsplit = "0.1.0" zeroize = "1.5.7" diff --git a/crates/radicle-node/src/main.rs b/crates/radicle-node/src/main.rs index ddc3edf0..a02e5c6b 100644 --- a/crates/radicle-node/src/main.rs +++ b/crates/radicle-node/src/main.rs @@ -13,7 +13,6 @@ use radicle::profile; use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner}; use radicle_node::fingerprint::{Fingerprint, FingerprintVerification}; use radicle_node::{Runtime, VERSION}; -#[cfg(unix)] use radicle_signals as signals; const HELP_MSG: &str = r#" @@ -320,20 +319,12 @@ fn execute(options: Options) -> Result<(), ExecutionError> { log::warn!(target: "node", "Unable to set process open file limit: {e}"); } - #[cfg(unix)] let signals = { let (notify, signals) = chan::bounded(1); signals::install(notify)?; signals }; - #[cfg(windows)] - let signals = { - let (_, signals) = chan::bounded(1); - log::info!(target: "node", "No signal handlers were installed, because this is not available on Windows."); - signals - }; - if options.force { log::debug!(target: "node", "Removing existing control socket.."); std::fs::remove_file(home.socket()).ok(); diff --git a/crates/radicle-node/src/runtime.rs b/crates/radicle-node/src/runtime.rs index d1b1befe..0e2b8699 100644 --- a/crates/radicle-node/src/runtime.rs +++ b/crates/radicle-node/src/runtime.rs @@ -287,7 +287,6 @@ impl Runtime { || control::listen(listener, handle) }); - #[cfg(unix)] let _signals = thread::spawn(&self.id, "signals", move || loop { use radicle::node::Handle as _; diff --git a/crates/radicle-signals/Cargo.toml b/crates/radicle-signals/Cargo.toml index 821b3311..28a9b5b6 100644 --- a/crates/radicle-signals/Cargo.toml +++ b/crates/radicle-signals/Cargo.toml @@ -8,7 +8,16 @@ edition.workspace = true version = "0.11.0" rust-version.workspace = true -[target.'cfg(unix)'.dependencies] +[dependencies] crossbeam-channel = { workspace = true } + +[target.'cfg(unix)'.dependencies] libc = { workspace = true } signals_receipts = { version = "0.2.0", features = ["channel_notify_facility"] } + +[target.'cfg(windows)'.dependencies.windows] +workspace = true +features = [ + "Win32_Foundation", + "Win32_System_Console", +] \ No newline at end of file diff --git a/crates/radicle-signals/src/lib.rs b/crates/radicle-signals/src/lib.rs index 824c2d9f..9e637802 100644 --- a/crates/radicle-signals/src/lib.rs +++ b/crates/radicle-signals/src/lib.rs @@ -1,9 +1,17 @@ +use std::io; + #[cfg(unix)] mod unix; #[cfg(unix)] pub use unix::*; +#[cfg(windows)] +mod windows; + +#[cfg(windows)] +pub use windows::*; + /// Operating system signal. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Signal { @@ -16,3 +24,12 @@ pub enum Signal { /// `SIGWINCH`. WindowChanged, } + +/// Return an error indicating that signal handling is already installed. +#[inline(always)] +fn already_installed() -> io::Error { + io::Error::new( + io::ErrorKind::AlreadyExists, + "signal handling is already installed", + ) +} diff --git a/crates/radicle-signals/src/unix.rs b/crates/radicle-signals/src/unix.rs index a7e37db1..39030f6a 100644 --- a/crates/radicle-signals/src/unix.rs +++ b/crates/radicle-signals/src/unix.rs @@ -57,10 +57,7 @@ pub fn install(notify: chan::Sender) -> io::Result<()> { } SignalsChannel::install_with_outside_channel(ChanSender(notify)).map_err(|e| match e { - InstallError::AlreadyInstalled { unused_notify: _ } => io::Error::new( - io::ErrorKind::AlreadyExists, - "signal handling is already installed", - ), + InstallError::AlreadyInstalled { unused_notify: _ } => already_installed(), _ => io::Error::other(e), // The error type is non-exhaustive. }) } diff --git a/crates/radicle-signals/src/windows.rs b/crates/radicle-signals/src/windows.rs new file mode 100644 index 00000000..23822595 --- /dev/null +++ b/crates/radicle-signals/src/windows.rs @@ -0,0 +1,50 @@ +use std::io; +use std::sync::OnceLock; + +use crossbeam_channel as chan; + +use ::windows::core::BOOL; +use ::windows::Win32::System::Console::{ + SetConsoleCtrlHandler, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_C_EVENT, CTRL_LOGOFF_EVENT, + CTRL_SHUTDOWN_EVENT, +}; + +use crate::{already_installed, Signal}; + +static NOTIFY: OnceLock> = OnceLock::new(); + +/// Callback function, called by the system when a control signal is to be received. +/// See . +unsafe extern "system" fn handler(ctrltype: u32) -> BOOL { + match ctrltype { + CTRL_C_EVENT | CTRL_BREAK_EVENT | CTRL_CLOSE_EVENT | CTRL_SHUTDOWN_EVENT => { + if let Some(notify) = NOTIFY.get() { + if notify.send(Signal::Terminate).is_ok() { + return true.into(); + } + } else { + // Do nothing, since we do not have a channel to send notifications to. + } + } + CTRL_LOGOFF_EVENT => { + // Do nothing, since we do not know which user is logging off. + } + _ => { + // Do nothing, since we received an unknown control signal. + } + } + + false.into() +} + +/// Install global signal handlers, with notifications sent to the given +/// `notify` channel. +pub fn install(notify: chan::Sender) -> io::Result<()> { + if let Err(_) = NOTIFY.set(notify) { + return Err(already_installed()); + } + + // SAFETY: Our handler function is sane. + let result = unsafe { SetConsoleCtrlHandler(Some(handler), true) }; + result.map_err(|_| io::Error::last_os_error()) +} diff --git a/crates/radicle-windows/Cargo.toml b/crates/radicle-windows/Cargo.toml index 1397c78a..2588243a 100644 --- a/crates/radicle-windows/Cargo.toml +++ b/crates/radicle-windows/Cargo.toml @@ -12,7 +12,7 @@ rust-version.workspace = true thiserror = { workspace = true } [target.'cfg(windows)'.dependencies.windows] -version = "0.62" +workspace = true features = [ "Win32_Foundation", "Win32_Security",