From f07bec5023d1f90e8352381346c2093a1e183967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?= <1588988+jruales@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:31:29 -0700 Subject: [PATCH] fix: don't let a listener exception wedge EventEmitter `EventEmitter.fire` left `_deliveryQueue` populated when a listener threw, so every later `fire` took the re-entrancy path and returned without dispatching. One exception permanently stopped all delivery on that emitter, which silently killed the CDP message pump for the connection and left the debuggee waiting for `Runtime.runIfWaitingForDebugger`. Restore the queue in a `finally` block, and treat messages for unknown sessions the same way messages for disposed sessions are already treated (warn and ignore) rather than throwing. Electron emits `Inspector.workerScriptLoaded` for sessions we don't track, which made this reachable in normal operation. Fixes #2401 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cdp/connection.ts | 7 +++++-- src/common/events.test.ts | 27 +++++++++++++++++++++++++++ src/common/events.ts | 11 +++++++---- 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/common/events.test.ts diff --git a/src/cdp/connection.ts b/src/cdp/connection.ts index ade3b967d..ebe69b5c1 100644 --- a/src/cdp/connection.ts +++ b/src/cdp/connection.ts @@ -105,9 +105,12 @@ export default class Connection { if (!session) { const disposedDate = this._disposedSessions.get(object.sessionId); if (!disposedDate) { - throw new Error( - `Unknown session id: ${object.sessionId} while processing: ${object.method}`, + this.logger.warn( + LogTag.Internal, + 'Got message for an unknown session', + { sessionId: object.sessionId, method: object.method }, ); + return; // We just ignore messages for unknown sessions } else { const secondsAgo = (Date.now() - disposedDate.getTime()) / 1000.0; this.logger.warn( diff --git a/src/common/events.test.ts b/src/common/events.test.ts new file mode 100644 index 000000000..693d0de03 --- /dev/null +++ b/src/common/events.test.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { expect } from 'chai'; +import { EventEmitter } from './events'; + +describe('EventEmitter', () => { + it('keeps delivering events after a listener throws', () => { + const emitter = new EventEmitter(); + const delivered: string[] = []; + + emitter.event(event => { + if (event === 'throws') { + throw new Error('listener error'); + } + + delivered.push(event); + }); + + emitter.fire('before'); + expect(() => emitter.fire('throws')).to.throw('listener error'); + emitter.fire('after'); + + expect(delivered).to.deep.equal(['before', 'after']); + }); +}); diff --git a/src/common/events.ts b/src/common/events.ts index aa7d9f7fd..cdcaa47fa 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -57,11 +57,14 @@ export class EventEmitter implements IDisposable { if (!this._deliveryQueue) this._deliveryQueue = []; for (const data of this._listeners) this._deliveryQueue.push({ data, event }); if (!dispatch) return; - for (let index = 0; index < this._deliveryQueue.length; index++) { - const { data, event } = this._deliveryQueue[index]; - data.listener.call(data.thisArg, event); + try { + for (let index = 0; index < this._deliveryQueue.length; index++) { + const { data, event } = this._deliveryQueue[index]; + data.listener.call(data.thisArg, event); + } + } finally { + this._deliveryQueue = undefined; } - this._deliveryQueue = undefined; } dispose() {