Skip to content

Support VS Code browser debug session termination - #18626

Draft
Adam Ratzman (adamint) wants to merge 50 commits into
microsoft:mainfrom
adamint:adamint-vscode-browser-wasm-debug-session
Draft

Adam Ratzman (adamint) wants to merge 50 commits into
microsoft:mainfrom
adamint:adamint-vscode-browser-wasm-debug-session

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Jul 3, 2026

Copy link
Copy Markdown
Member

Description

Adds the VS Code extension lifecycle needed for WithBrowserDebugger() browser resources.

  • Builds Chromium launch configurations for DCP browser runs.
  • Uses the root browser debug session as the run lifetime signal instead of js-debug child page/worker sessions.
  • Reports natural termination exactly once.
  • Makes explicit browser stops awaitable, single-flight, bounded, and retryable after failure.
  • Keeps DCP browser runs alive until VS Code confirms the browser session stopped; a failed or timed-out stop returns an error and can be retried.
  • Stops browser sessions that finish starting after the owning Aspire session has already shut down.

The implementation is intentionally Chromium-only. The Firefox adapter defaults sessions without an explicit port to port 6000 and does not support a race-free dynamic port reservation, so it cannot safely support multiple Aspire browser resources. Managed WASM debugging through the C# debug bridge remains separate work under #17797.

Relationship to #19125

#19125 owns the general adapter-backed DCP stop/retention lifecycle. This PR preserves the browser-specific confirmed-stop rule: DCP must not report a browser run as terminated until VS Code confirms the root browser session ended. If #19125 lands first, this branch should retain that distinction while rebasing its overlapping DCP code.

Validation

  • TypeScript compile and lint
  • 219 focused tests covering browser configuration, Chromium root-session termination, retry/timeout handling, DCP retention, adapter tracking, and Aspire debug-session integration

Part of #17797

Checklist

  • Browser debug-session termination slice is complete
  • Tests cover the changed behavior
  • No public API added
  • Managed WASM bridge work remains out of scope

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings July 3, 2026 02:13
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18626

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 18626"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a focused slice of browser debugging support for WithBrowserDebugger() in the Aspire VS Code extension. When DCP asks the extension to launch a browser resource, it now configures a js-debug (pwa-chrome/pwa-msedge) launch with a stable isolated user-data directory and clean-launch runtime flags, and reports sessionTerminated back to DCP when the browser debug session ends. This prevents closed browser debug resources from appearing as orphaned run sessions. WASM managed debugging via the C# debug bridge is explicitly left as follow-up work.

Changes:

  • Rewrote the browser launch configuration builder to normalize the browser name to a js-debug adapter type, merge/deduplicate clean-launch runtime args, use a per-run user-data directory, and require a URL.
  • Added sendSessionTerminated plus a one-shot terminate listener in AspireDebugSession that emits the DCP sessionTerminated notification (with de-duplication) when the browser session ends, driven by two new optional config fields.
  • Added unit tests covering adapter selection, runtime-arg merging, user-data-dir, and the terminate-to-notification flow.
Show a summary per file
File Description
extension/src/debugger/languages/browser.ts Builds the js-debug browser launch config: adapter mapping, runtime-arg merge, isolated user-data dir, and session-terminated flags.
extension/src/debugger/AspireDebugSession.ts Adds sendSessionTerminated (de-duplicated) and wires a terminate listener to emit the DCP notification on browser session end.
extension/src/dcp/types.ts Adds optional sessionTerminatedDcpId and sendSessionTerminatedOnDebugSessionEnd fields to the extended debug configuration.
extension/src/test/browserDebugger.test.ts New tests for adapter selection, runtime-arg preservation, user-data-dir, and the terminate→sessionTerminated flow.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread extension/src/debugger/AspireDebugSession.ts Outdated
Comment thread extension/src/debugger/languages/browser.ts Outdated
Adam Ratzman (adamint) and others added 2 commits July 2, 2026 20:23
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 3, 2026 02:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread extension/src/debugger/languages/browser.ts Outdated
Comment thread extension/src/debugger/AspireDebugSession.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 6, 2026 09:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread extension/src/debugger/languages/browser.ts Outdated
Adam Ratzman (adamint) and others added 3 commits July 6, 2026 03:42
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 6, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

Comment thread extension/src/debugger/AspireDebugSession.ts Outdated
Wait for VS Code to settle browser debug shutdown before resolving a disposed-after-start launch as failed, so run cleanup cannot delete the browser profile while js-debug is still stopping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI review requested due to automatic review settings July 28, 2026 02:41
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (4)

extension/src/test/browserDebugger.test.ts:17

  • These tests stub every vscode.debug lifecycle API, so they cannot catch regressions in real Extension Host adapter selection or session event ordering. The PR description claims a browserDebugger.e2e.test.js shard and workflow rows, but neither the E2E source nor a browser-debugger row exists in the current tree. Add the claimed extension E2E coverage and workflow entries so this debugger flow runs in CI.
    test('configures Chromium for an isolated browser that exits with the debug session', async () => {

extension/src/debugger/languages/browser.ts:31

  • BrowserLaunchConfiguration.url is optional and isBrowserLaunchConfiguration() only checks type, so this path can still emit a js-debug launch with url: undefined. Validate a non-empty URL before selecting the adapter; otherwise malformed DCP input reaches VS Code instead of failing as an invalid launch configuration.
        debugConfiguration.type = getBrowserDebugAdapter(launchConfig.browser);

extension/src/debugger/languages/browser.ts:59

  • This does not implement the profile ownership model stated in the PR description and security checklist. The branch delegates profile creation and cleanup to js-debug via userDataDir = true; there is no Aspire mkdtemp, realpath containment check, or registered recursive cleanup. Either restore the described security-sensitive implementation and its tests, or update the description/checklist and validation claims to match the actual threat and cleanup model.
            // Let js-debug create and clean up the isolated profile. A workspace-provided profile
            // path or command-line override would make Aspire stop a session without necessarily
            // owning the browser instance that was launched.
            debugConfiguration.userDataDir = true;
            debugConfiguration.runtimeArgs = mergeRuntimeArgs(debugConfiguration.runtimeArgs);

extension/src/debugger/AspireDebugSession.ts:659

  • For process-backed sessions, this is now the only successful-stop path that calls cleanupRun, but no listener performs the same bookkeeping when the debug session ends naturally. adapterTracker.ts:102-124 sends DCP termination on adapter exit without cleanup, so handlers such as the Azure Functions host cleanup registered at languages/azureFunctions.ts:241 remain leaked after a normal exit. Observe root-session termination for non-browser resources too (without duplicating the adapter's DCP notification) and run cleanup there.
            stopSessionPromise = Promise.resolve(vscode.debug.stopDebugging(session)).then(
              () => {
                // Run cleanup only after VS Code confirms the session stopped. A failed stop keeps
                // both the DCP run and its resource-specific cleanup available for a retry.
                cleanupRun(debugConfig.runId);
  • Files reviewed: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (3)

extension/src/debugger/languages/browser.ts:75

  • There is no browser debugger test under extension/src/test-e2e; all new adapter-selection and missing-Firefox tests stub VS Code APIs. Restore extension-host coverage that calls the browser resource configuration path and verifies Edge/Chrome/Firefox adapter selection and the missing-adapter error. This can remain configuration-only and does not need to launch a real browser.
        if (debugType === firefoxDebugAdapterType && !isFirefoxDebuggerInstalled()) {
            promptToInstallFirefoxDebugger();
            throw new Error(firefoxDebuggerNotInstalled);

extension/src/debugger/browserDebugSessionTermination.ts:44

  • A stop that reaches the outer 10-second deadline is not actually retryable. resetStopSessionAttempt clears only the wrapper in AspireDebugSession; this class keeps _stopPromise pointing at the original never-settling stopCore(), so the next attempt reuses it and times out without issuing another vscode.debug.stopDebugging call. Expose a way to clear the browser stop attempt and invoke it from each timeout path (including DCP teardown) before allowing a retry.
        this._stopPromise ??= this.stopCore();

        return this._stopPromise;

extension/src/debugger/languages/browser.ts:60

  • The supported-browser path still accepts a missing or whitespace-only url: the type guard only checks type, so Firefox (and the existing Chromium entries) reaches startDebugging with url: undefined and fails inside the adapter. Keep the unsupported-browser diagnostic as-is, then explicitly reject a supported browser whose URL is absent with a missing-URL launch-configuration error before assigning debugConfiguration.url.
            extensionLogOutputChannel.warn(`No supported debug adapter is registered for browser '${browser}'.`);
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

this._terminationListener.dispose();

if (this._dcpId) {
this._sendSessionTerminated(this._runId, this._dcpId);

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.

finish() marks the lifecycle complete, then sends the DCP notification before cleanupRun. If that synchronous callback throws, the browser run cleanup is skipped even though the listener is already disposed. This helper owns terminal cleanup, so I think cleanup should run in a finally; I'll add a throwing-callback test.

// can run concurrently without conflicting
debugConfiguration.userDataDir = true;

if (debugConfiguration.type === firefoxDebugAdapterType) {

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 Firefox adapter defaults every launch without port to 6000. Two Aspire Firefox browser resources can therefore collide or connect the second adapter to the first process, and the adapter doesn't support port: 0 or another race-free reservation mechanism. Firefox support was added during this PR, but it isn't safe for Aspire's multi-resource model yet. I'm going to remove Firefox from this PR and leave Chromium termination as the supported path rather than ship a hidden single-session constraint.

return merged;
}

function isUserDataDirArg(arg: string): boolean {

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.

There's still a Windows hole in this fix: isUserDataDirArg compares the switch name case-sensitively, while Chromium normalizes switch names on Windows. --User-Data-Dir=... therefore survives and can replace js-debug's isolated profile. I'll make this comparison case-insensitive and cover both combined and split forms.

this._finished = true;
this._terminationListener.dispose();

if (this._dcpId) {

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 warning we said we'd keep here was lost in the later simplification. A null DCP ID now silently skips the terminal notification. Normal DCP starts should always have one, but this should still log the run-specific warning so the invalid state is diagnosable. I'll restore that with coverage.

return Promise.resolve();
}

this._stopPromise ??= this.stopCore();

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.

Timeouts still break the retry guarantee here. The outer stop promise is cleared when the budget expires, but BrowserDebugSessionTermination._stopPromise and the DCP teardownPromise keep the original pending operation. Every later DELETE then reuses the same hung stop. I'll make resets identity/generation-based across the three layers and cover a permanently hung first attempt followed by a successful retry.

Two conflicts, both additive collisions where the branch and main added
different code at the same place:

- capabilities.ts: the branch's Firefox debugger detection and main's Java
  detection landed adjacently. Union of both.
- aspireDebugSession.test.ts: import lists. Main's set plus the branch's
  cleanupRun.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 522a7395-2ea1-4d1a-91b3-40050853b9f4

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 15/15 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread extension/src/debugger/languages/browser.ts Outdated
Comment thread extension/src/debugger/browserDebugSessionTermination.ts Outdated
Adam Ratzman (adamint) and others added 2 commits August 31, 2026 18:57
…r-wasm-debug-session

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

# Conflicts:
#	extension/loc/xlf/aspire-vscode.xlf
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

New issues introduced by this change (2)
Severity Finding
High severity extension/​src/​dcp/​AspireDcpServer.ts — The stop timeout does not cover this await. If browser startup never settles (for example,…
Low severity extension/​src/​test-e2e/​browserDebugger.e2e.test.ts — This test confirms that Firefox is still rejected, but the PR description explicitly says Firefox…
Pre-existing issues (1)
Severity Finding
Medium severity extension/​src/​debugger/​browserDebugSessionTermination.ts — A stop that exceeds Aspire's shutdown budget leaves this promise pending forever.… View comment
Issues resolved since last review (1)
Severity Finding
Low severity extension/​src/​debugger/​languages/​browser.ts — This user-visible debugger path has only stubbed unit coverage; there is currently no browser… View resolved comment

Comment thread extension/src/dcp/AspireDcpServer.ts Outdated
Comment thread extension/src/test-e2e/browserDebugger.e2e.test.ts
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@adamint

Copy link
Copy Markdown
Member Author

Two Copilot findings still need cleanup on the current head: the browser stop budget needs to cover a startup that never settles, and the PR description still advertises Firefox after the implementation intentionally narrowed to Chromium. I'm fixing both before calling this ready.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7c01c030-226e-41c5-81ca-a6483a1856df

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

Review tier: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

New issues introduced by this change (1)
Severity Finding
High severity extension/​src/​debugger/​AspireDebugSession.ts — This browser lifecycle registration is skipped by the isShuttingDown early return above. That…
Pre-existing issues (2)
Severity Finding
Medium severity extension/​src/​debugger/​browserDebugSessionTermination.ts — A stop that exceeds Aspire's shutdown budget leaves this promise pending forever.… View comment
Low severity extension/​src/​test-e2e/​browserDebugger.e2e.test.ts — This test confirms that Firefox is still rejected, but the PR description explicitly says Firefox… View comment
Issues resolved since last review (1)
Severity Finding
High severity extension/​src/​dcp/​AspireDcpServer.ts — The stop timeout does not cover this await. If browser startup never settles (for example,… View resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

extension/src/debugger/browserDebugSessionTermination.ts:71

  • This method is invoked through _disposables only after ordered shutdown succeeded or during irreversible finalizeForExtensionShutdown(), yet a genuine stop rejection leaves the global termination listener registered indefinitely. That contradicts finalization's listener-release contract and retains this session object after its owner is gone. Dispose _terminationListener in the rejection handler; a root termination that races the rejection already makes stopCore() return successfully via _finished.
    stopAndDisposeOnFailure(): void {
        // A failed explicit stop can still be followed by a natural root-session termination.
        // Keep observing that event while handling the rejection so disposal cannot create an
        // unhandled promise or suppress the eventual DCP notification and cleanup.
        void this.stop().catch(() => { });

Comment thread extension/src/debugger/AspireDebugSession.ts Outdated
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@adamint

Copy link
Copy Markdown
Member Author

One current lifecycle gap remains: a browser session created during shutdown is stopped on the late-session path, but DCP loses the session handle and can report termination before that stop is confirmed. I’m keeping the late browser session reachable until its stop result is known, while preserving the existing shutdown ordering and retry semantics.

…llowup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7c01c030-226e-41c5-81ca-a6483a1856df
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7c01c030-226e-41c5-81ca-a6483a1856df

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The lifecycle changes are coherent and comprehensively covered by focused race, retry, shutdown, and configuration tests.

Review tier: Balanced
Findings: 1 Low severity

Pre-existing issues (1)
Severity Finding
Low severity extension/​src/​test-e2e/​browserDebugger.e2e.test.ts — This test confirms that Firefox is still rejected, but the PR description explicitly says Firefox… View comment
Issues resolved since last review (2)
Severity Finding
High severity extension/​src/​debugger/​AspireDebugSession.ts — This browser lifecycle registration is skipped by the isShuttingDown early return above. That… View resolved comment
Medium severity extension/​src/​debugger/​browserDebugSessionTermination.ts — A stop that exceeds Aspire's shutdown budget leaves this promise pending forever.… View resolved comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants