Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
14 changes: 11 additions & 3 deletions extension/src/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ export type Capability =
| 'project' // Support for running C# projects
| 'ms-dotnettools.csharp' // Older AppHost versions used this extension identifier instead of project
| 'python' // Support for running Python projects
| 'ms-python.python'; // Older AppHost versions used this extension identifier instead of python
| 'ms-python.python' // Older AppHost versions used this extension identifier instead of python
| 'node' // Support for running Node.js/JavaScript projects (built-in to VS Code)
| 'browser'; // Support for browser debugging (built-in to VS Code via js-debug)

export type Capabilities = Capability[];
export type Capabilities = (Capability | string)[];

function isExtensionInstalled(extensionId: string): boolean {
const extension = vscode.extensions.getExtension(extensionId);
Expand All @@ -33,7 +35,8 @@ export function isPythonInstalled() {
}

export function getSupportedCapabilities(): Capabilities {
const capabilities: Capabilities = ['prompting', 'baseline.v1', 'secret-prompts.v1', 'build-dotnet-using-cli'];
// Node.js and browser debugging are built into VS Code via ms-vscode.js-debug, so always available
const capabilities: Capabilities = ['prompting', 'baseline.v1', 'secret-prompts.v1', 'build-dotnet-using-cli', 'node', 'browser'];

if (isCsDevKitInstalled()) {
capabilities.push("devkit");
Expand All @@ -50,6 +53,11 @@ export function getSupportedCapabilities(): Capabilities {
capabilities.push("ms-python.python");
}

// Also, push all extensions as capabilities
for (const extension of vscode.extensions.all) {
capabilities.push(extension.id);
}

return capabilities;
}

Expand Down
4 changes: 2 additions & 2 deletions extension/src/dcp/AspireDcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ export default class AspireDcpServer {
}

const launchConfig = payload.launch_configurations[0];
const foundDebuggerExtension = debuggerExtensions.find(ext => ext.resourceType === launchConfig.type) ?? null;
const foundDebuggerExtension = debuggerExtensions.find(ext => ext.resourceType === launchConfig.type);

if (!foundDebuggerExtension) {
if (!foundDebuggerExtension && !launchConfig.debugger_properties) {
const error: ErrorDetails = {
code: 'UnsupportedLaunchConfiguration',
message: `Unsupported launch configuration type: ${launchConfig.type}`,
Expand Down
21 changes: 21 additions & 0 deletions extension/src/dcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,30 @@ export interface ErrorDetails {

type LaunchConfigurationMode = "Debug" | "NoDebug";

/**
* Debugger properties passed from the apphost.
* Contains DAP-standard properties (type, name, request, cwd) plus any
* IDE-specific and debug adapter-specific properties.
* @see https://microsoft.github.io/debug-adapter-protocol/specification
*/
export interface DebuggerProperties {
type: string;
name: string;
request: string;
cwd: string;
[key: string]: any; // Allow additional IDE-specific and adapter-specific properties
}

export interface ExecutableLaunchConfiguration {
type: string;
mode?: LaunchConfigurationMode | undefined;
/**
* Debugger-specific properties passed from the apphost.
* Contains required VS Code debug configuration properties (type, name, request, cwd)
* plus any additional adapter-specific properties.
* @see https://code.visualstudio.com/docs/debugtest/debugging-configuration
*/
debugger_properties?: DebuggerProperties;
}

export interface ProjectLaunchConfiguration extends ExecutableLaunchConfiguration {
Expand Down
56 changes: 42 additions & 14 deletions extension/src/debugger/debuggerExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "path";
import { ExecutableLaunchConfiguration, EnvVar, LaunchOptions, AspireResourceExtendedDebugConfiguration, AspireExtendedDebugConfiguration } from "../dcp/types";
import { debugProject, runProject } from "../loc/strings";
import { debugProject, invalidLaunchConfiguration, runProject } from "../loc/strings";
import { mergeEnvs } from "../utils/environment";
import { extensionLogOutputChannel } from "../utils/logging";
import { projectDebuggerExtension } from "./languages/dotnet";
Expand All @@ -17,31 +17,59 @@ export interface ResourceDebuggerExtension {
getProjectFile: (launchConfig: ExecutableLaunchConfiguration) => string;
getSupportedFileTypes: () => string[];
createDebugSessionConfigurationCallback?: (launchConfig: ExecutableLaunchConfiguration, args: string[] | undefined, env: EnvVar[], launchOptions: LaunchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration) => Promise<void>;
isDeprecated: boolean;
}

export async function createDebugSessionConfiguration(debugSessionConfig: AspireExtendedDebugConfiguration, launchConfig: ExecutableLaunchConfiguration, args: string[] | undefined, env: EnvVar[], launchOptions: LaunchOptions, debuggerExtension: ResourceDebuggerExtension): Promise<AspireResourceExtendedDebugConfiguration> {
export async function createDebugSessionConfiguration(debugSessionConfig: AspireExtendedDebugConfiguration, launchConfig: ExecutableLaunchConfiguration, args: string[] | undefined, env: EnvVar[], launchOptions: LaunchOptions, debuggerExtension?: ResourceDebuggerExtension): Promise<AspireResourceExtendedDebugConfiguration> {
if (debuggerExtension === null) {
extensionLogOutputChannel.warn(`Unknown type: ${launchConfig.type}.`);
}

const projectPath = debuggerExtension.getProjectFile(launchConfig);

const configuration: AspireResourceExtendedDebugConfiguration = {
type: debuggerExtension.debugAdapter || launchConfig.type,
request: 'launch',
name: launchOptions.debug ? debugProject(debuggerExtension.getDisplayName(launchConfig)) : runProject(debuggerExtension.getDisplayName(launchConfig)),
program: projectPath,
const baseConfig = {
args: args,
cwd: await isDirectory(projectPath) ? projectPath : path.dirname(projectPath),
env: mergeEnvs(process.env, env),
justMyCode: false,
stopAtEntry: false,
noDebug: !launchOptions.debug,
runId: launchOptions.runId,
debugSessionId: launchOptions.debugSessionId,
console: 'internalConsole'
};

let configuration: AspireResourceExtendedDebugConfiguration | undefined;

if (debuggerExtension) {
// For example, the Python debugger extension has been superceded by apphost-driven debugger properties.
// Thus, we can skip this section if debugger_properties exist and the debugger extension is deprecated.
if (launchConfig.debugger_properties && debuggerExtension.isDeprecated) {
extensionLogOutputChannel.debug(`Skipping debugger configuration for deprecated extension: ${debuggerExtension.extensionId}`);
}
else {
const projectPath = debuggerExtension.getProjectFile(launchConfig);

configuration = {
type: debuggerExtension.debugAdapter || launchConfig.type,
request: 'launch',
name: launchOptions.debug ? debugProject(debuggerExtension.getDisplayName(launchConfig)) : runProject(debuggerExtension.getDisplayName(launchConfig)),
program: projectPath,
justMyCode: false,
stopAtEntry: false,
cwd: await isDirectory(projectPath) ? projectPath : path.dirname(projectPath),
console: 'internalConsole',
...baseConfig
};
}
}

if (launchConfig.debugger_properties) {
configuration = {
...baseConfig,
...launchConfig.debugger_properties
} as AspireResourceExtendedDebugConfiguration;
}

if (!configuration) {
extensionLogOutputChannel.error(`Failed to create debug configuration for ${JSON.stringify(launchConfig)} - resulting configuration was undefined or empty.`);
throw new Error(invalidLaunchConfiguration(JSON.stringify(configuration)));
Comment on lines +69 to +70

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

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

This throws invalidLaunchConfiguration(JSON.stringify(configuration)) when configuration is undefined. With strict TS settings, JSON.stringify(undefined) is undefined (not a string), so this won’t type-check and also loses the actual failing input. Pass a definite string (e.g., details about launchConfig / launchConfig.type) instead.

Suggested change
extensionLogOutputChannel.error(`Failed to create debug configuration for ${JSON.stringify(launchConfig)} - resulting configuration was undefined or empty.`);
throw new Error(invalidLaunchConfiguration(JSON.stringify(configuration)));
const failureDetails = JSON.stringify({ launchConfigType: launchConfig.type, launchConfig });
extensionLogOutputChannel.error(`Failed to create debug configuration for ${JSON.stringify(launchConfig)} - resulting configuration was undefined or empty.`);
throw new Error(invalidLaunchConfiguration(failureDetails));

Copilot uses AI. Check for mistakes.
}

if (debugSessionConfig.debuggers) {
// 1. Check if this is the apphost
if (launchOptions.isApphost && debugSessionConfig.debuggers['apphost']) {
Expand All @@ -55,7 +83,7 @@ export async function createDebugSessionConfiguration(debugSessionConfig: Aspire
}


if (debuggerExtension.createDebugSessionConfigurationCallback) {
if (debuggerExtension?.createDebugSessionConfigurationCallback) {
await debuggerExtension.createDebugSessionConfigurationCallback(launchConfig, args, env, launchOptions, configuration);
}

Expand Down
1 change: 1 addition & 0 deletions extension/src/debugger/languages/dotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export function createProjectDebuggerExtension(dotNetServiceProducer: (debugSess
extensionId: 'ms-dotnettools.csharp',
getDisplayName: (launchConfig: ExecutableLaunchConfiguration) => `C#: ${path.basename((launchConfig as ProjectLaunchConfiguration).project_path)}`,
getSupportedFileTypes: () => ['.cs', '.csproj'],
isDeprecated: false,
getProjectFile: (launchConfig) => {
if (isProjectLaunchConfiguration(launchConfig)) {
return launchConfig.project_path;
Expand Down
1 change: 1 addition & 0 deletions extension/src/debugger/languages/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const pythonDebuggerExtension: ResourceDebuggerExtension = {
getDisplayName: (launchConfiguration: ExecutableLaunchConfiguration) => `Python: ${vscode.workspace.asRelativePath(getProjectFile(launchConfiguration))}`,
getSupportedFileTypes: () => ['.py'],
getProjectFile: (launchConfig) => getProjectFile(launchConfig),
isDeprecated: true,
createDebugSessionConfigurationCallback: async (launchConfig, args, env, launchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration): Promise<void> => {
if (!isPythonLaunchConfiguration(launchConfig)) {
extensionLogOutputChannel.info(`The resource type was not python for ${JSON.stringify(launchConfig)}`);
Expand Down
7 changes: 6 additions & 1 deletion extension/src/server/rpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ export class RpcClient implements ICliRpcClient {

async stopCli() {
if (!this._connectionClosed) {
await this._messageConnection.sendRequest('stopCli');
try {
await this._messageConnection.sendRequest('stopCli');
} catch (error) {
// Connection may have been closed during shutdown, which is expected
extensionLogOutputChannel.info(`stopCli request failed (likely connection already closed): ${error}`);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions extension/src/utils/AspireTerminalProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ export class AspireTerminalProvider implements vscode.Disposable {
const env: any = {
...process.env,

// IDE identification for Aspire debug features
ASPIRE_IDE: 'vscode',

// Extension connection information
ASPIRE_EXTENSION_ENDPOINT: this.rpcServerConnectionInfo.address,
ASPIRE_EXTENSION_TOKEN: this.rpcServerConnectionInfo.token,
Expand All @@ -150,6 +153,7 @@ export class AspireTerminalProvider implements vscode.Disposable {
env.ASPIRE_EXTENSION_DEBUG_RUN_MODE = noDebug === false ? "Debug" : "NoDebug";
env.DEBUG_SESSION_INFO = JSON.stringify(getRunSessionInfo());
env.ASPIRE_EXTENSION_CAPABILITIES = getSupportedCapabilities().join(',');
env.ASPIRE_EXTENSION_WORKSPACE_ROOT = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;

Comment on lines 168 to 171

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

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

ASPIRE_EXTENSION_WORKSPACE_ROOT may be set to undefined when VS Code has no workspace folder. That can flow into the AppHost as the literal string "undefined" and break relative-path display logic. Consider only setting this env var when a workspace folder exists (or set it to an empty string / omit it).

Copilot uses AI. Check for mistakes.
// if DCP debug logging is enabled, set DCP-specific logging environment variables
const dcpDebugLoggingEnabled = vscode.workspace.getConfiguration('aspire').get<boolean>('enableAspireDcpDebugLogging', false);
Expand Down
1 change: 1 addition & 0 deletions extension/src/utils/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export async function checkCliAvailableOrRedirect(cliPath: string): Promise<bool
cliAvailableOnPath = true;
return true;
} catch (error) {
extensionLogOutputChannel.error(`Aspire CLI not found at path: ${cliPath}. Error: ${error}`);
cliAvailableOnPath = false;
vscode.window.showErrorMessage(
cliNotAvailable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
.WithEnvironment("BROWSER", "none") // Disable opening browser on npm start
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints()
.WithBrowserDebugger(configureDebuggerProperties: props =>
{
// Enable verbose trace to see source map resolution in Debug Console
props.Trace = "verbose";
})
.PublishAsDockerFile();

builder.AddJavaScriptApp("vue", "../AspireJavaScript.Vue")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Allow experimental browser debugger API -->
<NoWarn>$(NoWarn);ASPIREEXTENSION001</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,12 @@
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddCors();

var app = builder.Build();

app.MapDefaultEndpoints();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors(static builder =>
builder.AllowAnyMethod()
Expand All @@ -42,8 +34,7 @@
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
.WithName("GetWeatherForecast");

app.UseFileServer();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5084",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand All @@ -23,7 +22,6 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7167;http://localhost:5084",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ app.use((err, req, res, next) => {
});

// General 404 handler (for non-API routes)
app.use('*', (req, res) => {
app.use((req, res) => {
res.status(404).json({
message: 'Route not found',
path: req.originalUrl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const HTMLWebpackPlugin = require("html-webpack-plugin");
module.exports = (env) => {
return {
entry: "./src/index.js",
devtool: "source-map",
devServer: {
port: env.PORT || 4001,
allowedHosts: "all",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

var builder = DistributedApplication.CreateBuilder(args);

builder.AddAzureContainerAppEnvironment("env");
Expand Down Expand Up @@ -41,6 +43,15 @@
#endif
.WithReference(queue)
.WithReference(blob)
.WithReference(funcApp);
.WithReference(funcApp)
.WithVSCodeCSharpDebuggerProperties(props =>
{
props.Logging = new VSCodeCSharpDebuggerProperties.LoggingOptions
{
ModuleLoad = false
};

props.RequireExactSource = false;
});

builder.Build().Run();
7 changes: 7 additions & 0 deletions playground/Stress/Stress.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
Expand Down Expand Up @@ -161,6 +162,12 @@
.WithEnvironment("ENV_TO_OVERRIDE", "this value came from the apphost")
.WithArgs("arg_from_apphost");

builder.AddProject<Projects.Stress_Empty>("empty-profile-3-fallback-to-process")
.WithVSCodeCSharpDebuggerProperties(props =>
{
throw new InvalidOperationException("Simulated failure to configure IDE debugging, should fall back to process execution");
});

builder.Build().Run();

static async Task ExecuteCommandForAllResourcesAsync(IServiceProvider serviceProvider, string commandName, CancellationToken cancellationToken)
Expand Down
17 changes: 14 additions & 3 deletions playground/python/Python.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

using Aspire.Hosting.Python;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddPythonApp("script-only", "../script_only", "main.py");
builder.AddPythonApp("instrumented-script", "../instrumented_script", "main.py");
builder.AddPythonApp("instrumented-script", "../instrumented_script", "main.py")
.WithVSCodePythonDebuggerProperties(props =>
{
props.StopOnEntry = true;
});

builder.AddPythonModule("fastapi-app", "../module_only", "uvicorn")
.WithArgs("api:app", "--reload", "--host=0.0.0.0", "--port=8000")
Expand All @@ -13,7 +20,7 @@

// Run the same app on another port using uvicorn directly
builder.AddPythonExecutable("fastapi-uvicorn-app", "../module_only", "uvicorn")
.WithDebugging()
.WithVSCodeDebugging()
.WithArgs("api:app", "--reload", "--host=0.0.0.0", "--port=8001")
.WithHttpEndpoint(targetPort: 8001);

Expand All @@ -27,7 +34,11 @@
c.Args.Add("--port=8002");
})
.WithHttpEndpoint(targetPort: 8002)
.WithUv();
.WithUv()
.WithVSCodePythonDebuggerProperties(props =>
{
props.AutoReload = new PythonAutoReloadOptions { Enable = true };
});

// Uvicorn app using the AddUvicornApp method
builder.AddUvicornApp("uvicorn-app", "../uvicorn_app", "app:app")
Expand Down
Loading
Loading