Skip to content
Merged
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
3 changes: 3 additions & 0 deletions extension/loc/xlf/aspire-vscode.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"aspire-vscode.strings.lookingForDevkitBuildTask": "C# Dev Kit is installed, looking for C# Dev Kit build task...",
"aspire-vscode.strings.csharpDevKitNotInstalled": "C# Dev Kit is not installed, building using dotnet CLI...",
"aspire-vscode.strings.cliNotAvailable": "Aspire CLI is not available on PATH. Please install it and restart VS Code.",
"aspire-vscode.strings.cliFoundAtDefaultPath": "Aspire CLI found at {0}. The extension will use this path.",
"aspire-vscode.strings.openCliInstallInstructions": "See CLI installation instructions",
"aspire-vscode.strings.dismissLabel": "Dismiss"
}
2 changes: 1 addition & 1 deletion extension/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';

export async function addCommand(terminalProvider: AspireTerminalProvider) {
terminalProvider.sendAspireCommandToAspireTerminal('add');
await terminalProvider.sendAspireCommandToAspireTerminal('add');
}
2 changes: 1 addition & 1 deletion extension/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';

export async function deployCommand(terminalProvider: AspireTerminalProvider) {
terminalProvider.sendAspireCommandToAspireTerminal('deploy');
await terminalProvider.sendAspireCommandToAspireTerminal('deploy');
}
2 changes: 1 addition & 1 deletion extension/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AspireTerminalProvider } from "../utils/AspireTerminalProvider";

export async function initCommand(terminalProvider: AspireTerminalProvider) {
terminalProvider.sendAspireCommandToAspireTerminal('init');
await terminalProvider.sendAspireCommandToAspireTerminal('init');
};
2 changes: 1 addition & 1 deletion extension/src/commands/new.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AspireTerminalProvider } from "../utils/AspireTerminalProvider";

export async function newCommand(terminalProvider: AspireTerminalProvider) {
terminalProvider.sendAspireCommandToAspireTerminal('new');
await terminalProvider.sendAspireCommandToAspireTerminal('new');
};
2 changes: 1 addition & 1 deletion extension/src/commands/publish.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';

export async function publishCommand(terminalProvider: AspireTerminalProvider) {
terminalProvider.sendAspireCommandToAspireTerminal('publish');
await terminalProvider.sendAspireCommandToAspireTerminal('publish');
}
2 changes: 1 addition & 1 deletion extension/src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';

export async function updateCommand(terminalProvider: AspireTerminalProvider) {
terminalProvider.sendAspireCommandToAspireTerminal('update');
await terminalProvider.sendAspireCommandToAspireTerminal('update');
}
12 changes: 2 additions & 10 deletions extension/src/debugger/AspireDebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import * as vscode from 'vscode';
import { defaultConfigurationName } from '../loc/strings';
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';
import { checkCliAvailableOrRedirect } from '../utils/workspace';

export class AspireDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
private _terminalProvider: AspireTerminalProvider;

constructor(terminalProvider: AspireTerminalProvider) {
this._terminalProvider = terminalProvider;
}

async provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): Promise<vscode.DebugConfiguration[]> {
if (folder === undefined) {
return [];
Expand All @@ -28,9 +21,8 @@ export class AspireDebugConfigurationProvider implements vscode.DebugConfigurati

async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise<vscode.DebugConfiguration | null | undefined> {
// Check if CLI is available before starting debug session
const cliPath = this._terminalProvider.getAspireCliExecutablePath();
const isCliAvailable = await checkCliAvailableOrRedirect(cliPath);
if (!isCliAvailable) {
const result = await checkCliAvailableOrRedirect();
if (!result.available) {
return undefined; // Cancel the debug session
}

Expand Down
30 changes: 22 additions & 8 deletions extension/src/debugger/AspireDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class AspireDebugSession implements vscode.DebugAdapter {
private _trackedDebugAdapters: string[] = [];
private _rpcClient?: ICliRpcClient;
private readonly _disposables: vscode.Disposable[] = [];
private _disposed = false;
private _userInitiatedStop = false;

public readonly onDidSendMessage = this._onDidSendMessage.event;
public readonly debugSessionId: string;
Expand Down Expand Up @@ -93,18 +95,19 @@ export class AspireDebugSession implements vscode.DebugAdapter {
if (isDirectory(appHostPath)) {
this.sendMessageWithEmoji("📁", launchingWithDirectory(appHostPath));

this.spawnRunCommand(args, appHostPath, noDebug);
void this.spawnRunCommand(args, appHostPath, noDebug);
}
Comment on lines 95 to 99
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

handleMessage() starts spawnRunCommand() with void and no error handling. Since spawnRunCommand is now async (awaiting CLI path resolution), a rejection (e.g., config update failure during resolveCliPath) would become an unhandled promise rejection. Consider wrapping spawnRunCommand in a try/catch internally, or attaching a .catch(...) when calling it to log and surface a user-friendly error.

Copilot uses AI. Check for mistakes.
else {
this.sendMessageWithEmoji("📂", launchingWithAppHost(appHostPath));

const workspaceFolder = path.dirname(appHostPath);
args.push('--project', appHostPath);
this.spawnRunCommand(args, workspaceFolder, noDebug);
void this.spawnRunCommand(args, workspaceFolder, noDebug);
}
Comment on lines 101 to 106
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Same as above: spawnRunCommand() is invoked with void and no .catch(...) in the file-path case, which can surface as an unhandled promise rejection if CLI path resolution or spawn setup fails. Consider handling the returned promise (or making spawnRunCommand non-throwing).

Copilot uses AI. Check for mistakes.
}
else if (message.command === 'disconnect' || message.command === 'terminate') {
this.sendMessageWithEmoji("🔌", disconnectingFromSession);
this._userInitiatedStop = true;
this.dispose();

this.sendEvent({
Expand Down Expand Up @@ -133,7 +136,7 @@ export class AspireDebugSession implements vscode.DebugAdapter {
}
}

spawnRunCommand(args: string[], workingDirectory: string | undefined, noDebug: boolean) {
async spawnRunCommand(args: string[], workingDirectory: string | undefined, noDebug: boolean) {
const disposable = this._rpcServer.onNewConnection((client: ICliRpcClient) => {
if (client.debugSessionId === this.debugSessionId) {
this._rpcClient = client;
Expand All @@ -143,7 +146,7 @@ export class AspireDebugSession implements vscode.DebugAdapter {

spawnCliProcess(
this._terminalProvider,
this._terminalProvider.getAspireCliExecutablePath(),
await this._terminalProvider.getAspireCliExecutablePath(),
args,
{
stdoutCallback: (data) => {
Expand Down Expand Up @@ -173,7 +176,9 @@ export class AspireDebugSession implements vscode.DebugAdapter {

this._disposables.push({
dispose: () => {
this._rpcClient?.stopCli();
this._rpcClient?.stopCli().catch((err) => {
extensionLogOutputChannel.info(`stopCli failed (connection may already be closed): ${err}`);
});
extensionLogOutputChannel.info(`Requested Aspire CLI exit with args: ${args.join(' ')}`);
}
});
Expand Down Expand Up @@ -219,9 +224,15 @@ export class AspireDebugSession implements vscode.DebugAdapter {

const disposable = vscode.debug.onDidTerminateDebugSession(async session => {
if (this._appHostDebugSession && session.id === this._appHostDebugSession.id) {
// We should also dispose of the parent Aspire debug session whenever the AppHost stops.
const shouldRestart = !this._userInitiatedStop;
const config = this.configuration;
// Always dispose the current Aspire debug session when the AppHost stops.
this.dispose();
disposable.dispose();

if (shouldRestart) {
extensionLogOutputChannel.info('AppHost terminated unexpectedly, restarting Aspire debug session');
await vscode.debug.startDebugging(undefined, config);
}
}
});

Expand Down Expand Up @@ -281,11 +292,14 @@ export class AspireDebugSession implements vscode.DebugAdapter {
}

dispose(): void {
if (this._disposed) {
return;
}
this._disposed = true;
extensionLogOutputChannel.info('Stopping the Aspire debug session');
vscode.debug.stopDebugging(this._session);
this._disposables.forEach(disposable => disposable.dispose());
this._trackedDebugAdapters = [];
this._rpcClient?.stopCli();
}

private sendResponse(request: any, body: any = {}) {
Expand Down
7 changes: 3 additions & 4 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(cliAddCommandRegistration, cliNewCommandRegistration, cliInitCommandRegistration, cliDeployCommandRegistration, cliPublishCommandRegistration, openTerminalCommandRegistration, configureLaunchJsonCommandRegistration);
context.subscriptions.push(cliUpdateCommandRegistration, settingsCommandRegistration, openLocalSettingsCommandRegistration, openGlobalSettingsCommandRegistration, runAppHostCommandRegistration, debugAppHostCommandRegistration);

const debugConfigProvider = new AspireDebugConfigurationProvider(terminalProvider);
const debugConfigProvider = new AspireDebugConfigurationProvider();
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider('aspire', debugConfigProvider, vscode.DebugConfigurationProviderTriggerKind.Dynamic)
);
Expand Down Expand Up @@ -114,9 +114,8 @@ async function tryExecuteCommand(commandName: string, terminalProvider: AspireTe
const cliCheckExcludedCommands: string[] = ["aspire-vscode.settings", "aspire-vscode.configureLaunchJson"];

if (!cliCheckExcludedCommands.includes(commandName)) {
const cliPath = terminalProvider.getAspireCliExecutablePath();
const isCliAvailable = await checkCliAvailableOrRedirect(cliPath);
if (!isCliAvailable) {
const result = await checkCliAvailableOrRedirect();
if (!result.available) {
return;
}
}
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 @@ -71,3 +71,4 @@ export const csharpDevKitNotInstalled = vscode.l10n.t('C# Dev Kit is not install
export const dismissLabel = vscode.l10n.t('Dismiss');
export const openCliInstallInstructions = vscode.l10n.t('See CLI installation instructions');
export const cliNotAvailable = vscode.l10n.t('Aspire CLI is not available on PATH. Please install it and restart VS Code.');
export const cliFoundAtDefaultPath = (path: string) => vscode.l10n.t('Aspire CLI found at {0}. The extension will use this path.', path);
76 changes: 20 additions & 56 deletions extension/src/test/aspireTerminalProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,58 @@ import * as assert from 'assert';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';
import * as cliPathModule from '../utils/cliPath';

suite('AspireTerminalProvider tests', () => {
let terminalProvider: AspireTerminalProvider;
let configStub: sinon.SinonStub;
let resolveCliPathStub: sinon.SinonStub;
let subscriptions: vscode.Disposable[];

setup(() => {
subscriptions = [];
terminalProvider = new AspireTerminalProvider(subscriptions);
configStub = sinon.stub(vscode.workspace, 'getConfiguration');
resolveCliPathStub = sinon.stub(cliPathModule, 'resolveCliPath');
});

teardown(() => {
configStub.restore();
resolveCliPathStub.restore();
subscriptions.forEach(s => s.dispose());
});

suite('getAspireCliExecutablePath', () => {
test('returns "aspire" when no custom path is configured', () => {
configStub.returns({
get: sinon.stub().returns('')
});
test('returns "aspire" when CLI is on PATH', async () => {
resolveCliPathStub.resolves({ cliPath: 'aspire', available: true, source: 'path' });

const result = terminalProvider.getAspireCliExecutablePath();
const result = await terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, 'aspire');
});

test('returns custom path when configured', () => {
configStub.returns({
get: sinon.stub().returns('/usr/local/bin/aspire')
});
test('returns resolved path when CLI found at default install location', async () => {
resolveCliPathStub.resolves({ cliPath: '/home/user/.aspire/bin/aspire', available: true, source: 'default-install' });

const result = terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, '/usr/local/bin/aspire');
const result = await terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, '/home/user/.aspire/bin/aspire');
});

test('returns custom path with spaces', () => {
configStub.returns({
get: sinon.stub().returns('/my path/with spaces/aspire')
});

const result = terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, '/my path/with spaces/aspire');
});
test('returns configured custom path', async () => {
resolveCliPathStub.resolves({ cliPath: '/usr/local/bin/aspire', available: true, source: 'configured' });

test('trims whitespace from configured path', () => {
configStub.returns({
get: sinon.stub().returns(' /usr/local/bin/aspire ')
});

const result = terminalProvider.getAspireCliExecutablePath();
const result = await terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, '/usr/local/bin/aspire');
});

test('returns "aspire" when configured path is only whitespace', () => {
configStub.returns({
get: sinon.stub().returns(' ')
});
test('returns "aspire" when CLI is not found', async () => {
resolveCliPathStub.resolves({ cliPath: 'aspire', available: false, source: 'not-found' });

const result = terminalProvider.getAspireCliExecutablePath();
const result = await terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, 'aspire');
});

test('handles Windows-style paths', () => {
configStub.returns({
get: sinon.stub().returns('C:\\Program Files\\Aspire\\aspire.exe')
});
test('handles Windows-style paths', async () => {
resolveCliPathStub.resolves({ cliPath: 'C:\\Program Files\\Aspire\\aspire.exe', available: true, source: 'configured' });

const result = terminalProvider.getAspireCliExecutablePath();
const result = await terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, 'C:\\Program Files\\Aspire\\aspire.exe');
});

test('handles Windows-style paths without spaces', () => {
configStub.returns({
get: sinon.stub().returns('C:\\aspire\\aspire.exe')
});

const result = terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, 'C:\\aspire\\aspire.exe');
});

test('handles paths with special characters', () => {
configStub.returns({
get: sinon.stub().returns('/path/with$dollar/aspire')
});

const result = terminalProvider.getAspireCliExecutablePath();
assert.strictEqual(result, '/path/with$dollar/aspire');
});
});
});
Loading
Loading