fix: don't throw resolving browser view dynamic events after view destroyed (fixes #333783) - #333787
Conversation
…troyed (fixes microsoft#333783) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📬 CODENOTIFYThe following users are being notified based on files changed in this PR: Joaquín Ruales (@jruales)Matched files:
|
There was a problem hiding this comment.
Pull request overview
Prevents benign IPC subscription races from throwing after an integrated browser view is destroyed.
Changes:
- Adds a safe dynamic-event resolver returning
Event.None. - Applies it to all per-view dynamic events.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…r JSDoc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Driver cycle recordederrors-fix-driver:cycle head:a478e221a08547d22c7477a9abb5271e419f11dc |
|
The |
There was a problem hiding this comment.
🟡 Changes recommended
A missed close subscription can leave a stale browser editor and model alive.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
… when view gone Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Driver cycle recordederrors-fix-driver:cycle head:19292e21e7033f07ec4671ae9192df762f45f9f2 |
…e comment to one line Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Driver cycle recordederrors-fix-driver:cycle head:c0db4de61875a7fd1abe8d735ca676ed9575a405 |
Kyle Cutler (kycutler)
left a comment
There was a problem hiding this comment.
Copilot adding these checks just masks the underlying issue: the workbench is trying to subscribe to the browser after it has been disposed. Investigate the actual root cause more, including identifying the path that is triggering the error and the associated disposal race. Treat the race within the workbench side and do not add these blanket checks as they will only mask further issues.
Summary
The main-process
BrowserViewMainServicethrowsBrowser view <id> not foundwhen a renderer subscribes to a per-view "dynamic" event (onDynamicDidChangePermissionsin the reported stack) over IPC after that view has already been destroyed. Event subscription is resolved asynchronously across the process boundary, so the view can disappear in the window between the renderer subscribing and the main process handling thelistenrequest. The throw crosses the IPC boundary as an unhandled error and is reported to telemetry (~177 users/bucket across 5 sibling buckets that differ only by the random view GUID). This is a post-fix recurrence of the same class fixed for #318995 in 1.124.0, now surfacing through the newer permissions event path.Fixes #333783
Recommended reviewer:
@kycutlerCulprit Commit
67aea9c1@kycutleronDynamicDidChangePermissions(id)(and the renderer subscribes to it inBrowserViewModel's constructor), following the existingonDynamic*pattern that calls_getBrowserView(id)— which throws when the view is gone. The permissions event is the specific accessor in the reported stack; all siblingonDynamic*accessors share the same race.Code Flow
sequenceDiagram participant Renderer as BrowserViewModel (renderer) participant IPC as IPC channel participant Main as BrowserViewMainService participant Map as browserViews (DisposableMap) Renderer->>IPC: listen onDynamicDidChangePermissions(id) Note over Map: ⚠️ Root cause:<br/>view destroyed before<br/>listen request handled IPC->>Main: onEventListen -> onDynamicDidChangePermissions(id) Main->>Map: _getBrowserView(id) -> get(id) === undefined Note over Main: 💥 throw new Error(<br/>`Browser view ${id} not found`) Main-->>IPC: error crosses process boundaryAffected Files
src/vs/platform/browserView/electron-main/browserViewMainService.tsthrow new Error(\Browser view ${id} not found`)`src/vs/platform/browserView/electron-main/browserViewMainService.tsonDynamicDidChangePermissions(id)returnsthis._getBrowserView(id).onDidChangePermissions, throwing when the view is already gonesrc/vs/workbench/contrib/browserView/common/browserView.tsthis._register(this.browserViewService.onDynamicDidChangePermissions(this.id)(snapshot => ...))subscribes over IPCRepro Steps
Non-deterministic timing race across the renderer↔main IPC boundary:
BrowserViewModelis created and subscribes toonDynamicDidChangePermissions(id)(and the otheronDynamic*events) over IPC.browserViews.deleteAndDispose(id)removes it from the map in the main process.listenrequest for anyonDynamic*event is handled by the main process after the view is removed,_getBrowserView(id)throws and the error is reported to telemetry.How the Fix Works
Chosen approach (
browserViewMainService.ts): Added a small producer-side helper_getBrowserViewEvent<T>(id, getEvent)that resolves the requested event only when the view still exists and returnsEvent.Noneotherwise. All 20onDynamic*event accessors now route through it instead ofthis._getBrowserView(id).<event>. This fixes the problem at the producer of the invalid state (the main-process event accessor), not at an unrelated crash site or by silencing telemetry.A missing view at event-subscription time is an expected outcome of asynchronous IPC subscription, not a real error: the view is gone, so there are no further events to deliver and an empty event stream is the semantically correct result. This is the external/untrusted-boundary case in the lifecycle-race guidance — the consumer lives in a separate process and cannot synchronously coordinate its subscription with main-process disposal, so the producer must tolerate the race for these event getters. Non-event methods (
getState,layout,loadURL, etc.) intentionally keep throwing via_getBrowserView, because calling those against a destroyed view is a genuine caller error rather than a benign subscription race.Alternatives considered:
onEventListenin try/catch — hides the error from telemetry for all channels and swallows genuine bugs, rather than fixing the specific event getters that are legitimately racy.BrowserViewModel) — the renderer cannot know the view was destroyed in the main process at subscription time, so it cannot avoid the race; the fix belongs at the producer._getBrowserViewitself returnundefined/Event.Nonefor everything — would weaken the invariant for the many state-mutating methods where a missing view really is a bug.Lifecycle pattern: external/untrusted boundary (cross-process IPC event subscription).
Producer site:
src/vs/platform/browserView/electron-main/browserViewMainService.ts→onDynamicDidChangePermissions()and siblingonDynamic*accessors.Consumer-side fix justification: the consumer (
BrowserViewModel) runs in the renderer process and subscribes over IPC; it has no synchronous view of main-process disposal, so it cannot prevent the race. The fix is applied at the producer (main-process event accessors), which is the correct side and keeps the throwing contract intact for genuine misuse of state-mutating methods.Recommended Owner
@kycutler— author of the culprit commit (#322639, "Support browser permissions") and of the surroundingonDynamic*browser-view accessors; actively committing tomicrosoft/vscodewithin the last 90 days.