Skip to content

fix(ext/node): signal listeners added via process.once can now be removed#32606

Merged
bartlomieju merged 3 commits intomainfrom
fix/process-once-remove-signal-listener
Mar 17, 2026
Merged

fix(ext/node): signal listeners added via process.once can now be removed#32606
bartlomieju merged 3 commits intomainfrom
fix/process-once-remove-signal-listener

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

Summary

  • Fix process.once("SIGINT", handler) followed by process.removeListener("SIGINT", handler) not actually removing the Deno-level signal listener
  • When EventEmitter.once() wraps a handler, Process.prototype.off now looks up the wrapper before passing it to Deno.removeSignalListener
  • Add spec test covering once, off, on, and prependOnceListener removal paths

Closes #32603

Test plan

🤖 Generated with Claude Code

…emoved

When `process.once()` was used to add a signal listener, EventEmitter
wrapped the handler in a once-wrapper before passing it to
`Process.prototype.on`, which registered the wrapper with
`Deno.addSignalListener`. However, `Process.prototype.off` passed the
original unwrapped handler to `Deno.removeSignalListener`, which didn't
match, so the Deno-level signal listener was never removed.

Fix by looking up the actual registered listener (which may be a
once-wrapper) before calling `Deno.removeSignalListener`.

Closes #32603

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +495 to +502
function _findSignalListener(
// deno-lint-ignore no-explicit-any
target: any,
event: string,
// deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
// deno-lint-ignore no-explicit-any
): ((...args: any[]) => void) | undefined {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The TS here makes me sad :(

Comment on lines +492 to +494
// Look up the actual registered listener for a signal event. When `once()` is
// used, EventEmitter wraps the listener in a function with a `.listener`
// property. We need the wrapper to pass to `Deno.removeSignalListener`.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

FYI all these gymnastics is because we handle signals using runtime API, Node does that by handling signals in Process object in native code. Something to address in a follow up cleanup.

Override `removeAllListeners` on Process prototype to call
`Deno.removeSignalListener` for each registered signal handler before
delegating to EventEmitter, preventing OS-level signal handler leaks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@kajukitli kajukitli left a comment

Choose a reason for hiding this comment

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

lgtm, good fix

the core issue is that EventEmitter.once() wraps the listener in a function that has .listener pointing to the original. when removing, you were passing the original to Deno.removeSignalListener but Deno has the wrapper registered.

the _findSignalListener lookup before removal is the right fix. also good that you handle removeAllListeners() for signal cleanup — that was an easy miss.

one edge case to consider: if someone calls process.off("SIGINT", someHandler) where someHandler was never registered, _findSignalListener returns undefined, and you call Deno.removeSignalListener with the original handler. this might throw or no-op depending on Deno's behavior. probably fine since that's user error, just noting it.

@bartlomieju
Copy link
Copy Markdown
Member Author

Re: the edge case with process.off("SIGINT", someHandler) where someHandler was never registered — verified that Deno.removeSignalListener silently no-ops in that case, which matches Node's behavior (Node also no-ops on process.off("SIGINT", unregisteredHandler)). So no issue there.

@bartlomieju bartlomieju enabled auto-merge (squash) March 10, 2026 16:30
@bartlomieju bartlomieju merged commit 3998494 into main Mar 17, 2026
112 checks passed
@bartlomieju bartlomieju deleted the fix/process-once-remove-signal-listener branch March 17, 2026 13:17
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.

Signal listeners added by process.once cannot be removed by process.removeListener

3 participants