Skip to content

Commit 6a49433

Browse files
adamintAdam Ratzman
andauthored
Support debugging python modules, add flask, uvicorn debugging support, add DCP logging util (#12456)
* add additional launch configurations, dcp logs setting * add python modules to ide spec, config * add module example to AspireWithPython * add file * make cert serial number generation better * add AddFlaskApp, add example, get uvicorn debug working * add uvicorn example, fix flask_app * add hot reload in development mode for uvicorn * improve naming * remove test class * change 01 -> 00 * instead of AddFlaskApp do AddGunicornApp * disable vs code gunicorn debug support in prod * add WithDebugging to note a python resource is debuggable * fix error * remove gunicorn stuff, use correct path for Windows python executable * rename WithVSCodeDebugSupport -> WithDebugSupport * show errors when resource fails to start * fix bug where python args were being removed when run within extension but without python extension installed * add additional tests * remove ide spec change for now --------- Co-authored-by: Adam Ratzman <[email protected]>
1 parent e1b6454 commit 6a49433

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1742
-948
lines changed

.vscode/launch.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,39 @@
2929
"env": {
3030
"ASPIRE_CLI_STOP_ON_ENTRY": "true"
3131
},
32+
"cwd": "${workspaceFolder}/extension"
33+
},
34+
{
35+
"name": "Run Extension (apphost stop on entry)",
36+
"type": "extensionHost",
37+
"request": "launch",
38+
"args": [
39+
"--extensionDevelopmentPath=${workspaceFolder}/extension"
40+
],
41+
"outFiles": [
42+
"${workspaceFolder}/extension/dist/**/*.js"
43+
],
44+
"preLaunchTask": "npm: watch extension",
45+
"env": {
46+
"ASPIRE_APPHOST_STOP_ON_ENTRY": "true"
47+
},
48+
"cwd": "${workspaceFolder}/extension"
49+
},
50+
{
51+
"name": "Run Extension (apphost AND cli stop on entry)",
52+
"type": "extensionHost",
53+
"request": "launch",
54+
"args": [
55+
"--extensionDevelopmentPath=${workspaceFolder}/extension"
56+
],
57+
"outFiles": [
58+
"${workspaceFolder}/extension/dist/**/*.js"
59+
],
60+
"preLaunchTask": "npm: watch extension",
61+
"env": {
62+
"ASPIRE_CLI_STOP_ON_ENTRY": "true",
63+
"ASPIRE_APPHOST_STOP_ON_ENTRY": "true"
64+
},
3265
"cwd": "${workspaceFolder}/extension"
3366
}
3467
]

extension/loc/xlf/aspire-vscode.xlf

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extension/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@
199199
"default": false,
200200
"description": "%configuration.aspire.enableAspireCliDebugLogging%",
201201
"scope": "window"
202+
},
203+
"aspire.enableAspireDcpDebugLogging": {
204+
"type": "boolean",
205+
"default": false,
206+
"description": "%configuration.aspire.enableAspireDcpDebugLogging%",
207+
"scope": "window"
202208
}
203209
}
204210
}

extension/package.nls.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"configuration.aspire.enableSettingsFileCreationPromptOnStartup": "Enable apphost discovery on extension activation and prompt to setup .aspire/settings.json.appHostPath if it does not exist in the workspace.",
1919
"configuration.aspire.aspireCliExecutablePath": "The path to the Aspire CLI executable. If not set, the extension will attempt to use 'aspire' from the system PATH.",
2020
"configuration.aspire.enableAspireCliDebugLogging": "Enable console debug logging for Aspire CLI commands executed by the extension.",
21+
"configuration.aspire.enableAspireDcpDebugLogging": "Enable Developer Control Plane (DCP) debug logging for Aspire applications. Logs will be stored in the workspace's .aspire/dcp/logs-{debugSessionId} folder.",
2122
"command.runAppHost": "Run Aspire apphost",
2223
"command.debugAppHost": "Debug Aspire apphost",
2324
"aspire-vscode.strings.noCsprojFound": "No apphost found in the current workspace.",
@@ -76,5 +77,10 @@
7677
"aspire-vscode.strings.invalidLaunchConfiguration": "Invalid launch configuration for {0}.",
7778
"aspire-vscode.strings.noAppHostInWorkspace": "No apphost found in the Aspire settings file.",
7879
"aspire-vscode.strings.dashboard": "Dashboard",
79-
"aspire-vscode.strings.codespaces": "Codespaces"
80+
"aspire-vscode.strings.codespaces": "Codespaces",
81+
"aspire-vscode.strings.encounteredErrorStartingResource": "Encountered an error starting resource: {0}.",
82+
"aspire-vscode.strings.invalidOrMissingToken": "Invalid or missing token in Authorization header.",
83+
"aspire-vscode.strings.invalidTokenLength": "Invalid token length in Authorization header.",
84+
"aspire-vscode.strings.authorizationHeaderMustStartWithBearer": "Authorization header must start with 'Bearer '.",
85+
"aspire-vscode.strings.authorizationAndDcpHeadersRequired": "Authorization and Microsoft-Developer-DCP-Instance-ID headers are required."
8086
}

extension/src/dcp/AspireDcpServer.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import express, { Request, Response, NextFunction } from 'express';
22
import https from 'https';
33
import WebSocket, { WebSocketServer } from 'ws';
4+
import * as vscode from 'vscode';
45
import { createSelfSignedCertAsync, generateToken } from '../utils/security';
56
import { extensionLogOutputChannel } from '../utils/logging';
67
import { AspireResourceDebugSession, DcpServerConnectionInfo, ErrorDetails, ErrorResponse, ProcessRestartedNotification, RunSessionNotification, RunSessionPayload, ServiceLogsNotification, SessionMessageNotification, SessionTerminatedNotification } from './types';
78
import { AspireDebugSession } from '../debugger/AspireDebugSession';
89
import { createDebugSessionConfiguration, ResourceDebuggerExtension } from '../debugger/debuggerExtensions';
910
import { timingSafeEqual } from 'crypto';
1011
import { getRunSessionInfo, getSupportedCapabilities } from '../capabilities';
12+
import { authorizationAndDcpHeadersRequired, authorizationHeaderMustStartWithBearer, encounteredErrorStartingResource, invalidOrMissingToken, invalidTokenLength } from '../loc/strings';
1113

1214
export default class AspireDcpServer {
1315
private readonly app: express.Express;
@@ -48,26 +50,26 @@ export default class AspireDcpServer {
4850
const auth = req.header('Authorization');
4951
const dcpId = req.header('microsoft-developer-dcp-instance-id');
5052
if (!auth || !dcpId) {
51-
res.status(401).json({ error: { code: 'MissingHeaders', message: 'Authorization and Microsoft-Developer-DCP-Instance-ID headers are required.' } });
53+
respondWithError(res, 401, { error: { code: 'MissingHeaders', message: authorizationAndDcpHeadersRequired, details: [] } });
5254
return;
5355
}
5456

5557
if (auth.split('Bearer ').length !== 2) {
56-
res.status(401).json({ error: { code: 'InvalidAuthHeader', message: 'Authorization header must start with "Bearer "' } });
58+
respondWithError(res, 401, { error: { code: 'InvalidAuthHeader', message: authorizationHeaderMustStartWithBearer, details: [] } });
5759
return;
5860
}
5961

6062
const bearerTokenBuffer = Buffer.from(auth.split('Bearer ')[1]);
6163
const expectedTokenBuffer = Buffer.from(token);
6264

6365
if (bearerTokenBuffer.length !== expectedTokenBuffer.length) {
64-
res.status(401).json({ error: { code: 'InvalidToken', message: 'Invalid token length in Authorization header.' } });
66+
respondWithError(res, 401, { error: { code: 'InvalidToken', message: invalidTokenLength, details: [] } });
6567
return;
6668
}
6769

6870
// timingSafeEqual is used to verify that the tokens are equivalent in a way that mitigates timing attacks
6971
if (timingSafeEqual(bearerTokenBuffer, expectedTokenBuffer) === false) {
70-
res.status(401).json({ error: { code: 'InvalidToken', message: 'Invalid or missing token in Authorization header.' } });
72+
respondWithError(res, 401, { error: { code: 'InvalidToken', message: invalidOrMissingToken, details: [] } });
7173
return;
7274
}
7375

@@ -99,7 +101,7 @@ export default class AspireDcpServer {
99101

100102
extensionLogOutputChannel.error(`Error creating debug session ${runId}: ${error.message}`);
101103
const response: ErrorResponse = { error };
102-
res.status(400).json(response).end();
104+
respondWithError(res, 400, response);
103105
return;
104106
}
105107

@@ -115,7 +117,7 @@ export default class AspireDcpServer {
115117

116118
extensionLogOutputChannel.error(`Error creating debug session ${runId}: ${error.message}`);
117119
const response: ErrorResponse = { error };
118-
res.status(400).json(response).end();
120+
respondWithError(res, 400, response);
119121
return;
120122
}
121123

@@ -129,7 +131,7 @@ export default class AspireDcpServer {
129131

130132
extensionLogOutputChannel.error(`Error creating debug session ${runId}: ${error.message}`);
131133
const response: ErrorResponse = { error };
132-
res.status(500).json(response).end();
134+
respondWithError(res, 500, response);
133135
return;
134136
}
135137

@@ -145,7 +147,7 @@ export default class AspireDcpServer {
145147

146148
extensionLogOutputChannel.error(`Error creating debug session ${runId}: ${error.message}`);
147149
const response: ErrorResponse = { error };
148-
res.status(500).json(response).end();
150+
respondWithError(res, 500, response);
149151
return;
150152
}
151153

@@ -311,3 +313,8 @@ function getDcpIdPrefix(dcpId: string): string | null {
311313

312314
return null;
313315
}
316+
317+
function respondWithError(res: Response, statusCode: number, message: ErrorResponse): void {
318+
res.status(statusCode).json(message).end();
319+
vscode.window.showErrorMessage(encounteredErrorStartingResource(message.error.message));
320+
}

extension/src/dcp/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@ export function isProjectLaunchConfiguration(obj: any): obj is ProjectLaunchConf
3030

3131
export interface PythonLaunchConfiguration extends ExecutableLaunchConfiguration {
3232
type: "python";
33+
34+
// legacy fields
35+
project_path?: string;
3336
program_path?: string;
34-
project_path?: string; // leftover from 9.5 usage of project path
37+
38+
module?: string;
39+
interpreter_path?: string;
3540
}
3641

3742
export function isPythonLaunchConfiguration(obj: any): obj is PythonLaunchConfiguration {

extension/src/debugger/AspireDebugSession.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export class AspireDebugSession implements vscode.DebugAdapter {
8282
args.push('--cli-wait-for-debugger');
8383
}
8484

85+
if (process.env[EnvironmentVariables.ASPIRE_APPHOST_STOP_ON_ENTRY] === 'true') {
86+
args.push('--wait-for-debugger');
87+
}
88+
8589
if (isDirectory(appHostPath)) {
8690
this.sendMessageWithEmoji("📁", launchingWithDirectory(appHostPath));
8791

extension/src/debugger/debuggerExtensions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { extensionLogOutputChannel } from "../utils/logging";
66
import { projectDebuggerExtension } from "./languages/dotnet";
77
import { isCsharpInstalled, isPythonInstalled } from "../capabilities";
88
import { pythonDebuggerExtension } from "./languages/python";
9+
import { isDirectory } from "../utils/io";
910

1011
// Represents a resource-specific debugger extension for when the default session configuration is not sufficient to launch the resource.
1112
export interface ResourceDebuggerExtension {
1213
resourceType: string;
1314
debugAdapter: string;
1415
extensionId: string | null;
15-
displayName: string;
16+
getDisplayName: (launchConfig: ExecutableLaunchConfiguration) => string;
1617
getProjectFile: (launchConfig: ExecutableLaunchConfiguration) => string;
1718
getSupportedFileTypes: () => string[];
1819
createDebugSessionConfigurationCallback?: (launchConfig: ExecutableLaunchConfiguration, args: string[] | undefined, env: EnvVar[], launchOptions: LaunchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration) => Promise<void>;
@@ -24,15 +25,14 @@ export async function createDebugSessionConfiguration(debugSessionConfig: Aspire
2425
}
2526

2627
const projectPath = debuggerExtension.getProjectFile(launchConfig);
27-
const displayName = `${debuggerExtension.displayName ?? launchConfig.type}: ${path.basename(projectPath)}`;
2828

2929
const configuration: AspireResourceExtendedDebugConfiguration = {
3030
type: debuggerExtension.debugAdapter || launchConfig.type,
3131
request: 'launch',
32-
name: launchOptions.debug ? debugProject(displayName) : runProject(displayName),
32+
name: launchOptions.debug ? debugProject(debuggerExtension.getDisplayName(launchConfig)) : runProject(debuggerExtension.getDisplayName(launchConfig)),
3333
program: projectPath,
3434
args: args,
35-
cwd: path.dirname(projectPath),
35+
cwd: await isDirectory(projectPath) ? projectPath : path.dirname(projectPath),
3636
env: mergeEnvs(process.env, env),
3737
justMyCode: false,
3838
stopAtEntry: false,

extension/src/debugger/languages/dotnet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as path from 'path';
77
import * as readline from 'readline';
88
import * as os from 'os';
99
import { doesFileExist } from '../../utils/io';
10-
import { AspireResourceExtendedDebugConfiguration, isProjectLaunchConfiguration } from '../../dcp/types';
10+
import { AspireResourceExtendedDebugConfiguration, ExecutableLaunchConfiguration, isProjectLaunchConfiguration, ProjectLaunchConfiguration } from '../../dcp/types';
1111
import { ResourceDebuggerExtension } from '../debuggerExtensions';
1212
import {
1313
readLaunchSettings,
@@ -184,7 +184,7 @@ export function createProjectDebuggerExtension(dotNetService: IDotNetService): R
184184
resourceType: 'project',
185185
debugAdapter: 'coreclr',
186186
extensionId: 'ms-dotnettools.csharp',
187-
displayName: 'C#',
187+
getDisplayName: (launchConfig: ExecutableLaunchConfiguration) => `C#: ${path.basename((launchConfig as ProjectLaunchConfiguration).project_path)}`,
188188
getSupportedFileTypes: () => ['.cs', '.csproj'],
189189
getProjectFile: (launchConfig) => {
190190
if (isProjectLaunchConfiguration(launchConfig)) {
Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,44 @@
1-
import { isPythonLaunchConfiguration } from "../../dcp/types";
1+
import { AspireResourceExtendedDebugConfiguration, ExecutableLaunchConfiguration, isPythonLaunchConfiguration } from "../../dcp/types";
22
import { invalidLaunchConfiguration } from "../../loc/strings";
3+
import { extensionLogOutputChannel } from "../../utils/logging";
34
import { ResourceDebuggerExtension } from "../debuggerExtensions";
5+
import * as vscode from 'vscode';
6+
7+
function getProjectFile(launchConfig: ExecutableLaunchConfiguration): string {
8+
if (isPythonLaunchConfiguration(launchConfig)) {
9+
const programPath = launchConfig.program_path || launchConfig.project_path;
10+
if (programPath) {
11+
return programPath;
12+
}
13+
}
14+
15+
throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));
16+
}
417

518
export const pythonDebuggerExtension: ResourceDebuggerExtension = {
619
resourceType: 'python',
720
debugAdapter: 'debugpy',
821
extensionId: 'ms-python.python',
9-
displayName: 'Python',
22+
getDisplayName: (launchConfiguration: ExecutableLaunchConfiguration) => `Python: ${vscode.workspace.asRelativePath(getProjectFile(launchConfiguration))}`,
1023
getSupportedFileTypes: () => ['.py'],
11-
getProjectFile: (launchConfig) => {
12-
if (isPythonLaunchConfiguration(launchConfig)) {
13-
const programPath = launchConfig.program_path || launchConfig.project_path;
14-
if (programPath) {
15-
return programPath;
16-
}
24+
getProjectFile: (launchConfig) => getProjectFile(launchConfig),
25+
createDebugSessionConfigurationCallback: async (launchConfig, args, env, launchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration): Promise<void> => {
26+
if (!isPythonLaunchConfiguration(launchConfig)) {
27+
extensionLogOutputChannel.info(`The resource type was not python for ${JSON.stringify(launchConfig)}`);
28+
throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));
1729
}
1830

19-
throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));
31+
if (launchConfig.interpreter_path) {
32+
debugConfiguration.python = launchConfig.interpreter_path;
33+
}
34+
35+
// By default, activate support for Jinja debugging
36+
debugConfiguration.jinja = true;
37+
38+
// If module is specified, remove program from the debug configuration
39+
if (!!launchConfig.module) {
40+
delete debugConfiguration.program;
41+
debugConfiguration.module = launchConfig.module;
42+
}
2043
}
2144
};

0 commit comments

Comments
 (0)