Skip to content

Add try_set_scoped_handler: Scoped Ctrl-C Handler for Non-'static Closures #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ harness = false
name = "main"
path = "tests/main/mod.rs"

[[test]]
harness = false
name = "mod_scoped"
path = "tests/main/mod_scoped.rs"

[[test]]
harness = false
name = "issue_97"
Expand Down
158 changes: 145 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,34 +90,128 @@ static INIT_LOCK: Mutex<()> = Mutex::new(());
///
/// # Panics
/// Any panic in the handler will not be caught and will cause the signal handler thread to stop.
pub fn set_handler<F>(user_handler: F) -> Result<(), Error>
pub fn set_handler<F>(mut user_handler: F) -> Result<(), Error>
where
F: FnMut() + 'static + Send,
{
init_and_set_handler(user_handler, true)
init_and_set_handler(
move || {
user_handler();
false
},
true,
StaticExecutor,
)
}

/// The same as ctrlc::set_handler but errors if a handler already exists for the signal(s).
///
/// # Errors
/// Will return an error if another handler exists or if a system error occurred while setting the
/// handler.
pub fn try_set_handler<F>(user_handler: F) -> Result<(), Error>
pub fn try_set_handler<F>(mut user_handler: F) -> Result<(), Error>
where
F: FnMut() + 'static + Send,
{
init_and_set_handler(user_handler, false)
init_and_set_handler(
move || {
user_handler();
false
},
false,
StaticExecutor,
)
}

/// Register a scoped Ctrl-C signal handler.
///
/// This function registers a Ctrl-C (SIGINT) signal handler using a scoped thread context,
/// allowing the use of non-`'static` closures. This is particularly useful for managing
/// state that lives within the scope of a thread, without requiring `Arc` or other
/// heap-allocated synchronization primitives.
///
/// Unlike [`ctrlc::set_handler`] or [`ctrlc::try_set_handler`], the provided handler does not need to be `'static`,
/// as it is guaranteed not to outlive the given [`std::thread::Scope`].
///
/// # Example
///
/// ```no_run
/// use std::sync::atomic::{AtomicBool, Ordering};
/// use std::thread;
///
/// let flag = AtomicBool::new(false);
/// thread::scope(|s| {
/// ctrlc::try_set_scoped_handler(s, || {
/// // Because the handler is scoped, we can use non-'static references.
/// flag.store(true, Ordering::SeqCst);
/// true // Returning `true` ensures the handler will not be invoked again.
/// }).unwrap();
///
/// // Do some work...
/// });
/// ```
///
/// > **Note**: Unlike `set_handler`, this function requires that the signal handler
/// > eventually terminate. If the handler returns `false`, the signal handler thread
/// > continues running, and the enclosing scope will never complete. Always ensure that
/// > the handler returns `true` at some point.
///
/// # Semantics
///
/// - The handler must return a `bool`, indicating whether the handler should be de-registered:
/// - `true`: the handler is removed and will not be called again.
/// - `false`: the handler remains active and will be called again on subsequent signals.
/// - This design ensures that the enclosing thread scope can only exit once the handler
/// has completed and returned `true`.
///
/// # Limitations
///
/// - Only one scoped handler may be registered per process.
/// - If a handler is already registered (scoped or static), this function will return an error.
/// - There is **no** `set_scoped_handler`; a scoped handler cannot be replaced once registered,
/// even if it has already finished executing.
///
/// # Errors
///
/// Returns an error if:
/// - A handler is already registered (scoped or static).
/// - A system-level error occurs during signal handler installation.
///
/// # Panics
///
/// If the handler panics, the signal handling thread will terminate and not be restarted. This
/// may leave the program in a state where no Ctrl-C handler is installed.
///
/// # Safety
///
/// The handler is executed in a separate thread, so ensure that shared state is synchronized
/// appropriately.
///
/// See also: [`try_set_handler`] for a `'static` version of this API.
pub fn try_set_scoped_handler<'scope, 'f: 'scope, 'env, F>(
scope: &'scope thread::Scope<'scope, 'env>,
user_handler: F,
) -> Result<(), Error>
where
F: FnMut() -> bool + 'f + Send,
{
init_and_set_handler(user_handler, false, ScopedExecutor { scope })
}

fn init_and_set_handler<F>(user_handler: F, overwrite: bool) -> Result<(), Error>
fn init_and_set_handler<'scope, 'f: 'scope, F, E>(
user_handler: F,
overwrite: bool,
executor: E,
) -> Result<(), Error>
where
F: FnMut() + 'static + Send,
F: FnMut() -> bool + 'f + Send,
E: Executor<'scope>,
{
if !INIT.load(Ordering::Acquire) {
let _guard = INIT_LOCK.lock().unwrap();

if !INIT.load(Ordering::Relaxed) {
set_handler_inner(user_handler, overwrite)?;
set_handler_inner(user_handler, overwrite, executor)?;
INIT.store(true, Ordering::Release);
return Ok(());
}
Expand All @@ -126,23 +220,61 @@ where
Err(Error::MultipleHandlers)
}

fn set_handler_inner<F>(mut user_handler: F, overwrite: bool) -> Result<(), Error>
fn set_handler_inner<'scope, 'f: 'scope, F, E>(
mut user_handler: F,
overwrite: bool,
executor: E,
) -> Result<(), Error>
where
F: FnMut() + 'static + Send,
F: FnMut() -> bool + 'f + Send,
E: Executor<'scope>,
{
unsafe {
platform::init_os_handler(overwrite)?;
}

thread::Builder::new()
.name("ctrl-c".into())
.spawn(move || loop {
let builder = thread::Builder::new().name("ctrl-c".into());
executor
.spawn(builder, move || loop {
unsafe {
platform::block_ctrl_c().expect("Critical system error while waiting for Ctrl-C");
}
user_handler();
let finished = user_handler();
if finished {
break;
}
})
.map_err(Error::System)?;

Ok(())
}

trait Executor<'scope> {
fn spawn<F>(self, builder: thread::Builder, f: F) -> Result<(), std::io::Error>
where
F: FnOnce() + Send + 'scope;
}

struct ScopedExecutor<'scope, 'env: 'scope> {
scope: &'scope thread::Scope<'scope, 'env>,
}
impl<'scope, 'env: 'scope> Executor<'scope> for ScopedExecutor<'scope, 'env> {
fn spawn<F>(self, builder: thread::Builder, f: F) -> Result<(), std::io::Error>
where
F: FnOnce() + Send + 'scope,
{
builder.spawn_scoped(self.scope, f)?;
Ok(())
}
}

struct StaticExecutor;
impl Executor<'static> for StaticExecutor {
fn spawn<F>(self, builder: thread::Builder, f: F) -> Result<(), std::io::Error>
where
F: FnOnce() + Send + 'static,
{
builder.spawn(f)?;
Ok(())
}
}
48 changes: 48 additions & 0 deletions tests/main/mod_scoped.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2023 CtrlC developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.

#[macro_use]
mod harness;
use harness::{platform, run_harness};

use std::{
sync::atomic::{AtomicBool, Ordering},
thread,
};

fn test_set_scoped_handler() {
let flag = AtomicBool::new(false);
thread::scope(|s| {
ctrlc::try_set_scoped_handler(s, || {
flag.store(true, Ordering::SeqCst);
true
})
.unwrap();

unsafe {
platform::raise_ctrl_c();
}

std::thread::sleep(std::time::Duration::from_millis(100));
assert!(flag.load(Ordering::SeqCst));

match ctrlc::try_set_scoped_handler(s, || true) {
Err(ctrlc::Error::MultipleHandlers) => {}
ret => panic!("{:?}", ret),
}
})
}

fn tests() {
run_tests!(test_set_scoped_handler);
}

fn main() {
run_harness(tests);
}