Skip to content

Commit 54fd0e3

Browse files
committed
fix(runtime): Restore default signal handler after user handlers are unregistered (#22757)
<!-- Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing 1. Give the PR a descriptive title. Examples of good title: - fix(std/http): Fix race condition in server - docs(console): Update docstrings - feat(doc): Handle nested reexports Examples of bad title: - fix #7123 - update docs - fix bugs 2. Ensure there is a related issue and it is referenced in the PR text. 3. Ensure there are tests that cover the changes. 4. Ensure `cargo test` passes. 5. Ensure `./tools/format.js` passes without changing files. 6. Ensure `./tools/lint.js` passes. 7. Open as a draft PR if your work is still in progress. The CI won't run all steps, but you can add '[ci]' to a commit message to force it to. 8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label. --> Fixes #22724. Fixes #7164. This does add a dependency on `signal-hook`, but it's just a higher level API on top of `signal-hook-registry` (which we and `tokio` already depend on) and doesn't add any transitive deps.
1 parent 88fab18 commit 54fd0e3

5 files changed

Lines changed: 102 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ regex.workspace = true
119119
ring.workspace = true
120120
rustyline = { workspace = true, features = ["custom-bindings"] }
121121
serde.workspace = true
122+
signal-hook = "0.3.17"
122123
signal-hook-registry = "1.4.0"
123124
tokio.workspace = true
124125
tokio-metrics.workspace = true

runtime/ops/signal.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ use deno_core::ResourceId;
1212

1313
use std::borrow::Cow;
1414
use std::cell::RefCell;
15+
#[cfg(unix)]
16+
use std::collections::BTreeMap;
1517
use std::rc::Rc;
18+
#[cfg(unix)]
19+
use std::sync::atomic::AtomicBool;
20+
#[cfg(unix)]
21+
use std::sync::Arc;
1622

1723
#[cfg(unix)]
1824
use tokio::signal::unix::signal;
@@ -32,13 +38,52 @@ use tokio::signal::windows::CtrlC;
3238
deno_core::extension!(
3339
deno_signal,
3440
ops = [op_signal_bind, op_signal_unbind, op_signal_poll],
41+
state = |state| {
42+
#[cfg(unix)]
43+
{
44+
state.put(SignalState::default());
45+
}
46+
}
3547
);
3648

49+
#[cfg(unix)]
50+
#[derive(Default)]
51+
struct SignalState {
52+
enable_default_handlers: BTreeMap<libc::c_int, Arc<AtomicBool>>,
53+
}
54+
55+
#[cfg(unix)]
56+
impl SignalState {
57+
/// Disable the default signal handler for the given signal.
58+
///
59+
/// Returns the shared flag to enable the default handler later, and whether a default handler already existed.
60+
fn disable_default_handler(
61+
&mut self,
62+
signo: libc::c_int,
63+
) -> (Arc<AtomicBool>, bool) {
64+
use std::collections::btree_map::Entry;
65+
66+
match self.enable_default_handlers.entry(signo) {
67+
Entry::Occupied(entry) => {
68+
let enable = entry.get();
69+
enable.store(false, std::sync::atomic::Ordering::Release);
70+
(enable.clone(), true)
71+
}
72+
Entry::Vacant(entry) => {
73+
let enable = Arc::new(AtomicBool::new(false));
74+
entry.insert(enable.clone());
75+
(enable, false)
76+
}
77+
}
78+
}
79+
}
80+
3781
#[cfg(unix)]
3882
/// The resource for signal stream.
3983
/// The second element is the waker of polling future.
4084
struct SignalStreamResource {
4185
signal: AsyncRefCell<Signal>,
86+
enable_default_handler: Arc<AtomicBool>,
4287
cancel: CancelHandle,
4388
}
4489

@@ -548,11 +593,29 @@ fn op_signal_bind(
548593
"Binding to signal '{sig}' is not allowed",
549594
)));
550595
}
596+
597+
let signal = AsyncRefCell::new(signal(SignalKind::from_raw(signo))?);
598+
599+
let (enable_default_handler, has_default_handler) = state
600+
.borrow_mut::<SignalState>()
601+
.disable_default_handler(signo);
602+
551603
let resource = SignalStreamResource {
552-
signal: AsyncRefCell::new(signal(SignalKind::from_raw(signo))?),
604+
signal,
553605
cancel: Default::default(),
606+
enable_default_handler: enable_default_handler.clone(),
554607
};
555608
let rid = state.resource_table.add(resource);
609+
610+
if !has_default_handler {
611+
// restore default signal handler when the signal is unbound
612+
// this can error if the signal is not supported, if so let's just leave it as is
613+
let _ = signal_hook::flag::register_conditional_default(
614+
signo,
615+
enable_default_handler,
616+
);
617+
}
618+
556619
Ok(rid)
557620
}
558621

@@ -606,6 +669,15 @@ pub fn op_signal_unbind(
606669
state: &mut OpState,
607670
#[smi] rid: ResourceId,
608671
) -> Result<(), AnyError> {
609-
state.resource_table.take_any(rid)?.close();
672+
let resource = state.resource_table.take::<SignalStreamResource>(rid)?;
673+
674+
#[cfg(unix)]
675+
{
676+
resource
677+
.enable_default_handler
678+
.store(true, std::sync::atomic::Ordering::Release);
679+
}
680+
681+
resource.close();
610682
Ok(())
611683
}

tests/unit/signal_test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,20 @@ Deno.test(
172172
}
173173

174174
// Sends SIGUSR1 (irrelevant signal) 3 times.
175+
// By default SIGUSR1 terminates, so set it to a no-op for this test.
176+
let count = 0;
177+
const irrelevant = () => {
178+
count++;
179+
};
180+
Deno.addSignalListener("SIGUSR1", irrelevant);
175181
for (const _ of Array(3)) {
176182
await delay(20);
177183
Deno.kill(Deno.pid, "SIGUSR1");
178184
}
185+
while (count < 3) {
186+
await delay(20);
187+
}
188+
Deno.removeSignalListener("SIGUSR1", irrelevant);
179189

180190
// No change
181191
assertEquals(c, "010101000");

tests/unit_node/process_test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ Deno.test({
237237

238238
Deno.test({
239239
name: "process.off signal",
240-
ignore: true, // This test fails to terminate
240+
ignore: Deno.build.os == "windows",
241241
async fn() {
242242
const testTimeout = setTimeout(() => fail("Test timed out"), 10_000);
243243
try {
@@ -246,13 +246,13 @@ Deno.test({
246246
"eval",
247247
`
248248
import process from "node:process";
249-
console.log("ready");
250249
setInterval(() => {}, 1000);
251250
const listener = () => {
251+
process.off("SIGINT", listener);
252252
console.log("foo");
253-
process.off("SIGINT", listener)
254253
};
255254
process.on("SIGINT", listener);
255+
console.log("ready");
256256
`,
257257
],
258258
stdout: "piped",
@@ -275,6 +275,7 @@ Deno.test({
275275
while (!output.includes("foo\n")) {
276276
await delay(10);
277277
}
278+
process.kill("SIGINT");
278279
await process.status;
279280
} finally {
280281
clearTimeout(testTimeout);

0 commit comments

Comments
 (0)