Skip to content

Commit 4434d55

Browse files
committed
Add try_set_scoped_handler: scoped Ctrl-C handler supporting non-'static closures'
1 parent b9cf4cf commit 4434d55

File tree

3 files changed

+199
-13
lines changed

3 files changed

+199
-13
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ harness = false
3131
name = "main"
3232
path = "tests/main/mod.rs"
3333

34+
[[test]]
35+
harness = false
36+
name = "mod_scoped"
37+
path = "tests/main/mod_scoped.rs"
38+
3439
[[test]]
3540
harness = false
3641
name = "issue_97"

src/lib.rs

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,34 +90,128 @@ static INIT_LOCK: Mutex<()> = Mutex::new(());
9090
///
9191
/// # Panics
9292
/// Any panic in the handler will not be caught and will cause the signal handler thread to stop.
93-
pub fn set_handler<F>(user_handler: F) -> Result<(), Error>
93+
pub fn set_handler<F>(mut user_handler: F) -> Result<(), Error>
9494
where
9595
F: FnMut() + 'static + Send,
9696
{
97-
init_and_set_handler(user_handler, true)
97+
init_and_set_handler(
98+
move || {
99+
user_handler();
100+
false
101+
},
102+
true,
103+
StaticExecutor,
104+
)
98105
}
99106

100107
/// The same as ctrlc::set_handler but errors if a handler already exists for the signal(s).
101108
///
102109
/// # Errors
103110
/// Will return an error if another handler exists or if a system error occurred while setting the
104111
/// handler.
105-
pub fn try_set_handler<F>(user_handler: F) -> Result<(), Error>
112+
pub fn try_set_handler<F>(mut user_handler: F) -> Result<(), Error>
106113
where
107114
F: FnMut() + 'static + Send,
108115
{
109-
init_and_set_handler(user_handler, false)
116+
init_and_set_handler(
117+
move || {
118+
user_handler();
119+
false
120+
},
121+
false,
122+
StaticExecutor,
123+
)
124+
}
125+
126+
/// Register a scoped Ctrl-C signal handler.
127+
///
128+
/// This function registers a Ctrl-C (SIGINT) signal handler using a scoped thread context,
129+
/// allowing the use of non-`'static` closures. This is particularly useful for managing
130+
/// state that lives within the scope of a thread, without requiring `Arc` or other
131+
/// heap-allocated synchronization primitives.
132+
///
133+
/// Unlike [`ctrlc::set_handler`] or [`ctrlc::try_set_handler`], the provided handler does not need to be `'static`,
134+
/// as it is guaranteed not to outlive the given [`std::thread::Scope`].
135+
///
136+
/// # Example
137+
///
138+
/// ```no_run
139+
/// use std::sync::atomic::{AtomicBool, Ordering};
140+
/// use std::thread;
141+
///
142+
/// let flag = AtomicBool::new(false);
143+
/// thread::scope(|s| {
144+
/// ctrlc::try_set_scoped_handler(s, || {
145+
/// // Because the handler is scoped, we can use non-'static references.
146+
/// flag.store(true, Ordering::SeqCst);
147+
/// true // Returning `true` ensures the handler will not be invoked again.
148+
/// }).unwrap();
149+
///
150+
/// // Do some work...
151+
/// });
152+
/// ```
153+
///
154+
/// > **Note**: Unlike `set_handler`, this function requires that the signal handler
155+
/// > eventually terminate. If the handler returns `false`, the signal handler thread
156+
/// > continues running, and the enclosing scope will never complete. Always ensure that
157+
/// > the handler returns `true` at some point.
158+
///
159+
/// # Semantics
160+
///
161+
/// - The handler must return a `bool`, indicating whether the handler should be de-registered:
162+
/// - `true`: the handler is removed and will not be called again.
163+
/// - `false`: the handler remains active and will be called again on subsequent signals.
164+
/// - This design ensures that the enclosing thread scope can only exit once the handler
165+
/// has completed and returned `true`.
166+
///
167+
/// # Limitations
168+
///
169+
/// - Only one scoped handler may be registered per process.
170+
/// - If a handler is already registered (scoped or static), this function will return an error.
171+
/// - There is **no** `set_scoped_handler`; a scoped handler cannot be replaced once registered,
172+
/// even if it has already finished executing.
173+
///
174+
/// # Errors
175+
///
176+
/// Returns an error if:
177+
/// - A handler is already registered (scoped or static).
178+
/// - A system-level error occurs during signal handler installation.
179+
///
180+
/// # Panics
181+
///
182+
/// If the handler panics, the signal handling thread will terminate and not be restarted. This
183+
/// may leave the program in a state where no Ctrl-C handler is installed.
184+
///
185+
/// # Safety
186+
///
187+
/// The handler is executed in a separate thread, so ensure that shared state is synchronized
188+
/// appropriately.
189+
///
190+
/// See also: [`try_set_handler`] for a `'static` version of this API.
191+
pub fn try_set_scoped_handler<'scope, 'f: 'scope, 'env, F>(
192+
scope: &'scope thread::Scope<'scope, 'env>,
193+
user_handler: F,
194+
) -> Result<(), Error>
195+
where
196+
F: FnMut() -> bool + 'f + Send,
197+
{
198+
init_and_set_handler(user_handler, false, ScopedExecutor { scope })
110199
}
111200

112-
fn init_and_set_handler<F>(user_handler: F, overwrite: bool) -> Result<(), Error>
201+
fn init_and_set_handler<'scope, 'f: 'scope, F, E>(
202+
user_handler: F,
203+
overwrite: bool,
204+
executor: E,
205+
) -> Result<(), Error>
113206
where
114-
F: FnMut() + 'static + Send,
207+
F: FnMut() -> bool + 'f + Send,
208+
E: Executor<'scope>,
115209
{
116210
if !INIT.load(Ordering::Acquire) {
117211
let _guard = INIT_LOCK.lock().unwrap();
118212

119213
if !INIT.load(Ordering::Relaxed) {
120-
set_handler_inner(user_handler, overwrite)?;
214+
set_handler_inner(user_handler, overwrite, executor)?;
121215
INIT.store(true, Ordering::Release);
122216
return Ok(());
123217
}
@@ -126,23 +220,62 @@ where
126220
Err(Error::MultipleHandlers)
127221
}
128222

129-
fn set_handler_inner<F>(mut user_handler: F, overwrite: bool) -> Result<(), Error>
223+
fn set_handler_inner<'scope, 'f: 'scope, F, E>(
224+
mut user_handler: F,
225+
overwrite: bool,
226+
executor: E,
227+
) -> Result<(), Error>
130228
where
131-
F: FnMut() + 'static + Send,
229+
F: FnMut() -> bool + 'f + Send,
230+
E: Executor<'scope>,
132231
{
133232
unsafe {
134233
platform::init_os_handler(overwrite)?;
135234
}
136235

137-
thread::Builder::new()
138-
.name("ctrl-c".into())
139-
.spawn(move || loop {
236+
let builder = thread::Builder::new().name("ctrl-c".into());
237+
executor
238+
.spawn(builder, move || loop {
140239
unsafe {
141240
platform::block_ctrl_c().expect("Critical system error while waiting for Ctrl-C");
142241
}
143-
user_handler();
242+
let finished = user_handler();
243+
if finished {
244+
break;
245+
}
144246
})
145247
.map_err(Error::System)?;
146248

147249
Ok(())
148250
}
251+
252+
trait Executor<'scope> {
253+
/// クロージャをスレッド/スコープに飛ばす
254+
fn spawn<F>(self, builder: thread::Builder, f: F) -> Result<(), std::io::Error>
255+
where
256+
F: FnOnce() + Send + 'scope;
257+
}
258+
259+
struct ScopedExecutor<'scope, 'env: 'scope> {
260+
scope: &'scope thread::Scope<'scope, 'env>,
261+
}
262+
impl<'scope, 'env: 'scope> Executor<'scope> for ScopedExecutor<'scope, 'env> {
263+
fn spawn<F>(self, builder: thread::Builder, f: F) -> Result<(), std::io::Error>
264+
where
265+
F: FnOnce() + Send + 'scope,
266+
{
267+
builder.spawn_scoped(self.scope, f)?;
268+
Ok(())
269+
}
270+
}
271+
272+
struct StaticExecutor;
273+
impl Executor<'static> for StaticExecutor {
274+
fn spawn<F>(self, builder: thread::Builder, f: F) -> Result<(), std::io::Error>
275+
where
276+
F: FnOnce() + Send + 'static,
277+
{
278+
builder.spawn(f)?;
279+
Ok(())
280+
}
281+
}

tests/main/mod_scoped.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2023 CtrlC developers
2+
// Licensed under the Apache License, Version 2.0
3+
// <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5+
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6+
// at your option. All files in the project carrying such
7+
// notice may not be copied, modified, or distributed except
8+
// according to those terms.
9+
10+
#[macro_use]
11+
mod harness;
12+
use harness::{platform, run_harness};
13+
14+
use std::{
15+
sync::atomic::{AtomicBool, Ordering},
16+
thread,
17+
};
18+
19+
fn test_set_scoped_handler() {
20+
let flag = AtomicBool::new(false);
21+
thread::scope(|s| {
22+
ctrlc::try_set_scoped_handler(s, || {
23+
flag.store(true, Ordering::SeqCst);
24+
true
25+
})
26+
.unwrap();
27+
28+
unsafe {
29+
platform::raise_ctrl_c();
30+
}
31+
32+
std::thread::sleep(std::time::Duration::from_millis(100));
33+
assert!(flag.load(Ordering::SeqCst));
34+
35+
match ctrlc::try_set_scoped_handler(s, || true) {
36+
Err(ctrlc::Error::MultipleHandlers) => {}
37+
ret => panic!("{:?}", ret),
38+
}
39+
})
40+
}
41+
42+
fn tests() {
43+
run_tests!(test_set_scoped_handler);
44+
}
45+
46+
fn main() {
47+
run_harness(tests);
48+
}

0 commit comments

Comments
 (0)