Skip to content

Commit f584d76

Browse files
committed
Merge branch 'handlers-mt'
2 parents d365eb3 + a201f0d commit f584d76

File tree

9 files changed

+133
-36
lines changed

9 files changed

+133
-36
lines changed

Diff for: Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: gix/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ regex = { version = "1.6.0", optional = true, default-features = false, features
191191
# For internal use to allow pure-Rust builds without openssl.
192192
reqwest-for-configuration-only = { package = "reqwest", version = "0.11.13", default-features = false, optional = true }
193193

194+
# for `interrupt` module
195+
parking_lot = "0.12.1"
196+
194197
document-features = { version = "0.2.0", optional = true }
195198

196199
[target.'cfg(target_vendor = "apple")'.dependencies]

Diff for: gix/examples/clone.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn main() -> anyhow::Result<()> {
1111
.nth(2)
1212
.context("The second argument is the directory to clone the repository into")?;
1313

14-
gix::interrupt::init_handler(|| {})?;
14+
gix::interrupt::init_handler(1, || {})?;
1515
std::fs::create_dir_all(&dst)?;
1616
let url = gix::url::parse(repo_url.to_str().unwrap().into())?;
1717

Diff for: gix/examples/interrupt-handler-allows-graceful-shutdown.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
use gix_tempfile::{AutoRemove, ContainingDirectory};
44

55
fn main() -> anyhow::Result<()> {
6-
gix::interrupt::init_handler(|| {})?;
6+
gix::interrupt::init_handler(1, || {})?;
77
eprintln!("About to emit the first term signal");
88
let tempfile_path = Path::new("example-file.tmp");
99
let _keep_tempfile = gix_tempfile::mark_at(tempfile_path, ContainingDirectory::Exists, AutoRemove::Tempfile)?;

Diff for: gix/examples/reversible-interrupt-handlers.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
fn main() -> anyhow::Result<()> {
22
{
3-
let _deregister_on_drop = gix::interrupt::init_handler(|| {})?.auto_deregister();
3+
let _deregister_on_drop = gix::interrupt::init_handler(1, || {})?.auto_deregister();
44
}
55
eprintln!("About to emit the first term signal, which acts just like a normal one");
66
signal_hook::low_level::raise(signal_hook::consts::SIGTERM)?;

Diff for: gix/src/interrupt.rs

+66-31
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,62 @@
88
mod init {
99
use std::{
1010
io,
11-
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
11+
sync::atomic::{AtomicUsize, Ordering},
1212
};
1313

14-
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
14+
static DEREGISTER_COUNT: AtomicUsize = AtomicUsize::new(0);
15+
static REGISTERED_HOOKS: once_cell::sync::Lazy<parking_lot::Mutex<Vec<(i32, signal_hook::SigId)>>> =
16+
once_cell::sync::Lazy::new(Default::default);
17+
static DEFAULT_BEHAVIOUR_HOOKS: once_cell::sync::Lazy<parking_lot::Mutex<Vec<signal_hook::SigId>>> =
18+
once_cell::sync::Lazy::new(Default::default);
1519

20+
/// A type to help deregistering hooks registered with [`init_handler`](super::init_handler());
1621
#[derive(Default)]
17-
pub struct Deregister(Vec<(i32, signal_hook::SigId)>);
22+
pub struct Deregister {
23+
do_reset: bool,
24+
}
1825
pub struct AutoDeregister(Deregister);
1926

2027
impl Deregister {
21-
/// Remove all previously registered handlers, and assure the default behaviour is reinstated.
28+
/// Remove all previously registered handlers, and assure the default behaviour is reinstated, if this is the last available instance.
2229
///
2330
/// Note that only the instantiation of the default behaviour can fail.
2431
pub fn deregister(self) -> std::io::Result<()> {
25-
if self.0.is_empty() {
32+
let mut hooks = REGISTERED_HOOKS.lock();
33+
let count = DEREGISTER_COUNT.fetch_sub(1, Ordering::SeqCst);
34+
if count > 1 || hooks.is_empty() {
2635
return Ok(());
2736
}
28-
static REINSTATE_DEFAULT_BEHAVIOUR: AtomicBool = AtomicBool::new(true);
29-
for (_, hook_id) in &self.0 {
37+
if self.do_reset {
38+
super::reset();
39+
}
40+
for (_, hook_id) in hooks.iter() {
3041
signal_hook::low_level::unregister(*hook_id);
3142
}
32-
IS_INITIALIZED.store(false, Ordering::SeqCst);
33-
if REINSTATE_DEFAULT_BEHAVIOUR
34-
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |_| Some(false))
35-
.expect("always returns value")
36-
{
37-
for (sig, _) in self.0 {
38-
// # SAFETY
39-
// * we only call a handler that is specifically designed to run in this environment.
40-
#[allow(unsafe_code)]
41-
unsafe {
42-
signal_hook::low_level::register(sig, move || {
43-
signal_hook::low_level::emulate_default_handler(sig).ok();
44-
})?;
45-
}
43+
44+
let hooks = hooks.drain(..);
45+
let mut default_hooks = DEFAULT_BEHAVIOUR_HOOKS.lock();
46+
// Even if dropped, `drain(..)` clears the vec which is a must.
47+
for (sig, _) in hooks {
48+
// # SAFETY
49+
// * we only register a handler that is specifically designed to run in this environment.
50+
#[allow(unsafe_code)]
51+
unsafe {
52+
default_hooks.push(signal_hook::low_level::register(sig, move || {
53+
signal_hook::low_level::emulate_default_handler(sig).ok();
54+
})?);
4655
}
4756
}
4857
Ok(())
4958
}
5059

60+
/// If called with `toggle` being `true`, when actually deregistering, we will also reset the trigger by
61+
/// calling [`reset()`](super::reset()).
62+
pub fn with_reset(mut self, toggle: bool) -> Self {
63+
self.do_reset = toggle;
64+
self
65+
}
66+
5167
/// Return a type that deregisters all installed signal handlers on drop.
5268
pub fn auto_deregister(self) -> AutoDeregister {
5369
AutoDeregister(self)
@@ -60,20 +76,33 @@ mod init {
6076
}
6177
}
6278

63-
/// Initialize a signal handler to listen to SIGINT and SIGTERM and trigger our [`trigger()`][super::trigger()] that way.
64-
/// Also trigger `interrupt()` which promises to never use a Mutex, allocate or deallocate.
79+
/// Initialize a signal handler to listen to SIGINT and SIGTERM and trigger our [`trigger()`](super::trigger()) that way.
80+
/// Also trigger `interrupt()` which promises to never use a Mutex, allocate or deallocate, or do anything else that's blocking.
81+
/// Use `grace_count` to determine how often the termination signal can be received before it's terminal, e.g. 1 would only terminate
82+
/// the application the second time the signal is received.
83+
/// Note that only the `grace_count` and `interrupt` of the first call are effective, all others will be ignored.
84+
///
85+
/// Use the returned `Deregister` type to explicitly deregister hooks, or to do so automatically.
6586
///
6687
/// # Note
6788
///
6889
/// It will abort the process on second press and won't inform the user about this behaviour either as we are unable to do so without
6990
/// deadlocking even when trying to write to stderr directly.
70-
pub fn init_handler(interrupt: impl Fn() + Send + Sync + Clone + 'static) -> io::Result<Deregister> {
71-
if IS_INITIALIZED
72-
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |_| Some(true))
73-
.expect("always returns value")
74-
{
75-
return Err(io::Error::new(io::ErrorKind::Other, "Already initialized"));
91+
pub fn init_handler(
92+
grace_count: usize,
93+
interrupt: impl Fn() + Send + Sync + Clone + 'static,
94+
) -> io::Result<Deregister> {
95+
let prev_count = DEREGISTER_COUNT.fetch_add(1, Ordering::SeqCst);
96+
if prev_count != 0 {
97+
// Try to obtain the lock before we return just to wait for the signals to actually be registered.
98+
let _guard = REGISTERED_HOOKS.lock();
99+
return Ok(Deregister::default());
100+
}
101+
let mut guard = REGISTERED_HOOKS.lock();
102+
if !guard.is_empty() {
103+
return Ok(Deregister::default());
76104
}
105+
77106
let mut hooks = Vec::with_capacity(signal_hook::consts::TERM_SIGNALS.len());
78107
for sig in signal_hook::consts::TERM_SIGNALS {
79108
// # SAFETY
@@ -88,7 +117,7 @@ mod init {
88117
INTERRUPT_COUNT.store(0, Ordering::SeqCst);
89118
}
90119
let msg_idx = INTERRUPT_COUNT.fetch_add(1, Ordering::SeqCst);
91-
if msg_idx == 1 {
120+
if msg_idx == grace_count {
92121
gix_tempfile::registry::cleanup_tempfiles_signal_safe();
93122
signal_hook::low_level::emulate_default_handler(*sig).ok();
94123
}
@@ -98,13 +127,19 @@ mod init {
98127
hooks.push((*sig, hook_id));
99128
}
100129
}
130+
for hook_id in DEFAULT_BEHAVIOUR_HOOKS.lock().drain(..) {
131+
signal_hook::low_level::unregister(hook_id);
132+
}
101133

102134
// This means that they won't setup a handler allowing us to call them right before we actually abort.
103135
gix_tempfile::signal::setup(gix_tempfile::signal::handler::Mode::None);
104136

105-
Ok(Deregister(hooks))
137+
*guard = hooks;
138+
Ok(Deregister::default())
106139
}
107140
}
141+
pub use init::Deregister;
142+
108143
use std::{
109144
io,
110145
sync::atomic::{AtomicBool, Ordering},

Diff for: gix/tests/interrupt.rs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use signal_hook::consts::SIGTERM;
2+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3+
4+
#[test]
5+
fn multi_registration() -> gix_testtools::Result {
6+
static V1: AtomicUsize = AtomicUsize::new(0);
7+
static V2: AtomicBool = AtomicBool::new(false);
8+
9+
let reg1 = gix::interrupt::init_handler(3, || {
10+
V1.fetch_add(1, Ordering::SeqCst);
11+
})
12+
.expect("succeeds");
13+
assert!(!gix::interrupt::is_triggered());
14+
assert_eq!(V1.load(Ordering::Relaxed), 0);
15+
let reg2 =
16+
gix::interrupt::init_handler(2, || V2.store(true, Ordering::SeqCst)).expect("multi-initialization is OK");
17+
assert!(!V2.load(Ordering::Relaxed));
18+
19+
signal_hook::low_level::raise(SIGTERM).expect("signal can be raised");
20+
assert!(gix::interrupt::is_triggered(), "this happens automatically");
21+
assert_eq!(V1.load(Ordering::Relaxed), 1, "the first trigger is invoked");
22+
assert!(!V2.load(Ordering::Relaxed), "the second trigger was ignored");
23+
24+
reg1.deregister()?;
25+
signal_hook::low_level::raise(SIGTERM).expect("signal can be raised");
26+
assert_eq!(V1.load(Ordering::Relaxed), 2, "the first trigger is still invoked");
27+
28+
assert!(gix::interrupt::is_triggered(), "this happens automatically");
29+
// now the registration is actually removed.
30+
reg2.with_reset(true).deregister()?;
31+
assert!(
32+
!gix::interrupt::is_triggered(),
33+
"the deregistration succeeded and this is an optional side-effect"
34+
);
35+
36+
let reg1 = gix::interrupt::init_handler(3, || {
37+
V1.fetch_add(1, Ordering::SeqCst);
38+
})
39+
.expect("succeeds");
40+
assert_eq!(V1.load(Ordering::Relaxed), 2, "nothing changed yet");
41+
let reg2 =
42+
gix::interrupt::init_handler(2, || V2.store(true, Ordering::SeqCst)).expect("multi-initialization is OK");
43+
assert!(!V2.load(Ordering::Relaxed));
44+
45+
signal_hook::low_level::raise(SIGTERM).expect("signal can be raised");
46+
assert_eq!(V1.load(Ordering::Relaxed), 3, "the first trigger is invoked");
47+
assert!(!V2.load(Ordering::Relaxed), "the second trigger was ignored");
48+
49+
reg2.auto_deregister();
50+
reg1.with_reset(true).auto_deregister();
51+
52+
assert!(
53+
!gix::interrupt::is_triggered(),
54+
"the deregistration succeeded and this is an optional side-effect"
55+
);
56+
57+
Ok(())
58+
}

Diff for: src/plumbing/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub fn main() -> Result<()> {
129129
let auto_verbose = !progress && !args.no_verbose;
130130

131131
let should_interrupt = Arc::new(AtomicBool::new(false));
132-
gix::interrupt::init_handler({
132+
gix::interrupt::init_handler(1, {
133133
let should_interrupt = Arc::clone(&should_interrupt);
134134
move || should_interrupt.store(true, Ordering::SeqCst)
135135
})?;

Diff for: src/porcelain/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn main() -> Result<()> {
2020
time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound);
2121
}
2222
let should_interrupt = Arc::new(AtomicBool::new(false));
23-
gix::interrupt::init_handler({
23+
gix::interrupt::init_handler(1, {
2424
let should_interrupt = Arc::clone(&should_interrupt);
2525
move || should_interrupt.store(true, Ordering::SeqCst)
2626
})?;

0 commit comments

Comments
 (0)