From 5ff338f4d8f9d26bc5bd4480ceeb2b3e482f08e5 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 28 Oct 2025 18:17:25 +0100 Subject: [PATCH 1/3] debugger: fix event listener leak in the run command It should remove both the error and the ready event listeners attached when either of them fires, instead of removing only the one whose corresponding event fires, otherwise the other event listener will always get leaked. --- lib/internal/debugger/inspect_client.js | 16 ++++++++++++++-- test/common/debugger.js | 4 ++++ test/parallel/test-debugger-restart-message.js | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/internal/debugger/inspect_client.js b/lib/internal/debugger/inspect_client.js index 315617bf08a800..22a3481993ca1e 100644 --- a/lib/internal/debugger/inspect_client.js +++ b/lib/internal/debugger/inspect_client.js @@ -344,8 +344,20 @@ class Client extends EventEmitter { }; return new Promise((resolve, reject) => { - this.once('error', reject); - this.once('ready', resolve); + const tearDown = () => { + this.removeListener('ready', onReady); + this.removeListener('error', onError); + }; + const onError = (err) => { + tearDown(); + reject(err); + }; + const onReady = () => { + tearDown(); + resolve(); + }; + this.once('error', onError); + this.once('ready', onReady); httpReq.on('upgrade', handshakeListener); httpReq.end(); diff --git a/test/common/debugger.js b/test/common/debugger.js index f5a47cbe06ea71..41d04fd67df64a 100644 --- a/test/common/debugger.js +++ b/test/common/debugger.js @@ -141,6 +141,10 @@ function startCLI(args, flags = [], spawnOpts = {}, opts = { randomPort: true }) return getOutput(); }, + get stderrOutput() { + return stderrOutput; + }, + get rawOutput() { return outputBuffer.join('').toString(); }, diff --git a/test/parallel/test-debugger-restart-message.js b/test/parallel/test-debugger-restart-message.js index 190d0c18ccc081..292a15bd9f4d7a 100644 --- a/test/parallel/test-debugger-restart-message.js +++ b/test/parallel/test-debugger-restart-message.js @@ -31,6 +31,8 @@ const startCLI = require('../common/debugger'); } finally { await cli.quit(); } + + assert.doesNotMatch(cli.stderrOutput, /MaxListenersExceededWarning/); } onWaitForInitialBreak(); From cda1c0ee4e2972d2f18821d8401d9d00830011d8 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 29 Oct 2025 01:33:09 +0100 Subject: [PATCH 2/3] fixup! debugger: fix event listener leak in the run command Co-authored-by: Luigi Pinca --- lib/internal/debugger/inspect_client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/debugger/inspect_client.js b/lib/internal/debugger/inspect_client.js index 22a3481993ca1e..59ae54fda40144 100644 --- a/lib/internal/debugger/inspect_client.js +++ b/lib/internal/debugger/inspect_client.js @@ -356,8 +356,8 @@ class Client extends EventEmitter { tearDown(); resolve(); }; - this.once('error', onError); - this.once('ready', onReady); + this.on('error', onError); + this.on('ready', onReady); httpReq.on('upgrade', handshakeListener); httpReq.end(); From ccabb5270a815f14c4f982985e82c458c89e1b7a Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 1 Nov 2025 09:41:42 +0100 Subject: [PATCH 3/3] fixup! fixup! debugger: fix event listener leak in the run command --- lib/internal/debugger/inspect_client.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/internal/debugger/inspect_client.js b/lib/internal/debugger/inspect_client.js index 59ae54fda40144..264e2743383972 100644 --- a/lib/internal/debugger/inspect_client.js +++ b/lib/internal/debugger/inspect_client.js @@ -13,7 +13,7 @@ const { const Buffer = require('buffer').Buffer; const crypto = require('crypto'); const { ERR_DEBUGGER_ERROR } = require('internal/errors').codes; -const { EventEmitter } = require('events'); +const { EventEmitter, once } = require('events'); const http = require('http'); const { URL } = require('internal/url'); @@ -343,25 +343,10 @@ class Client extends EventEmitter { this.emit('ready'); }; - return new Promise((resolve, reject) => { - const tearDown = () => { - this.removeListener('ready', onReady); - this.removeListener('error', onError); - }; - const onError = (err) => { - tearDown(); - reject(err); - }; - const onReady = () => { - tearDown(); - resolve(); - }; - this.on('error', onError); - this.on('ready', onReady); - - httpReq.on('upgrade', handshakeListener); - httpReq.end(); - }); + const onReady = once(this, 'ready'); + httpReq.on('upgrade', handshakeListener); + httpReq.end(); + return onReady; } }