Skip to content

fix(ext/node): pass signal name to process signal handlers#33425

Merged
bartlomieju merged 4 commits into
mainfrom
fix/process-signal-name
Apr 25, 2026
Merged

fix(ext/node): pass signal name to process signal handlers#33425
bartlomieju merged 4 commits into
mainfrom
fix/process-signal-name

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Summary

Closes #33379

Node.js calls signal listeners with the signal name as the first argument (e.g. "SIGINT"), but Deno.addSignalListener calls them with no arguments. This caused process.on('SIGINT', (signal) => ...) to receive undefined instead of the signal name.

Fix: wrap signal listeners registered via process.on() / process.prependListener() to pass the signal name. A WeakMap tracks the mapping so process.off() / process.removeListener() can remove the correct wrapped listener from Deno's signal system.

Test plan

Before:

$ deno run -A test.mjs
handler got: undefined

After:

$ deno run -A test.mjs
handler got: SIGINT

Node.js calls signal listeners with the signal name as the first
argument (e.g. "SIGINT"), but Deno.addSignalListener calls them with
no arguments. Wrap signal listeners registered via process.on() to
pass the signal name, matching Node.js behavior.

Closes #33379
test-signal-args.js relies on setImmediate keeping the event loop alive
for signal delivery, which does not work in Deno (signals need a real
async tick like setTimeout). Replace with a unit test using setTimeout.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The wrap/unwrap structure threads through both flows cleanly:

  • Direct process.on("SIGINT", handler): _wrapSignalListener lazily allocates W = () => handler("SIGINT") keyed on (handler, event) in the outer WeakMap + inner Map, and registers W with Deno.addSignalListener. process.off("SIGINT", handler) finds nothing via _findSignalListener (since handler is its own list entry), then _unwrapSignalListener looks handler up, returns W, and deletes the inner-map entry — so the corresponding Deno.removeSignalListener(W) lines up.
  • process.once("SIGINT", handler): EventEmitter wraps as wrapped (with .listener = handler). The wrap path stores the entry under wrapped in the WeakMap. When wrapped fires and re-enters via process.removeListener("SIGINT", wrapFn), _findSignalListener returns wrapped (matched via .listener === handler), and _unwrapSignalListener("SIGINT", wrapped) recovers the same W that was registered, so the Deno-side listener is correctly cleaned up.
  • WeakMap-keyed-on-listener means once the user lets go of the handler, the wrappers are GCd automatically; wrappersByEvent.delete(event) prevents reuse but keeps the inner map so a re-on after off allocates a fresh wrapper rather than re-using a stale one.

Unit test exercises the SIGUSR1 → received: ["SIGUSR1"] happy path. CI fully green.

@bartlomieju
bartlomieju merged commit 38609bb into main Apr 25, 2026
220 of 222 checks passed
@bartlomieju
bartlomieju deleted the fix/process-signal-name branch April 25, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

process.on('SIGINT') handler receives undefined instead of signal name

2 participants