{{ message }}
Repository navigation
fix: tolerate post-close debug adapter messages (fixes #340511) - #340519
Merged
Dmitriy Vasyura (dmitrivMS) merged 3 commits intoOct 12, 2026
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The post-close behavior needs regression coverage in the existing service test suite.
1 open finding
What changed in this PR
Prevents crashes from debug adapter messages arriving after session teardown.
Changes:
- Uses a non-throwing adapter lookup for delayed messages.
- Removes the obsolete throwing helper.
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
Dmitriy Vasyura (dmitrivMS)
approved these changes
Oct 12, 2026
Dmitriy Vasyura (dmitrivMS)
enabled auto-merge (squash)
October 12, 2026 21:06
roblourens
approved these changes
Oct 12, 2026
Paul (pwang347)
approved these changes
Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
$acceptDAMessageon the main-thread side of the debug extension-host bridge dereferences the debug adapter viagetDebugAdapter(handle), which throwsError('Invalid debug adapter')when the handle is no longer in_debugAdapters. Debug-adapter messages arrive over IPC from the extension host; when a session ends (or restarts), the main thread removes the adapter from the map in itsonDidEndSessionhandler, so any message already in flight across the process boundary lands after the entry is gone and crashes the unhandled-error pipeline. Impact: a reliably reproducible post-close race that surfaced to 1,542 users in the first 1.141.0 stable build across Windows, Mac, and Linux.Fixes #340511
Recommended reviewer:
@dmitrivMSCulprit Commit
0d321b80@dmitrivMS544a8291...73d5322b. It makesDirectDebugAdapter.stopSession()dispose the adapter (and its message listener) eagerly on stop, which tightens session teardown timing on the extension-host side and makes it more likely that a DA message is delivered to the main thread after the main thread has already evicted the adapter inonDidEndSession, reaching the throwinggetDebugAdapter()path. The pre-existing throw site had always been reachable in principle, but this change shifted teardown ordering enough to turn it into a stable-release anomaly.Code Flow
sequenceDiagram participant EH as ExtHost DirectDebugAdapter participant IPC as rpcProtocol (IPC) participant MT as MainThreadDebugService participant Map as _debugAdapters map Note over MT,Map: onDidEndSession removes adapter:<br/>_debugAdapters.delete(handle) EH->>IPC: $acceptDAMessage(handle, message) Note over EH: ⚠️ message already in flight<br/>when session ended IPC->>MT: $acceptDAMessage(handle, message) MT->>Map: getDebugAdapter(handle) Note over MT: 💥 adapter absent →<br/>throw Error('Invalid debug adapter')Affected Files
src/vs/workbench/api/browser/mainThreadDebugService.tsgetDebugAdapter):this.getDebugAdapter(handle).acceptMessage(...)threw when handle missingsrc/vs/workbench/api/browser/mainThreadDebugService.tsonDidEndSession→for (const [handle, value] of this._debugAdapters) { if (value.session === session) { this._debugAdapters.delete(handle); } }removes the adapter the in-flight message still referencessrc/vs/workbench/api/common/extHostDebugService.tsmythis._debugServiceProxy.$acceptDAMessage(debugAdapterHandle, message)fires from the extension host without coordination with main-thread map stateRepro Steps
This is a cross-process timing race; it triggers when a debug adapter emits a protocol message at the same moment its session is ending or restarting.
event/response) at or just after session stop or restart._debugAdapters(onDidEndSession), while the already-dispatched$acceptDAMessageIPC call is still being delivered.$acceptDAMessagecallsgetDebugAdapter(handle), the handle is gone, andError('Invalid debug adapter')is thrown to the unhandled-error telemetry pipeline. Rapid stop/restart cycles increase the likelihood.How the Fix Works
Chosen approach —
src/vs/workbench/api/browser/mainThreadDebugService.ts→$acceptDAMessage(): deliver the message with the same non-throwing lookup its sibling handlers already use.$acceptDAErrorand$acceptDAExitboth readthis._debugAdapters.get(handle)?....with the standing comment "don't use getDebugAdapter since an error can be expected on a post-close" — i.e. the codebase already acknowledges that cross-process messages legitimately arrive after the adapter is gone.$acceptDAMessagewas the one handler still using the throwinggetDebugAdapter(). The change makes itthis._debugAdapters.get(handle)?.acceptMessage(...), so a post-close message is silently dropped (there is no longer an adapter to deliver it to) instead of crashing. The now-unused privategetDebugAdapter()helper (its only caller) is removed.This follows the principle of handling a cross-process lifecycle race at the receiver that genuinely cannot control delivery ordering, rather than adding a
try/catchor suppressing the telemetry: the extension host is an external boundary across an IPC channel, so the main thread cannot guarantee a message is retracted once the session ends. NologService.errorcall is removed and no error is swallowed — the message is a benign post-close artifact, consistent with how errors and exits in the same state are already treated. A dropped message for an already-removed adapter is correct because the adapter (and its session) no longer exist to consume it.Alternatives considered:
onDidEndSession(delay the_debugAdapters.delete) — rejected: the delete is intentional ("any restarted session will create a new DA, so always throw the old one away") and cannot retract messages already dispatched across the IPC boundary, so the race window cannot be fully closed on the producer side.try/catch— rejected: hides the condition instead of handling it, and is inconsistent with the explicit?.pattern already established by the two sibling handlers.Recommended Owner
@dmitrivMS— author of the culprit commit/PR #338371 and merger of that PR intomicrosoft/vscode(write access), active in the repository in the last 90 days; owns the debug adapter teardown change whose timing surfaced this throw.