signals: Add support for Windows

This commit is contained in:
Lorenz Leutgeb 2026-02-12 19:14:58 +01:00 committed by Fintan Halpenny
parent 865abd35e2
commit 03bbe52417
10 changed files with 83 additions and 16 deletions

View File

@ -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. This is fixed by using `winsplit` on Windows instead.
- On Windows, zombie `git-upload-pack` processes are now prevented by using the - 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. "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 ## Deprecations

1
Cargo.lock generated
View File

@ -3151,6 +3151,7 @@ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"libc", "libc",
"signals_receipts", "signals_receipts",
"windows 0.62.2",
] ]
[[package]] [[package]]

View File

@ -70,6 +70,7 @@ sqlite = "0.32.0"
tempfile = "3.3.0" tempfile = "3.3.0"
thiserror = { version = "2", default-features = false } thiserror = { version = "2", default-features = false }
uds_windows = "1.1.0" uds_windows = "1.1.0"
windows = "0.62"
winpipe = "0.1.1" winpipe = "0.1.1"
winsplit = "0.1.0" winsplit = "0.1.0"
zeroize = "1.5.7" zeroize = "1.5.7"

View File

@ -13,7 +13,6 @@ use radicle::profile;
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner}; use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
use radicle_node::fingerprint::{Fingerprint, FingerprintVerification}; use radicle_node::fingerprint::{Fingerprint, FingerprintVerification};
use radicle_node::{Runtime, VERSION}; use radicle_node::{Runtime, VERSION};
#[cfg(unix)]
use radicle_signals as signals; use radicle_signals as signals;
const HELP_MSG: &str = r#" 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}"); log::warn!(target: "node", "Unable to set process open file limit: {e}");
} }
#[cfg(unix)]
let signals = { let signals = {
let (notify, signals) = chan::bounded(1); let (notify, signals) = chan::bounded(1);
signals::install(notify)?; signals::install(notify)?;
signals 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 { if options.force {
log::debug!(target: "node", "Removing existing control socket.."); log::debug!(target: "node", "Removing existing control socket..");
std::fs::remove_file(home.socket()).ok(); std::fs::remove_file(home.socket()).ok();

View File

@ -287,7 +287,6 @@ impl Runtime {
|| control::listen(listener, handle) || control::listen(listener, handle)
}); });
#[cfg(unix)]
let _signals = thread::spawn(&self.id, "signals", move || loop { let _signals = thread::spawn(&self.id, "signals", move || loop {
use radicle::node::Handle as _; use radicle::node::Handle as _;

View File

@ -8,7 +8,16 @@ edition.workspace = true
version = "0.11.0" version = "0.11.0"
rust-version.workspace = true rust-version.workspace = true
[target.'cfg(unix)'.dependencies] [dependencies]
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true } libc = { workspace = true }
signals_receipts = { version = "0.2.0", features = ["channel_notify_facility"] } signals_receipts = { version = "0.2.0", features = ["channel_notify_facility"] }
[target.'cfg(windows)'.dependencies.windows]
workspace = true
features = [
"Win32_Foundation",
"Win32_System_Console",
]

View File

@ -1,9 +1,17 @@
use std::io;
#[cfg(unix)] #[cfg(unix)]
mod unix; mod unix;
#[cfg(unix)] #[cfg(unix)]
pub use unix::*; pub use unix::*;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use windows::*;
/// Operating system signal. /// Operating system signal.
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Signal { pub enum Signal {
@ -16,3 +24,12 @@ pub enum Signal {
/// `SIGWINCH`. /// `SIGWINCH`.
WindowChanged, 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",
)
}

View File

@ -57,10 +57,7 @@ pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
} }
SignalsChannel::install_with_outside_channel(ChanSender(notify)).map_err(|e| match e { SignalsChannel::install_with_outside_channel(ChanSender(notify)).map_err(|e| match e {
InstallError::AlreadyInstalled { unused_notify: _ } => io::Error::new( InstallError::AlreadyInstalled { unused_notify: _ } => already_installed(),
io::ErrorKind::AlreadyExists,
"signal handling is already installed",
),
_ => io::Error::other(e), // The error type is non-exhaustive. _ => io::Error::other(e), // The error type is non-exhaustive.
}) })
} }

View File

@ -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<chan::Sender<Signal>> = OnceLock::new();
/// Callback function, called by the system when a control signal is to be received.
/// See <https://learn.microsoft.com/en-us/windows/console/handlerroutine>.
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<Signal>) -> 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())
}

View File

@ -12,7 +12,7 @@ rust-version.workspace = true
thiserror = { workspace = true } thiserror = { workspace = true }
[target.'cfg(windows)'.dependencies.windows] [target.'cfg(windows)'.dependencies.windows]
version = "0.62" workspace = true
features = [ features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_Security", "Win32_Security",