Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
678bc81
Fix MAUI iOS simulator VS Code launch
adamint Jun 2, 2026
b3fa1b0
Avoid MAUI launch protocol changes
adamint Jun 2, 2026
d62a003
Share dotnet run launch setup
adamint Jun 2, 2026
63b2e41
Simplify dotnet run argument helpers
adamint Jun 2, 2026
c9d0548
Fix MAUI iOS simulator process launch
adamint Jun 3, 2026
f16215e
Preserve AppHost dotnet run options for custom project executables
adamint Jun 15, 2026
976352a
Keep MAUI launch profile args after dotnet run
adamint Jun 15, 2026
1605937
Merge main into MAUI iOS simulator PR
adamint Jun 15, 2026
4fda000
Address launch args review feedback
adamint Jun 17, 2026
b41a9ae
Support MAUI debug launch metadata
adamint Jun 19, 2026
53e42df
Merge remote-tracking branch 'upstream/main' into fleet/pr-lane-vscod…
adamint Jun 19, 2026
7f6fc1e
Fix MAUI debug merge with upstream extension API
adamint Jun 19, 2026
16ca6ff
Cover missing Android physical device target
adamint Jun 19, 2026
1ef71f7
Let MAUI launch stopped Android emulators
adamint Jun 19, 2026
a34a5b8
Include MAUI target in debug session name
adamint Jun 19, 2026
08962bc
Harden MAUI debug session logging and disposal
adamint Jun 19, 2026
020439e
Fix MAUI debug review and CI regressions
adamint Jun 19, 2026
10b61f9
Fix E2E integrated browser endpoint result
adamint Jun 20, 2026
3f76df4
Adopt AppHost termination refresh hook
adamint Jun 20, 2026
4cabd8d
Merge main into MAUI iOS simulator PR
adamint Jun 20, 2026
66b8156
Remove dead MAUI device flag
adamint Jun 20, 2026
f228ad5
Restore E2E publish AppHost bridge
adamint Jun 20, 2026
fff6385
Fix explicit Aspire repo detection cache
adamint Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ extension/node_modules/
extension/.corepack-cache/
**/.vscode-test/
extension/.version
extension/.vscode-test-short.mjs
extension/local-shortpath.vscode-test.mjs
extension/v/
extension/x/

# Generated extension localization files (regenerated from XLF during build)
extension/l10n/bundle.l10n.json
Expand Down
16 changes: 16 additions & 0 deletions extension/schemas/aspire-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,22 @@
"description": "Enable or disable access to the staging channel for early access to preview features and packages",
"default": false
},
"terminalCommandsEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Experimental) Enable the 'aspire terminal' command group ('aspire terminal ps', 'aspire terminal attach'). Used in conjunction with the experimental WithTerminal() API (ASPIRETERMINAL001). Hidden by default while the API surface is in preview.",
"default": false
},
"updateNotificationsEnabled": {
"anyOf": [
{
Expand Down
16 changes: 16 additions & 0 deletions extension/schemas/aspire-global-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,22 @@
"description": "Enable or disable access to the staging channel for early access to preview features and packages",
"default": false
},
"terminalCommandsEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Experimental) Enable the 'aspire terminal' command group ('aspire terminal ps', 'aspire terminal attach'). Used in conjunction with the experimental WithTerminal() API (ASPIRETERMINAL001). Hidden by default while the API surface is in preview.",
"default": false
},
"updateNotificationsEnabled": {
"anyOf": [
{
Expand Down
16 changes: 16 additions & 0 deletions extension/schemas/aspire-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,22 @@
"description": "Enable or disable access to the staging channel for early access to preview features and packages",
"default": false
},
"terminalCommandsEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Experimental) Enable the 'aspire terminal' command group ('aspire terminal ps', 'aspire terminal attach'). Used in conjunction with the experimental WithTerminal() API (ASPIRETERMINAL001). Hidden by default while the API surface is in preview.",
"default": false
},
"updateNotificationsEnabled": {
"anyOf": [
{
Expand Down
11 changes: 11 additions & 0 deletions extension/src/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type Capability =
| 'bun' // Support for running Bun projects
| 'oven.bun-vscode' // Bun debug adapter extension identifier
| 'browser' // Support for browser debugging (built-in to VS Code via js-debug)
| 'maui' // Support for running .NET MAUI projects
| 'ms-dotnettools.dotnet-maui' // MAUI debug adapter extension identifier
| 'azure-functions'; // Support for running Azure Functions projects

export type Capabilities = Capability[];
Expand Down Expand Up @@ -48,6 +50,10 @@ export function isAzureFunctionsExtensionInstalled() {
return isExtensionInstalled("ms-azuretools.vscode-azurefunctions");
}

export function isMauiInstalled() {
return isExtensionInstalled("ms-dotnettools.dotnet-maui");
}

export function isNodeInstalled() {
// Node.js debugging uses VS Code's built-in js-debug, no extension needed
return true;
Expand Down Expand Up @@ -96,6 +102,11 @@ export function getSupportedCapabilities(): Capabilities {
capabilities.push("oven.bun-vscode");
}

if (isMauiInstalled()) {
capabilities.push("maui");
capabilities.push("ms-dotnettools.dotnet-maui");
}

return capabilities;
}

Expand Down
15 changes: 15 additions & 0 deletions extension/src/dcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ export function isAzureFunctionsLaunchConfiguration(obj: any): obj is AzureFunct
return obj && obj.type === 'azure-functions';
}

export interface MauiLaunchConfiguration extends ExecutableLaunchConfiguration {
type: "maui";
project_path: string;
target_framework?: string;
platform?: string;
target_kind?: string;
device?: string;
runtime_identifier?: string;
msbuild_properties?: Record<string, string>;
}

export function isMauiLaunchConfiguration(obj: any): obj is MauiLaunchConfiguration {
return obj && obj.type === 'maui';
}

export interface EnvVar {
name: string;
value: string;
Expand Down
123 changes: 117 additions & 6 deletions extension/src/debugger/AspireDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AnsiColors } from "../utils/AspireTerminalProvider";
import { applyTextStyle } from "../utils/strings";
import { nodeDebuggerExtension } from "./languages/node";
import { cleanupRun } from "./runCleanupRegistry";
import { runWithRunStartWrappers } from "./runStartRegistry";
import AspireRpcServer from "../server/AspireRpcServer";
import { createDebugSessionConfiguration } from "./debuggerExtensions";
import { AspireTerminalProvider } from "../utils/AspireTerminalProvider";
Expand All @@ -25,7 +26,29 @@ import type { AspireDebugConsoleOutputEvent } from "../types/extensionApi";

export type DashboardBrowserType = 'openExternalBrowser' | 'integratedBrowser' | 'debugChrome' | 'debugEdge' | 'debugFirefox';

export function getLoggableDebugConfiguration(debugConfig: AspireResourceExtendedDebugConfiguration, includeEnvironment: boolean): vscode.DebugConfiguration {
if (includeEnvironment && debugConfig.type !== 'maui') {
return debugConfig;
}

if (includeEnvironment) {
return {
...debugConfig,
environmentVariables: debugConfig.environmentVariables ? '<redacted>' : undefined,
};
}

return {
...debugConfig,
env: debugConfig.env ? '<redacted>' : undefined,
environmentVariables: debugConfig.environmentVariables ? '<redacted>' : undefined,
msbuildProperties: debugConfig.msbuildProperties instanceof Map ? Object.fromEntries(debugConfig.msbuildProperties) : debugConfig.msbuildProperties,
};
}

export class AspireDebugSession implements vscode.DebugAdapter {
private static readonly _mauiDebugStartMaxAttempts = 3;
private static readonly _mauiDebugStartRetryDelayMs = 5000;
private readonly _onDidSendMessage = new EventEmitter<any>();
private readonly _onDidSendDebugConsoleOutput = new EventEmitter<AspireDebugConsoleOutputEvent>();
private _messageSeq = 1;
Expand Down Expand Up @@ -204,6 +227,31 @@ export class AspireDebugSession implements vscode.DebugAdapter {
body: {}
});
}
else if (message.command === 'setBreakpoints') {
const breakpoints = Array.isArray(message.arguments?.breakpoints)
? message.arguments.breakpoints
: [];

this.sendResponse(message, {
// The Aspire adapter does not bind user breakpoints itself, but VS Code still
// sends breakpoint requests to every active debug session. The DAP response
// must include a breakpoint array; otherwise newer VS Code builds throw while
// reading the missing body.breakpoints field and can prevent child sessions
// from receiving the same source breakpoints.
breakpoints: breakpoints.map((breakpoint: { line?: number; column?: number }, index: number) => ({
id: index + 1,
verified: false,
line: breakpoint.line,
column: breakpoint.column,
}))
});
}
else if (message.command === 'setFunctionBreakpoints' || message.command === 'setDataBreakpoints') {
this.sendResponse(message, { breakpoints: [] });
}
else if (message.command === 'setExceptionBreakpoints') {
this.sendResponse(message, { breakpoints: [] });
}
else if (message.command) {
// Respond to all other requests with a generic success
this.sendEvent({
Expand Down Expand Up @@ -440,17 +488,25 @@ export class AspireDebugSession implements vscode.DebugAdapter {

async startAndGetDebugSession(debugConfig: AspireResourceExtendedDebugConfiguration): Promise<AspireResourceDebugSession | undefined> {
return new Promise(async (resolve) => {
const logConfig = this._terminalProvider.isDebugConfigEnvironmentLoggingEnabled()
? debugConfig
: { ...debugConfig, env: debugConfig.env ? '<redacted>' : undefined };
const logConfig = getLoggableDebugConfiguration(debugConfig, this._terminalProvider.isDebugConfigEnvironmentLoggingEnabled());
extensionLogOutputChannel.info(`Starting debug session with configuration: ${JSON.stringify(logConfig)}`);
this.createDebugAdapterTrackerCore(debugConfig.type);

let resolved = false;
const disposable = vscode.debug.onDidStartDebugSession(session => {
if (session.configuration.runId === debugConfig.runId) {
extensionLogOutputChannel.info(`Debug session started: ${session.name} (run id: ${session.configuration.runId})`);
disposable.dispose();

if (this._disposed) {
extensionLogOutputChannel.info(`Stopping debug session that started after Aspire session disposal: ${session.name} (run id: ${session.configuration.runId})`);
vscode.debug.stopDebugging(session);
cleanupRun(debugConfig.runId);
resolved = true;
resolve(undefined);
return;
}

const disposalFunction = () => {
extensionLogOutputChannel.info(`Stopping debug session: ${session.name} (run id: ${session.configuration.runId})`);
vscode.debug.stopDebugging(session);
Expand All @@ -470,23 +526,74 @@ export class AspireDebugSession implements vscode.DebugAdapter {
dispose: disposalFunction
});

resolved = true;
resolve(vsCodeDebugSession);
}
});

const started = await vscode.debug.startDebugging(undefined, debugConfig, this._session);
if (!started) {
let started = false;
try {
const workspaceFolder = this.getDebugSessionWorkspaceFolder(debugConfig);
const maxAttempts = debugConfig.type === 'maui' ? AspireDebugSession._mauiDebugStartMaxAttempts : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (this._disposed) {
break;
}

started = await runWithRunStartWrappers(debugConfig.runId, () => this.startDebugging(workspaceFolder, debugConfig));
if (started) {
break;
}

if (attempt < maxAttempts && !this._disposed) {
extensionLogOutputChannel.warn(`Debug session did not start for run ID ${debugConfig.runId}; retrying (${attempt}/${maxAttempts}).`);
await delay(AspireDebugSession._mauiDebugStartRetryDelayMs);
}
}
} catch (error) {
disposable.dispose();
cleanupRun(debugConfig.runId);
extensionLogOutputChannel.error(`Failed to start debug session: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
resolved = true;
resolve(undefined);
return;
}

setTimeout(() => {
if (!started) {
disposable.dispose();
cleanupRun(debugConfig.runId);
resolved = true;
resolve(undefined);
}

setTimeout(() => {
if (!resolved) {
disposable.dispose();
cleanupRun(debugConfig.runId);
resolved = true;
resolve(undefined);
}
}, 10000);
});
}

private async startDebugging(workspaceFolder: vscode.WorkspaceFolder | undefined, debugConfig: AspireResourceExtendedDebugConfiguration): Promise<boolean> {
// VS Code terminates the parent debug session when the MAUI extension cancels
// a parented child launch before the MAUI project system is ready. We still
// track and stop the MAUI session ourselves once it starts, so leave it
// unparented to keep the AppHost alive across bounded start retries.
const parentSession = debugConfig.type === 'maui' ? undefined : this._session;
return await vscode.debug.startDebugging(workspaceFolder, debugConfig, parentSession);
}

private getDebugSessionWorkspaceFolder(debugConfig: AspireResourceExtendedDebugConfiguration): vscode.WorkspaceFolder | undefined {
const resourcePath = typeof debugConfig.cwd === 'string'
? debugConfig.cwd
: typeof debugConfig.program === 'string' ? debugConfig.program : undefined;

return resourcePath ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(resourcePath)) : undefined;
}

/**
* Opens the dashboard URL in the specified browser.
* For debugChrome/debugEdge/debugFirefox, launches as a child debug session that auto-closes with the Aspire debug session.
Expand Down Expand Up @@ -702,6 +809,10 @@ export class AspireDebugSession implements vscode.DebugAdapter {
}
}

function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

export function buildAspireCommandArgs(command: string, commandArgs: string[], extensionArgs: string[]): string[] {
const args = [command];
const separatorIndex = commandArgs.indexOf('--');
Expand Down
9 changes: 8 additions & 1 deletion extension/src/debugger/debuggerExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { debugProject, runProject } from "../loc/strings";
import { getEnvironmentWithoutE2EBridgeVariables, mergeEnvs } from "../utils/environment";
import { extensionLogOutputChannel } from "../utils/logging";
import { projectDebuggerExtension } from "./languages/dotnet";
import { isAzureFunctionsExtensionInstalled, isBunInstalled, isCsharpInstalled, isGoInstalled, isPythonInstalled } from '../capabilities';
import { isAzureFunctionsExtensionInstalled, isBunInstalled, isCsharpInstalled, isGoInstalled, isMauiInstalled, isPythonInstalled } from '../capabilities';
import { pythonDebuggerExtension } from "./languages/python";
import { nodeDebuggerExtension } from "./languages/node";
import { browserDebuggerExtension } from "./languages/browser";
import { azureFunctionsDebuggerExtension } from "./languages/azureFunctions";
import { goDebuggerExtension } from "./languages/go";
import { bunDebuggerExtension } from "./languages/bun";
import { mauiDebuggerExtension } from "./languages/maui";
import { isDirectory } from "../utils/io";
import { waitForRunStartIdle } from "./runStartRegistry";

// Represents a resource-specific debugger extension for when the default session configuration is not sufficient to launch the resource.
export interface ResourceDebuggerExtension {
Expand All @@ -30,6 +32,7 @@ export async function createDebugSessionConfiguration(debugSessionConfig: Aspire
}

const projectPath = debuggerExtension.getProjectFile(launchConfig);
await waitForRunStartIdle();

const configuration: AspireResourceExtendedDebugConfiguration = {
type: debuggerExtension.debugAdapter || launchConfig.type,
Expand Down Expand Up @@ -93,5 +96,9 @@ export function getResourceDebuggerExtensions(): ResourceDebuggerExtension[] {
extensions.push(bunDebuggerExtension);
}

if (isMauiInstalled()) {
extensions.push(mauiDebuggerExtension);
}

return extensions;
}
Loading
Loading