Skip to content
Open
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
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@
"preLaunchTask": "tasks: watch extension",
"cwd": "${workspaceFolder}/extension"
},
{
"name": "Run Extension (Go debugging playground)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/extension",
"${workspaceFolder}/playground/GoDebugging"
],
"outFiles": [
"${workspaceFolder}/extension/dist/**/*.js"
],
"preLaunchTask": "tasks: watch extension",
"cwd": "${workspaceFolder}/extension"
},
{
"name": "Run Extension (cli stop on entry)",
"type": "extensionHost",
Expand Down
2 changes: 2 additions & 0 deletions extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
"aspire-vscode.strings.yesLabel": "Yes",
"aspire-vscode.strings.noLabel": "No",
"aspire-vscode.strings.invalidLaunchConfiguration": "Invalid launch configuration for {0}.",
"aspire-vscode.strings.goDisplayName": "Go: {0}",
"aspire-vscode.strings.goLabel": "Go",
"aspire-vscode.strings.noAppHostInWorkspace": "No AppHost found in the Aspire settings file.",
"aspire-vscode.strings.dashboard": "Dashboard",
"aspire-vscode.strings.codespaces": "Codespaces",
Expand Down
11 changes: 11 additions & 0 deletions extension/src/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type Capability =
| '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
| 'go' // Support for running Go projects
| 'golang.go' // Older AppHost versions used this extension identifier instead of go
| 'node' // Support for running Node.js projects
| 'browser' // Support for browser debugging (built-in to VS Code via js-debug)
| 'azure-functions'; // Support for running Azure Functions projects
Expand All @@ -36,6 +38,10 @@ export function isPythonInstalled() {
return isExtensionInstalled("ms-python.python");
}

export function isGoInstalled() {
return isExtensionInstalled("golang.go");
}

export function isAzureFunctionsExtensionInstalled() {
return isExtensionInstalled("ms-azuretools.vscode-azurefunctions");
}
Expand Down Expand Up @@ -69,6 +75,11 @@ export function getSupportedCapabilities(): Capabilities {
capabilities.push("ms-python.python");
}

if (isGoInstalled()) {
capabilities.push("go");
capabilities.push("golang.go");
}

if (isNodeInstalled()) {
capabilities.push("node");
capabilities.push("browser");
Expand Down
11 changes: 11 additions & 0 deletions extension/src/dcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ export function isPythonLaunchConfiguration(obj: any): obj is PythonLaunchConfig
return obj && obj.type === 'python';
}

export interface GoLaunchConfiguration extends ExecutableLaunchConfiguration {
type: "go";
program?: string;
working_directory?: string;
build_flags?: string;
}

export function isGoLaunchConfiguration(obj: any): obj is GoLaunchConfiguration {
return obj && obj.type === 'go';
}

export interface NodeLaunchConfiguration extends ExecutableLaunchConfiguration {
type: "node"; // Provided by VS Code's built-in js-debug, no extension needed
script_path?: string;
Expand Down
8 changes: 6 additions & 2 deletions extension/src/debugger/debuggerExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { debugProject, runProject } from "../loc/strings";
import { mergeEnvs } from "../utils/environment";
import { extensionLogOutputChannel } from "../utils/logging";
import { projectDebuggerExtension } from "./languages/dotnet";
import { isAzureFunctionsExtensionInstalled, isCsharpInstalled, isPythonInstalled } from '../capabilities';
import { isAzureFunctionsExtensionInstalled, isCsharpInstalled, isGoInstalled, 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 { isDirectory } from "../utils/io";

// Represents a resource-specific debugger extension for when the default session configuration is not sufficient to launch the resource.
Expand Down Expand Up @@ -80,9 +81,12 @@ export function getResourceDebuggerExtensions(): ResourceDebuggerExtension[] {
extensions.push(pythonDebuggerExtension);
}

if (isGoInstalled()) {
extensions.push(goDebuggerExtension);
}

extensions.push(nodeDebuggerExtension);
extensions.push(browserDebuggerExtension);

return extensions;
}

56 changes: 56 additions & 0 deletions extension/src/debugger/languages/go.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as vscode from 'vscode';
import { AspireResourceExtendedDebugConfiguration, ExecutableLaunchConfiguration, isGoLaunchConfiguration } from "../../dcp/types";
import { goDisplayName, goLabel, invalidLaunchConfiguration } from "../../loc/strings";
import { extensionLogOutputChannel } from "../../utils/logging";
import { ResourceDebuggerExtension } from "../debuggerExtensions";

function getProjectFile(launchConfig: ExecutableLaunchConfiguration): string {
if (isGoLaunchConfiguration(launchConfig)) {
return launchConfig.program || launchConfig.working_directory || '';
}

throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));
}

export const goDebuggerExtension: ResourceDebuggerExtension = {
resourceType: 'go',
debugAdapter: 'go',
extensionId: 'golang.go',
getDisplayName: (launchConfiguration: ExecutableLaunchConfiguration) => {
if (isGoLaunchConfiguration(launchConfiguration)) {
const displayPath = launchConfiguration.program || launchConfiguration.working_directory || '';
return displayPath ? goDisplayName(vscode.workspace.asRelativePath(displayPath)) : goLabel;
}

return goLabel;
},
getSupportedFileTypes: () => ['.go'],
getProjectFile: (launchConfig) => getProjectFile(launchConfig),
createDebugSessionConfigurationCallback: async (launchConfig, args, _env, launchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration): Promise<void> => {
if (!isGoLaunchConfiguration(launchConfig)) {
extensionLogOutputChannel.info(`The resource type was not go for ${JSON.stringify(launchConfig)}`);
throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));
}

debugConfiguration.type = 'go';
debugConfiguration.request = 'launch';
debugConfiguration.mode = 'debug';
debugConfiguration.debugAdapter = 'dlv-dap';
debugConfiguration.noDebug = !launchOptions.debug;

const program = launchConfig.program || launchConfig.working_directory;
if (program) {
debugConfiguration.program = program;
}

if (launchConfig.working_directory) {
debugConfiguration.cwd = launchConfig.working_directory;
}

if (launchConfig.build_flags) {
debugConfiguration.buildFlags = launchConfig.build_flags;
}

debugConfiguration.args = args ?? [];
}
};
2 changes: 2 additions & 0 deletions extension/src/loc/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export const errorGettingConfigInfo = (error: any) => vscode.l10n.t('Error getti
export const invalidLaunchConfiguration = (projectPath: string) => vscode.l10n.t('Invalid launch configuration for {0}.', projectPath);
export const browserDisplayName = (url: string) => vscode.l10n.t('Browser: {0}', url);
export const browserLabel = vscode.l10n.t('Browser');
export const goDisplayName = (program: string) => vscode.l10n.t('Go: {0}', program);
export const goLabel = vscode.l10n.t('Go');
export const dontShowAgainLabel = vscode.l10n.t("Don't Show Again");
export const doYouWantToSetDefaultApphost = (appHost: string) => vscode.l10n.t('Do you want to set {0} as the default AppHost for this workspace?', appHost);
export const doYouWantToSelectDefaultApphost = vscode.l10n.t('Do you want to select the default AppHost for this workspace?');
Expand Down
100 changes: 100 additions & 0 deletions extension/src/test/goDebugger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import { getSupportedCapabilities } from '../capabilities';
import { AspireDebugSession } from '../debugger/AspireDebugSession';
import { getResourceDebuggerExtensions } from '../debugger/debuggerExtensions';
import { goDebuggerExtension } from '../debugger/languages/go';
import { AspireResourceExtendedDebugConfiguration, GoLaunchConfiguration } from '../dcp/types';

suite('Go Debugger Extension Tests', () => {
const fakeAspireDebugSession = {} as AspireDebugSession;

teardown(() => sinon.restore());

test('advertises Go support when the Go extension is installed', () => {
sinon.stub(vscode.extensions, 'getExtension').callsFake((extensionId: string) => {
return extensionId === 'golang.go' ? { id: extensionId } as vscode.Extension<unknown> : undefined;
});

const capabilities = getSupportedCapabilities();
assert.ok(capabilities.includes('go'));
assert.ok(capabilities.includes('golang.go'));
assert.ok(getResourceDebuggerExtensions().some(extension => extension.resourceType === 'go'));
});

test('configures VS Code Go debugger with dlv-dap', async () => {
const launchConfig: GoLaunchConfiguration = {
type: 'go',
program: '/workspace/api/cmd/server',
working_directory: '/workspace/api',
build_flags: "-tags='integration' -gcflags='all=-N -l'"
};
const debugConfig = createDebugConfig();

await goDebuggerExtension.createDebugSessionConfigurationCallback!(
launchConfig,
['--listen', ':8080'],
[],
{ debug: true, runId: '1', debugSessionId: '1', isApphost: false, debugSession: fakeAspireDebugSession },
debugConfig);

assert.strictEqual(debugConfig.type, 'go');
assert.strictEqual(debugConfig.request, 'launch');
assert.strictEqual(debugConfig.mode, 'debug');
assert.strictEqual(debugConfig.debugAdapter, 'dlv-dap');
assert.strictEqual(debugConfig.program, '/workspace/api/cmd/server');
assert.strictEqual(debugConfig.cwd, '/workspace/api');
assert.strictEqual(debugConfig.buildFlags, "-tags='integration' -gcflags='all=-N -l'");
assert.deepStrictEqual(debugConfig.args, ['--listen', ':8080']);
assert.strictEqual(debugConfig.noDebug, false);
});

test('sets noDebug when launch option disables debugging', async () => {
const launchConfig: GoLaunchConfiguration = {
type: 'go',
program: '/workspace/api',
working_directory: '/workspace/api'
};
const debugConfig = createDebugConfig();

await goDebuggerExtension.createDebugSessionConfigurationCallback!(
launchConfig,
[],
[],
{ debug: false, runId: '1', debugSessionId: '1', isApphost: false, debugSession: fakeAspireDebugSession },
debugConfig);

assert.strictEqual(debugConfig.noDebug, true);
});

test('uses working directory as program when program is absent', async () => {
const launchConfig: GoLaunchConfiguration = {
type: 'go',
working_directory: '/workspace/api'
};
const debugConfig = createDebugConfig();

await goDebuggerExtension.createDebugSessionConfigurationCallback!(
launchConfig,
[],
[],
{ debug: true, runId: '1', debugSessionId: '1', isApphost: false, debugSession: fakeAspireDebugSession },
debugConfig);

assert.strictEqual(debugConfig.program, '/workspace/api');
assert.strictEqual(debugConfig.cwd, '/workspace/api');
});
});

function createDebugConfig(): AspireResourceExtendedDebugConfiguration {
return {
runId: '1',
debugSessionId: '1',
type: 'go',
name: 'Go',
request: 'launch',
program: '/workspace/api',
args: []
};
}
6 changes: 6 additions & 0 deletions playground/GoDebugging/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"microsoft-aspire.aspire-vscode",
"golang.go"
]
}
11 changes: 11 additions & 0 deletions playground/GoDebugging/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Go Debugging AppHost",
"type": "aspire",
"request": "launch",
"program": "${workspaceFolder}/GoDebugging.AppHost/GoDebugging.AppHost.csproj"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>5f2f9a51-df01-40a9-857c-927070ccdf6b</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
<AspireProjectOrPackageReference Include="Aspire.Hosting.Go" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions playground/GoDebugging/GoDebugging.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma warning disable ASPIREGO001

var builder = DistributedApplication.CreateBuilder(args);

builder.AddGoApp("api", "../api", buildTags: ["playground"])
.WithAppArgs("--message", "hello-from-aspire")
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"https": {
"commandName": "Project",
"applicationUrl": "https://localhost:17110;http://localhost:15110",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21110",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22110"
}
}
}
}
11 changes: 11 additions & 0 deletions playground/GoDebugging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Go Debugging Playground

This playground is for manually testing Aspire's VS Code Go debugger integration.

1. Open the Aspire repo in VS Code.
2. Start the `Run Extension (Go debugging playground)` launch configuration.
3. In the Extension Development Host, install the recommended Go extension if prompted.
4. Start the `Debug Go Debugging AppHost` launch configuration.
5. Set a breakpoint in `api/main.go`.

The AppHost references the local `Aspire.Hosting.Go` project so it exercises the current repo changes. The Go API uses the `playground` build tag and receives `--message hello-from-aspire` as app args, which verifies that the Aspire launch configuration passes `buildFlags` to VS Code and strips Go tool arguments from the debugged program args.
3 changes: 3 additions & 0 deletions playground/GoDebugging/api/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module goapphost/api

go 1.26
Loading
Loading