Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
28 changes: 27 additions & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"orchestrator",
"apphost"
],
"activationEvents": [],
"activationEvents": [
"workspaceContains:**/*AppHost*"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
Expand Down Expand Up @@ -57,6 +59,30 @@
"title": "%command.deploy%",
"category": "Aspire"
}
],
"debuggers": [
{
"type": "aspire",
"label": "Aspire Debug",
"configurationAttributes": {
"launch": {
"properties": {
"project": {
"type": "string",
"description": "Path to the Aspire project"
}
}
}
},
"initialConfigurations": [
{
"type": "aspire",
"request": "launch",
"name": "Aspire: Launch",
"project": "${workspaceFolder}"
}
]
}
]
},
"repository": {
Expand Down
3 changes: 2 additions & 1 deletion extension/package.nls.cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.es.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.it.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
3 changes: 2 additions & 1 deletion extension/package.nls.zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@
"aspire-vscode.strings.aspireTerminalName": "Aspire Terminal",
"aspire-vscode.strings.rpcServerError": "RPC Server error: {0}",
"aspire-vscode.strings.aspireOutputChannelName": "Aspire Extension",
"aspire-vscode.strings.fieldRequired": "This field is required"
"aspire-vscode.strings.fieldRequired": "This field is required",
"aspire-vscode.strings.attachToAppHost": "Attach to AppHost"
}
30 changes: 30 additions & 0 deletions extension/src/debugger/appHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as vscode from 'vscode';
import { startAndGetDebugSession } from './common';
import { extensionLogOutputChannel } from '../utils/logging';
import { attachToAppHost as attachToAppHostString } from '../loc/strings';

let appHostDebugSession: vscode.DebugSession | undefined = undefined;

export async function attachToAppHost(pid: number, sourceRoot?: string): Promise<void> {
if (appHostDebugSession) {
extensionLogOutputChannel.info(`Stopping existing AppHost debug session.`);
vscode.debug.stopDebugging(appHostDebugSession);
appHostDebugSession = undefined;
extensionLogOutputChannel.info(`Stopped existing AppHost debug session.`);
}

extensionLogOutputChannel.info(`Attaching to AppHost with PID: ${pid}`);

const config: vscode.DebugConfiguration = {
type: 'coreclr',
request: 'attach',
name: attachToAppHostString,
processId: pid.toString(),
justMyCode: false,
// Provide source mapping if specified
...(sourceRoot ? { sourceSearchPaths: [sourceRoot] } : {}),
appHost: true
};

appHostDebugSession = await startAndGetDebugSession(config);
}
66 changes: 66 additions & 0 deletions extension/src/debugger/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as vscode from 'vscode';
import { extensionLogOutputChannel } from '../utils/logging';
import { currentAspireDebugSession } from './debugAdapter';

// Track child debug sessions for Aspire
const aspireChildDebugSessions: Set<vscode.DebugSession> = new Set();

// Listen for new debug sessions and track children of Aspire session
vscode.debug.onDidStartDebugSession((session) => {
if (currentAspireDebugSession?.session && session.parentSession && session.parentSession.id === currentAspireDebugSession.session.id) {
aspireChildDebugSessions.add(session);
}
});

// Listen for Aspire debug session termination and terminate all its children
vscode.debug.onDidTerminateDebugSession(async (session) => {
// If Aspire parent session ends, terminate all its children
if (currentAspireDebugSession?.session && session.id === currentAspireDebugSession.session.id) {
extensionLogOutputChannel.info(`Aspire debug session terminated. Terminating ${aspireChildDebugSessions.size} child session(s).`);
for (const child of aspireChildDebugSessions) {
try {
await vscode.debug.stopDebugging(child);
} catch (e) {
extensionLogOutputChannel.error(`Failed to terminate child debug session ${child.name}: ${e}`);
}
}
aspireChildDebugSessions.clear();
}
// If an AppHost child session ends, also stop the Aspire parent session
else if (aspireChildDebugSessions.has(session)) {
aspireChildDebugSessions.delete(session);
// Check if this session is an AppHost by config
if (session.configuration && session.configuration.appHost && currentAspireDebugSession?.session) {
extensionLogOutputChannel.info(`AppHost child session terminated. Stopping Aspire parent session.`);
try {
await vscode.debug.stopDebugging(currentAspireDebugSession.session);
} catch (e) {
extensionLogOutputChannel.error(`Failed to terminate Aspire parent session: ${e}`);
}
}
}
});

export async function startAndGetDebugSession(debugConfig: vscode.DebugConfiguration): Promise<vscode.DebugSession | undefined> {
return new Promise(async (resolve) => {
const disposable = vscode.debug.onDidStartDebugSession(session => {
if (session.name === debugConfig.name) {
extensionLogOutputChannel.info(`Debug session started: ${session.name}`);
disposable.dispose();
resolve(session);
}
});

extensionLogOutputChannel.info(`Starting debug session with configuration: ${JSON.stringify(debugConfig)}`);
const started = await vscode.debug.startDebugging(undefined, debugConfig, currentAspireDebugSession?.session);
if (!started) {
disposable.dispose();
resolve(undefined);
}

setTimeout(() => {
disposable.dispose();
resolve(undefined);
}, 10000);
});
}
5 changes: 5 additions & 0 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { publishCommand } from './commands/publish';
import { errorMessage } from './loc/strings';
import { extensionLogOutputChannel } from './utils/logging';
import { initializeTelemetry, sendTelemetryEvent } from './utils/telemetry';
import { AspireDebugAdapterDescriptorFactory } from './debugger/debugAdapter';

export let rpcServerInfo: RpcServerInformation | undefined;

Expand All @@ -33,6 +34,10 @@ export async function activate(context: vscode.ExtensionContext) {

context.subscriptions.push(cliRunCommand, cliAddCommand, cliNewCommand, cliConfigCommand, cliDeployCommand, cliPublishCommand);

context.subscriptions.push(
vscode.debug.registerDebugAdapterDescriptorFactory('aspire', new AspireDebugAdapterDescriptorFactory())
);

// Return exported API for tests or other extensions
return {
rpcServerInfo: rpcServerInfo,
Expand Down
1 change: 1 addition & 0 deletions extension/src/loc/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export const requiredCapability = (capability: string) => localize('aspire-vscod
export const aspireTerminalName = localize('aspire-vscode.strings.aspireTerminalName', 'Aspire Terminal');
export const aspireOutputChannelName = localize('aspire-vscode.strings.aspireOutputChannelName', 'Aspire Extension');
export const fieldRequired = localize('aspire-vscode.strings.fieldRequired', 'This field is required');
export const attachToAppHost = localize('aspire-vscode.strings.attachToAppHost', 'Attach to AppHost');
18 changes: 13 additions & 5 deletions extension/src/server/interactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { yesLabel, noLabel, directLink, codespacesLink, openAspireDashboard, fai
import { ICliRpcClient } from './rpcClient';
import { formatText } from '../utils/strings';
import { extensionLogOutputChannel } from '../utils/logging';
import { attachToAppHost } from '../debugger/appHost';
import { getAspireTerminal } from '../utils/terminal';

type CSLogLevel = 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Critical';

Expand All @@ -24,6 +26,7 @@ export interface IInteractionService {
displayCancellationMessage: (message: string) => void;
openProject: (projectPath: string) => void;
logMessage: (logLevel: CSLogLevel, message: string) => void;
requestAppHostAttach(pid: number): Promise<void>;
}

type DashboardUrls = {
Expand All @@ -32,8 +35,8 @@ type DashboardUrls = {
};

type ConsoleLine = {
stream: 'stdout' | 'stderr';
line: string;
Stream: 'stdout' | 'stderr';
Line: string;
};

export class InteractionService implements IInteractionService {
Expand Down Expand Up @@ -217,9 +220,9 @@ export class InteractionService implements IInteractionService {
}

displayLines(lines: ConsoleLine[]) {
const displayText = lines.map(line => line.line).join('\n');
vscode.window.showInformationMessage(formatText(displayText));
lines.forEach(line => extensionLogOutputChannel.info(formatText(line.line)));
const displayText = lines.map(line => line.Line).join('\n');
getAspireTerminal().sendText(formatText(displayText), false);
lines.forEach(line => extensionLogOutputChannel.info(formatText(line.Line)));
}

displayCancellationMessage(message: string) {
Expand Down Expand Up @@ -256,6 +259,10 @@ export class InteractionService implements IInteractionService {
}
}

requestAppHostAttach(pid: number): Promise<void> {
return attachToAppHost(pid);
}

clearStatusBar() {
if (this._statusBarItem) {
this._statusBarItem.hide();
Expand All @@ -281,4 +288,5 @@ export function addInteractionServiceEndpoints(connection: MessageConnection, in
connection.onRequest("displayCancellationMessage", withAuthentication(interactionService.displayCancellationMessage.bind(interactionService)));
connection.onRequest("openProject", withAuthentication(interactionService.openProject.bind(interactionService)));
connection.onRequest("logMessage", withAuthentication(interactionService.logMessage.bind(interactionService)));
connection.onRequest("requestAppHostAttach", withAuthentication(interactionService.requestAppHostAttach.bind(interactionService)));
}
5 changes: 5 additions & 0 deletions extension/src/server/rpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { logAsyncOperation } from '../utils/logging';
export interface ICliRpcClient {
getCliVersion(): Promise<string>;
validatePromptInputString(input: string): Promise<ValidationResult | null>;
stopCli: () => Promise<void>;
}

export type ValidationResult = {
Expand Down Expand Up @@ -42,4 +43,8 @@ export class RpcClient implements ICliRpcClient {
}
);
}

async stopCli(): Promise<void> {
await this._messageConnection.sendRequest<void>('stopCli', this._token);
}
}
Loading