diff --git a/extension/package.json b/extension/package.json index b55967002fc..eb4825afe68 100644 --- a/extension/package.json +++ b/extension/package.json @@ -23,7 +23,9 @@ "orchestrator", "apphost" ], - "activationEvents": [], + "activationEvents": [ + "workspaceContains:**/*AppHost*" + ], "main": "./dist/extension.js", "contributes": { "commands": [ @@ -57,7 +59,31 @@ "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": { "type": "git", @@ -84,11 +110,13 @@ }, "devDependencies": { "@eslint/js": "^9.27.0", + "@types/express": "^5.0.3", "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/node-forge": "^1.3.11", "@types/sinon": "^17.0.4", "@types/vscode": "^1.98.0", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "@vscode/test-cli": "^0.0.10", @@ -111,11 +139,13 @@ "dependencies": { "@vscode/extension-telemetry": "^1.0.0", "@vscode/vsce": "^3.3.2", + "express": "^5.1.0", "node-forge": "^1.3.1", "sinon": "^20.0.0", "ts-node": "^10.9.2", "vscode-jsonrpc": "^8.2.1", - "vscode-nls": "5.2.0" + "vscode-nls": "5.2.0", + "ws": "^8.18.3" }, "resolutions": { "braces": "^3.0.3" diff --git a/extension/src/dcp/dcpServer.ts b/extension/src/dcp/dcpServer.ts new file mode 100644 index 00000000000..1066963eb18 --- /dev/null +++ b/extension/src/dcp/dcpServer.ts @@ -0,0 +1,337 @@ +import express, { Request, Response, NextFunction } from 'express'; +import http from 'http'; +import WebSocket, { WebSocketServer } from 'ws'; +import { ChildProcess, spawn } from 'child_process'; +import { DebugConfiguration } from 'vscode'; +import * as vscode from 'vscode'; +import { mergeEnvs } from '../utils/environment'; +import { generateToken } from '../utils/security'; +import { extensionLogOutputChannel } from '../utils/logging'; +import { DcpServerInformation, ErrorDetails, ErrorResponse, RunSessionNotification, RunSessionPayload } from './types'; +import { sendStoppedToAspireDebugSession, shouldOverrideDcpDebug } from './debugAdapterFactory'; + +const runsBySession = new Map(); + +export class DcpServer { + public readonly info: DcpServerInformation; + public readonly app: express.Express; + private server: http.Server; + private wss: WebSocketServer; + + private constructor(info: DcpServerInformation, app: express.Express, server: http.Server, wss: WebSocketServer) { + this.info = info; + this.app = app; + this.server = server; + this.wss = wss; + } + + static async start(): Promise { + return new Promise((resolve, reject) => { + const token = generateToken(); + + const app = express(); + app.use(express.json()); + + // Log all incoming requests + app.use((req: Request, res: Response, next: NextFunction) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`); + console.log('Headers:', req.headers); + next(); + }); + + function requireHeaders(req: Request, res: Response, next: NextFunction): void { + const auth = req.header('Authorization'); + const dcpId = req.header('Microsoft-Developer-DCP-Instance-ID'); + if (!auth || !dcpId) { + res.status(401).json({ error: { code: 'MissingHeaders', message: 'Authorization and Microsoft-Developer-DCP-Instance-ID headers are required.' } }); + return; + } + + if (auth.split('Bearer ').length !== 2) { + res.status(401).json({ error: { code: 'InvalidAuthHeader', message: 'Authorization header must start with "Bearer "' } }); + return; + } + + if (auth.split('Bearer ')[1] !== token) { + res.status(401).json({ error: { code: 'InvalidToken', message: 'Invalid or missing token in Authorization header.' } }); + return; + } + + next(); + } + + app.get("/telemetry/enabled", (req: Request, res: Response) => { + res.json(false); + }); + + app.get('/info', (req: Request, res: Response) => { + res.json({ + protocols_supported: ["2024-03-03"] + }); + }); + + app.put('/run_session', requireHeaders, async (req: Request, res: Response) => { + const payload: RunSessionPayload = req.body; + const runId = Math.random().toString(36).substring(2, 10); + + const command = payload.env?.find(env => env.name === 'EXECUTABLE_COMMAND_NAME')?.value; + if (!command) { + const error: ErrorDetails = { + code: 'MissingCommand', + message: 'EXECUTABLE_COMMAND_NAME environment variable is required.', + details: [] + }; + + extensionLogOutputChannel.error(`Error creating run session ${runId}: ${error.message}`); + const response: ErrorResponse = { error }; + res.status(400).json(response).end(); + return; + } + + const processes: (vscode.DebugSession | vscode.Terminal)[] = []; + + if (payload.launch_configurations.length === 0) { + spawnProcess(); + } + else { + for (const launchConfig of payload.launch_configurations) { + if (launchConfig.mode === 'NoDebug' || shouldOverrideDcpDebug()) { + spawnProcess(launchConfig.project_path); + } + else { + let debugConfig: DebugConfiguration | undefined; + if (launchConfig.type === "node") { + debugConfig = { + type: 'pwa-node', + request: 'launch', + name: `DCP Debug Session ${runId} for ${launchConfig.project_path}`, + runtimeExecutable: command, + runtimeArgs: payload.args || [], + cwd: launchConfig.project_path, + env: mergeEnvs(process.env, payload.env), + console: 'integratedTerminal' + }; + } + else if (launchConfig.type === "python") { + if (!payload.args || payload.args.length === 0) { + const error: ErrorDetails = { + code: 'MissingScript', + message: 'Python debug mode requires a script to run. Please provide the script as the first argument in the args array.', + details: [] + }; + + extensionLogOutputChannel.error(`Error creating debug session ${runId}: ${error.message}`); + const response: ErrorResponse = { error }; + res.status(400).json(response).end(); + return; + } + + const script = payload.args[0]; + const args = payload.args.slice(1); + + debugConfig = { + type: 'python', + request: 'launch', + name: `DCP Debug Session ${runId} for ${launchConfig.project_path}`, + program: script, + args: args, + cwd: launchConfig.project_path, + env: mergeEnvs(process.env, payload.env), + console: 'integratedTerminal', + justMyCode: false + }; + } + else { + throw new Error(`Unsupported command for debug mode: ${command}`); + } + + extensionLogOutputChannel.info(`Debugging session created with ID: ${runId}`); + + const debugSession = await startAndGetDebugSession(debugConfig); + if (!debugSession) { + const error: ErrorDetails = { + code: 'DebugSessionFailed', + message: `Failed to start debug session for run ID ${runId}`, + details: [] + }; + + extensionLogOutputChannel.error(`Error creating debug session ${runId}: ${error.message}`); + const response: ErrorResponse = { error }; + res.status(400).json(response).end(); + return; + } + + processes.push(debugSession); + } + } + } + + runsBySession.set(runId, processes); + res.status(201).set('Location', `${req.protocol}://${req.get('host')}/run_session/${runId}`).end(); + extensionLogOutputChannel.info(`New run session created with ID: ${runId}`); + + function spawnProcess(workingDirectory?: string) { + const termName = `DCP Run ${runId}`; + const envVars = mergeEnvs(process.env, payload.env); + const terminal = vscode.window.createTerminal({ + name: termName, + cwd: workingDirectory ?? process.cwd(), + env: envVars + }); + + terminal.show(); + terminal.sendText(`${command} ${(payload.args ?? []).map(a => JSON.stringify(a)).join(' ')}`); + processes.push(terminal); + extensionLogOutputChannel.info(`Spawned terminal for run session ${runId}`); + } + }); + + app.delete('/run_session/:id', requireHeaders, async (req: Request, res: Response) => { + const runId = req.params.id; + if (runsBySession.has(runId)) { + const sessions = runsBySession.get(runId); + for (const session of sessions || []) { + if (isTerminal(session)) { + try { + session.dispose(); + extensionLogOutputChannel.info(`Closed terminal for run session ${runId}`); + } catch (err) { + extensionLogOutputChannel.error(`Failed to close terminal for run session ${runId}: ${err}`); + } + } else if (session instanceof ChildProcess) { + // Kill the spawned process if it exists + if (session.pid) { + try { + process.kill(-session.pid, 'SIGTERM'); + extensionLogOutputChannel.info(`Killed process for run session ${runId} (pid: ${session.pid})`); + } catch (err) { + extensionLogOutputChannel.error(`Failed to kill process for run session ${runId}: ${err}`); + } + } + } + } + + // After all processes/terminals are cleaned up, check for any active Aspire debug session and send 'stopped' + sendStoppedToAspireDebugSession(); + + runsBySession.delete(runId); + res.status(200).end(); + } else { + res.status(204).end(); + } + }); + + function isTerminal(obj: any): obj is vscode.Terminal { + return obj && typeof obj.dispose === 'function' && typeof obj.sendText === 'function'; + } + + const server = http.createServer(app); + const wss = new WebSocketServer({ noServer: true }); + + server.on('upgrade', (request, socket, head) => { + if (request.url?.startsWith('/run_session/notify')) { + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); + } else { + socket.destroy(); + } + }); + + wss.on('connection', (ws: WebSocket, req) => { + ws.send(JSON.stringify({ notification_type: 'connected' }) + '\n'); + }); + + server.listen(0, () => { + const addr = server.address(); + if (typeof addr === 'object' && addr) { + console.log(`DCP IDE Execution server listening on port ${addr.port} (HTTP)`); + const info: DcpServerInformation = { + address: `localhost:${addr.port}`, + token: token, + certificate: '' + }; + resolve(new DcpServer(info, app, server, wss)); + } else { + reject(new Error('Failed to get server address')); + } + }); + server.on('error', reject); + }); + } + + public emitProcessRestarted(session_id: string, pid?: number) { + this.broadcastNotification({ + notification_type: 'processRestarted', + session_id, + pid + }); + } + + public emitSessionTerminated(session_id: string, exit_code: number) { + this.broadcastNotification({ + notification_type: 'sessionTerminated', + session_id, + exit_code + }); + } + + public emitServiceLog(session_id: string, log_message: string, is_std_err: boolean = false) { + this.broadcastNotification({ + notification_type: 'serviceLogs', + session_id, + is_std_err, + log_message + }); + } + + private broadcastNotification(notification: RunSessionNotification) { + if (!this.wss) { return; } + const line = JSON.stringify(notification) + '\n'; + this.wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(line); + } + }); + } + + public stop(): void { + // Send WebSocket close message to all clients before shutting down + if (this.wss) { + this.wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.close(1000, 'DCP server shutting down'); + } + }); + this.wss.close(); + } + if (this.server) { + this.server.close(); + } + } +} + +async function startAndGetDebugSession(debugConfig: vscode.DebugConfiguration): Promise { + return new Promise(async (resolve) => { + const disposable = vscode.debug.onDidStartDebugSession(session => { + if (session.name === debugConfig.name) { + disposable.dispose(); + resolve(session); + } + }); + const started = await vscode.debug.startDebugging(undefined, debugConfig); + if (!started) { + disposable.dispose(); + resolve(undefined); + } + // Optionally add a timeout to avoid waiting forever + setTimeout(() => { + disposable.dispose(); + resolve(undefined); + }, 10000); + }); +} + +export function createDcpServer(): Promise { + return DcpServer.start(); +} \ No newline at end of file diff --git a/extension/src/dcp/debugAdapterFactory.ts b/extension/src/dcp/debugAdapterFactory.ts new file mode 100644 index 00000000000..2ba3bfa81db --- /dev/null +++ b/extension/src/dcp/debugAdapterFactory.ts @@ -0,0 +1,102 @@ +import * as vscode from 'vscode'; +import { EventEmitter } from 'vscode'; +import { getAspireTerminal, sendToAspireTerminal } from '../utils/terminal'; + +export class AspireDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { + createDebugAdapterDescriptor( + session: vscode.DebugSession, + executable: vscode.DebugAdapterExecutable | undefined + ): vscode.ProviderResult { + return new vscode.DebugAdapterInlineImplementation(new AspireDebugSession(session)); + } +} + +// Allow only a single Aspire debug session at a time +let currentAspireDebugSession: AspireDebugSession | undefined = undefined; + +class AspireDebugSession implements vscode.DebugAdapter { + private readonly _onDidSendMessage = new EventEmitter(); + public readonly onDidSendMessage = this._onDidSendMessage.event; + + public get session(): vscode.DebugSession { + return this._session; + } + + constructor(private _session: vscode.DebugSession) { + currentAspireDebugSession = this; + } + + handleMessage(message: any): void { + if (message.command === 'initialize') { + sendToAspireTerminal('aspire run', true); + this._onDidSendMessage.fire({ + type: 'event', + event: 'initialized', + body: {} + }); + this._onDidSendMessage.fire({ + type: 'response', + request_seq: message.seq, + success: true, + command: 'initialize', + body: { + supportsConfigurationDoneRequest: true + } + }); + } else if (message.command === 'disconnect' || message.command === 'terminate') { + // Dispose the Aspire terminal to stop the run + const terminal = getAspireTerminal(); + terminal.dispose(); + + this._onDidSendMessage.fire({ + type: 'response', + request_seq: message.seq, + success: true, + command: message.command, + body: {} + }); + } else { + // Respond to all other requests with a generic success + if (message.command) { + this._onDidSendMessage.fire({ + type: 'response', + request_seq: message.seq, + success: true, + command: message.command, + body: {} + }); + } + } + } + + sendStoppedEvent(reason: string = 'stopped'): void { + this._onDidSendMessage.fire({ + type: 'event', + event: 'stopped', + body: { + reason, + threadId: 1 // VS Code requires a threadId + } + }); + } + + dispose(): void { + const terminal = getAspireTerminal(); + terminal.dispose(); + + if (currentAspireDebugSession === this) { + currentAspireDebugSession = undefined; + } + } +} + +// Export a function to send 'stopped' to the current Aspire debug session +export function sendStoppedToAspireDebugSession(reason: string = 'stopped') { + if (currentAspireDebugSession) { + currentAspireDebugSession.sendStoppedEvent(reason); + } +} + +export function shouldOverrideDcpDebug(): boolean { + return !!currentAspireDebugSession && currentAspireDebugSession.session.configuration.noDebug; +} diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts new file mode 100644 index 00000000000..a02d4ed13c0 --- /dev/null +++ b/extension/src/dcp/types.ts @@ -0,0 +1,63 @@ +export type ErrorResponse = { + error: ErrorDetails; +}; + +export type ErrorDetails = { + code: string; + message: string; + details: ErrorDetails[]; +}; + +type LaunchConfigurationType = "project" | "node" | "python"; +type LaunchConfigurationMode = "Debug" | "NoDebug"; + +export interface LaunchConfiguration { + type: LaunchConfigurationType; + project_path: string; + mode?: LaunchConfigurationMode | undefined; + launch_profile?: string; + disable_launch_profile?: boolean; +} + +export interface EnvVar { + name: string; + value: string; +} + +export interface RunSessionPayload { + launch_configurations: LaunchConfiguration[]; + env?: EnvVar[]; + args?: string[]; +} + +export interface DcpServerInformation { + address: string; + token: string; + certificate: string; +} + +export type RunSessionNotification = + | ProcessRestartedNotification + | SessionTerminatedNotification + | ServiceLogsNotification; + +export interface BaseNotification { + notification_type: 'processRestarted' | 'sessionTerminated' | 'serviceLogs'; + session_id: string; +} + +export interface ProcessRestartedNotification extends BaseNotification { + notification_type: 'processRestarted'; + pid?: number; +} + +export interface SessionTerminatedNotification extends BaseNotification { + notification_type: 'sessionTerminated'; + exit_code: number; +} + +export interface ServiceLogsNotification extends BaseNotification { + notification_type: 'serviceLogs'; + is_std_err: boolean; + log_message: string; +} \ No newline at end of file diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 54707ccf182..beb6c6512ae 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -12,8 +12,11 @@ import { publishCommand } from './commands/publish'; import { errorMessage } from './loc/strings'; import { extensionLogOutputChannel } from './utils/logging'; import { initializeTelemetry, sendTelemetryEvent } from './utils/telemetry'; +import { createDcpServer, DcpServer } from './dcp/dcpServer'; +import { AspireDebugAdapterDescriptorFactory } from './dcp/debugAdapterFactory'; export let rpcServerInfo: RpcServerInformation | undefined; +export let dcpServer: DcpServer | undefined; export async function activate(context: vscode.ExtensionContext) { initializeTelemetry(context); @@ -24,6 +27,8 @@ export async function activate(context: vscode.ExtensionContext) { (connection, token: string) => new RpcClient(connection, token) ); + dcpServer = await createDcpServer(); + const cliRunCommand = vscode.commands.registerCommand('aspire-vscode.run', () => tryExecuteCommand('aspire-vscode.run', runCommand)); const cliAddCommand = vscode.commands.registerCommand('aspire-vscode.add', () => tryExecuteCommand('aspire-vscode.add', addCommand)); const cliNewCommand = vscode.commands.registerCommand('aspire-vscode.new', () => tryExecuteCommand('aspire-vscode.new', newCommand)); @@ -33,9 +38,14 @@ 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, + dcpServer: dcpServer }; } diff --git a/extension/src/utils/environment.ts b/extension/src/utils/environment.ts new file mode 100644 index 00000000000..a97400d7e23 --- /dev/null +++ b/extension/src/utils/environment.ts @@ -0,0 +1,11 @@ +import { EnvVar } from "../dcp/types"; + +export function mergeEnvs(base: NodeJS.ProcessEnv, envVars?: EnvVar[]): Record { + const merged: Record = { ...base }; + if (envVars) { + for (const e of envVars) { + merged[e.name] = e.value; + } + } + return merged; +} diff --git a/extension/src/utils/extensions.ts b/extension/src/utils/extensions.ts new file mode 100644 index 00000000000..21aaf32fa3d --- /dev/null +++ b/extension/src/utils/extensions.ts @@ -0,0 +1,20 @@ +import * as vscode from 'vscode'; + +function isExtensionInstalled(extensionId: string): boolean { + const ext = vscode.extensions.getExtension(extensionId); + return !!ext; +} + +export function isPythonExtensionInstalled(): boolean { + return isExtensionInstalled('ms-python.python'); +} + +export function getSupportedDebugLanguages(): string[] { + const languages = ['node']; + + if (isPythonExtensionInstalled()) { + languages.push('python'); + } + + return languages; +} \ No newline at end of file diff --git a/extension/src/utils/terminal.ts b/extension/src/utils/terminal.ts index 0547ab3a950..803108bf5eb 100644 --- a/extension/src/utils/terminal.ts +++ b/extension/src/utils/terminal.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; -import { rpcServerInfo } from '../extension'; +import { dcpServer, rpcServerInfo } from '../extension'; import { aspireTerminalName } from '../loc/strings'; import { extensionLogOutputChannel } from './logging'; +import { getSupportedDebugLanguages } from './extensions'; let hasRunGetAspireTerminal = false; export function getAspireTerminal(): vscode.Terminal { @@ -9,6 +10,10 @@ export function getAspireTerminal(): vscode.Terminal { throw new Error('RPC server is not initialized. Ensure activation before using this function.'); } + if (!dcpServer?.info) { + throw new Error('DCP server is not initialized. Ensure activation before using this function.'); + } + const terminalName = aspireTerminalName; const existingTerminal = vscode.window.terminals.find(terminal => terminal.name === terminalName); @@ -31,7 +36,17 @@ export function getAspireTerminal(): vscode.Terminal { ASPIRE_EXTENSION_TOKEN: rpcServerInfo.token, ASPIRE_EXTENSION_CERT: Buffer.from(rpcServerInfo.cert, 'utf-8').toString('base64'), ASPIRE_EXTENSION_PROMPT_ENABLED: 'true', - ASPIRE_LOCALE_OVERRIDE: vscode.env.language + + // Use the current locale in the CLI + ASPIRE_LOCALE_OVERRIDE: vscode.env.language, + + // Include DCP server info + DEBUG_SESSION_PORT: dcpServer.info.address, + DEBUG_SESSION_TOKEN: dcpServer.info.token, + //DEBUG_SESSION_SERVER_CERTIFICATE: Buffer.from(dcpServer.info.certificate, 'utf-8').toString('base64') + + // Indicate that this extension supports + DEBUG_SESSION_SUPPORTED_RESOURCE_TYPES: getSupportedDebugLanguages().join(',') }; return vscode.window.createTerminal({ @@ -40,9 +55,9 @@ export function getAspireTerminal(): vscode.Terminal { }); } -export function sendToAspireTerminal(command: string) { +export function sendToAspireTerminal(command: string, preserveFocus?: boolean) { const terminal = getAspireTerminal(); extensionLogOutputChannel.info(`Sending command to Aspire terminal: ${command}`); terminal.sendText(command); - terminal.show(); + terminal.show(preserveFocus); } diff --git a/extension/yarn.lock b/extension/yarn.lock index ac6b8b15922..79a4e55f9f1 100644 --- a/extension/yarn.lock +++ b/extension/yarn.lock @@ -638,6 +638,21 @@ resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@tsconfig/node16/-/node16-1.0.4.tgz" integrity sha1-C5LcwMwcgfbzBqOB8o4xsaVlNuk= +"@types/body-parser@*": + version "1.19.6" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha1-GFm+u4/X2smRikXVTBlxq4ta9HQ= + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha1-W6fzvE+73q/43e2VLl/yzFP42Fg= + dependencies: + "@types/node" "*" + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/eslint-scope/-/eslint-scope-3.7.7.tgz" @@ -659,6 +674,30 @@ resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/estree/-/estree-1.0.7.tgz" integrity sha1-QVjTEFJ2dz1bdpXNSDSxci5PN6g= +"@types/express-serve-static-core@^5.0.0": + version "5.0.6" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8" + integrity sha1-Qf7E6iDpx7IvAkq4ipXGuyiPUbg= + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^5.0.3": + version "5.0.3" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha1-bEvGrN3C4qWHFC4di+C84gdX6VY= + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha1-W3SasrFroRNCP+saZKldzTA5hHI= + "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" @@ -669,6 +708,11 @@ resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE= +"@types/mime@^1": + version "1.3.5" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha1-HvMC4Bz30rWg+lJnkMkSO/HQZpA= + "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/minimatch/-/minimatch-3.0.5.tgz" @@ -698,11 +742,38 @@ resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" integrity sha1-VuLMJsOXwDj6sOOpF6EtXFkJ6QE= +"@types/qs@*": + version "6.14.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha1-2LYM7PYvLbD7aOXgBgd7kXi4XeU= + +"@types/range-parser@*": + version "1.2.7" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha1-UK5DU+qt3AQEQnmBL1LIxlhX28s= + "@types/sarif@^2.1.4": version "2.1.7" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/sarif/-/sarif-2.1.7.tgz" integrity sha1-2rTRa6dWjphGxFSodk8zxdmOVSQ= +"@types/send@*": + version "0.17.5" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha1-2ZHU8rFvKx70lxMfAKkRQpB5HnQ= + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.8" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha1-gYDD++SnDo8AufcLm6fwjzWYeHc= + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + "@types/sinon@^17.0.4": version "17.0.4" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/sinon/-/sinon-17.0.4.tgz" @@ -720,6 +791,13 @@ resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/vscode/-/vscode-1.100.0.tgz" integrity sha1-Nc1iioaxFYeFbflL6UBUqrAfLxc= +"@types/ws@^8.18.1": + version "8.18.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha1-SEZOS/Ld/RfbE9hFRn9gcP/qSqk= + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@8.33.0", "@typescript-eslint/eslint-plugin@^8.28.0": version "8.33.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz" @@ -1104,6 +1182,14 @@ resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@xtuc/long/-/long-4.2.2.tgz" integrity sha1-0pHGpOl5ibXGHZrPOWrk/hM6cY0= +accepts@^2.0.0: + version "2.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha1-u89LpQdUZ/PyEx6rPP/HPC9deJU= + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -1485,6 +1571,21 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +body-parser@^2.2.0: + version "2.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha1-96llbeMFJJpxW1Sbe4/Rq5393Po= + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + boolbase@^1.0.0: version "1.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/boolbase/-/boolbase-1.0.0.tgz" @@ -1567,6 +1668,11 @@ bundle-name@^4.1.0: dependencies: run-applescript "^7.0.0" +bytes@3.1.2, bytes@^3.1.2: + version "3.1.2" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha1-iwvuuYYFrfGxKPpDhkA8AJ4CIaU= + c8@^9.1.0: version "9.1.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/c8/-/c8-9.1.0.tgz" @@ -1932,6 +2038,18 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha1-hEQmyzmPk0yu/LsXIgASa8fOrOI= + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.5: + version "1.0.5" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha1-i3cxYmVtHRCGeEyPI6VM5tc9eRg= + convert-source-map@^1.0.0, convert-source-map@^1.5.0: version "1.9.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/convert-source-map/-/convert-source-map-1.9.0.tgz" @@ -1942,6 +2060,16 @@ convert-source-map@^2.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha1-S1YPZJ/E6RjdCrdc9JYei8iC2Co= +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha1-V8f8PMKTrKuf7FTXPhVpDr5KF5M= + +cookie@^0.7.1: + version "0.7.2" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha1-VWNpxHKiupEPKXmJG1JrNDYjftc= + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/copy-descriptor/-/copy-descriptor-0.1.1.tgz" @@ -2157,6 +2285,11 @@ delayed-stream@~1.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +depd@2.0.0, depd@^2.0.0: + version "2.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8= + detect-file@^1.0.0: version "1.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/detect-file/-/detect-file-1.0.0.tgz" @@ -2261,6 +2394,11 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" +ee-first@1.1.1: + version "1.1.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + electron-to-chromium@^1.5.160: version "1.5.161" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz" @@ -2281,6 +2419,11 @@ emoji-regex@^9.2.2: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha1-hAyIA7DYBH9P8M+WMXazLU7z7XI= +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha1-e46omAd9fkCdOsRUdOo46vCFelg= + encoding-sniffer@^0.2.0: version "0.2.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz" @@ -2400,6 +2543,11 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/escalade/-/escalade-3.2.0.tgz" integrity sha1-ARo/aYVroYnf+n3I/M6Z0qh5A+U= +escape-html@^1.0.3: + version "1.0.3" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" @@ -2530,6 +2678,11 @@ esutils@^2.0.2: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esutils/-/esutils-2.0.3.tgz" integrity sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q= +etag@^1.8.1: + version "1.8.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + event-emitter@^0.3.5: version "0.3.5" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/event-emitter/-/event-emitter-0.3.5.tgz" @@ -2581,6 +2734,39 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +express@^5.1.0: + version "5.1.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha1-0xvq9xWgAW8NU/R9O016zyjHXMk= + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + ext@^1.7.0: version "1.7.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ext/-/ext-1.7.0.tgz" @@ -2706,6 +2892,18 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha1-cjBjc6qJ0FqCQu1WnthqG/98Vh8= + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + find-up@^1.0.0: version "1.1.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/find-up/-/find-up-1.1.2.tgz" @@ -2822,6 +3020,11 @@ form-data@^4.0.0: es-set-tostringtag "^2.1.0" mime-types "^2.1.12" +forwarded@0.2.0: + version "0.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha1-ImmTZCiq1MFcfr6XeahL8LKoGBE= + fragment-cache@^0.2.1: version "0.2.1" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fragment-cache/-/fragment-cache-0.2.1.tgz" @@ -2829,6 +3032,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@^2.0.0: + version "2.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha1-jdffahs6Gzpc8YbAWl3SZ2ImNaQ= + from@^0.1.7: version "0.1.7" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/from/-/from-0.1.7.tgz" @@ -3292,6 +3500,17 @@ htmlparser2@^9.1.0: domutils "^3.1.0" entities "^4.5.0" +http-errors@2.0.0, http-errors@^2.0.0: + version "2.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha1-t3dKFIbvc892Z6ya4IWMASxXudM= + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.2: version "7.0.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz" @@ -3369,7 +3588,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/inherits/-/inherits-2.0.4.tgz" integrity sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w= @@ -3394,6 +3613,11 @@ invert-kv@^1.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/invert-kv/-/invert-kv-1.0.0.tgz" integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha1-v/OFQ+64mEglB5/zoqjmy9RngbM= + is-absolute@^1.0.0: version "1.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-absolute/-/is-absolute-1.0.0.tgz" @@ -3567,6 +3791,11 @@ is-promise@^2.2.2: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-promise/-/is-promise-2.2.2.tgz" integrity sha1-OauVnMv5p3TPB597QMeib3YxNfE= +is-promise@^4.0.0: + version "4.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha1-Qv+fhCBsGZHSbev1IN1cAQQt0vM= + is-relative@^1.0.0: version "1.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-relative/-/is-relative-1.0.0.tgz" @@ -4129,6 +4358,11 @@ mdurl@^2.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/mdurl/-/mdurl-2.0.0.tgz" integrity sha1-gGduwEMwJd0+F+6YPQ/o3loiN+A= +media-typer@^1.1.0: + version "1.1.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha1-ardLjy0zIPIGSyqHo455Mf86VWE= + memoizee@0.4.X: version "0.4.17" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/memoizee/-/memoizee-0.4.17.tgz" @@ -4143,6 +4377,11 @@ memoizee@0.4.X: next-tick "^1.1.0" timers-ext "^0.1.7" +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha1-6pIvZgY1oiSe5WXgRJ+VHmtgOAg= + merge-stream@^2.0.0: version "2.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/merge-stream/-/merge-stream-2.0.0.tgz" @@ -4185,6 +4424,11 @@ mime-db@1.52.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/mime-db/-/mime-db-1.52.0.tgz" integrity sha1-u6vNwChZ9JhzAchW4zh85exDv3A= +mime-db@^1.54.0: + version "1.54.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha1-zds+5PnGRTDf9kAjZmHULLajFPU= + mime-types@^2.1.12, mime-types@^2.1.27: version "2.1.35" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/mime-types/-/mime-types-2.1.35.tgz" @@ -4192,6 +4436,13 @@ mime-types@^2.1.12, mime-types@^2.1.27: dependencies: mime-db "1.52.0" +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha1-sdlNaZepsy/WnrrtDbc96Ky1Gc4= + dependencies: + mime-db "^1.54.0" + mime@^1.3.4: version "1.6.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/mime/-/mime-1.6.0.tgz" @@ -4352,6 +4603,11 @@ natural-compare@^1.4.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@^1.0.0: + version "1.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha1-tskbtHFy1p+Tz9fDV7u1KQGbX2o= + neo-async@^2.6.2: version "2.6.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/neo-async/-/neo-async-2.6.2.tgz" @@ -4526,6 +4782,13 @@ object.reduce@^1.0.0: for-own "^1.0.0" make-iterator "^1.0.0" +on-finished@^2.4.1: + version "2.4.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha1-WMjEQRblSEWtV/FKsQsDUzGErD8= + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/once/-/once-1.4.0.tgz" @@ -4714,6 +4977,11 @@ parse5@^7.0.0, parse5@^7.1.2: dependencies: entities "^6.0.0" +parseurl@^1.3.3: + version "1.3.3" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ= + pascalcase@^0.1.1: version "0.1.1" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/pascalcase/-/pascalcase-0.1.1.tgz" @@ -4779,6 +5047,11 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha1-c5kMwp5Xo/8qDZFAlRVt9dt56LQ= + path-type@^1.0.0: version "1.1.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-type/-/path-type-1.1.0.tgz" @@ -4910,6 +5183,14 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha1-eCDZsWEgzFXKmud5JoCufbptf+I= +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha1-8Z/mnOqzEe65S0LnDowgcPm6ECU= + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + pump@^2.0.0: version "2.0.1" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/pump/-/pump-2.0.1.tgz" @@ -4945,7 +5226,7 @@ punycode@^2.1.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/punycode/-/punycode-2.3.1.tgz" integrity sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU= -qs@^6.9.1: +qs@^6.14.0, qs@^6.9.1: version "6.14.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/qs/-/qs-6.14.0.tgz" integrity sha1-xj+kBoDSxclBQSoOiZyJr2DAqTA= @@ -4964,6 +5245,21 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +range-parser@^1.2.1: + version "1.2.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha1-PPNwI9GZ4cJNGlW4SADC8+ZGgDE= + +raw-body@^3.0.0: + version "3.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha1-JbNHbwelFgBhna4/6C3cKKNuXg8= + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + rc-config-loader@^4.1.3: version "4.1.3" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/rc-config-loader/-/rc-config-loader-4.1.3.tgz" @@ -5203,6 +5499,17 @@ reusify@^1.0.4: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/reusify/-/reusify-1.1.0.tgz" integrity sha1-D+E7lSLhRz9RtVjueW4I8R+bSJ8= +router@^2.2.0: + version "2.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha1-AZvmILcRyHZBFnzHm5kJDwCxRu8= + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + run-applescript@^7.0.0: version "7.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/run-applescript/-/run-applescript-7.0.0.tgz" @@ -5215,7 +5522,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha1-Hq+fqb2x/dTsdfWPnNtOa3gn7sY= @@ -5282,6 +5589,23 @@ semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semve resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/semver/-/semver-7.7.2.tgz" integrity sha1-Z9mf3NNc7CHm+Lh6f9UVoz+YK1g= +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha1-MqdVT7d3uDHfqCg3D3c6OAjTchI= + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + serialize-javascript@^6.0.2: version "6.0.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/serialize-javascript/-/serialize-javascript-6.0.2.tgz" @@ -5289,6 +5613,16 @@ serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" +serve-static@^2.2.0: + version "2.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha1-nAJWTuJZvdIlG4LWWaLn4ZONZvk= + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/set-blocking/-/set-blocking-2.0.0.tgz" @@ -5321,6 +5655,11 @@ setimmediate@^1.0.5: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha1-ZsmiSnP5/CjL5msJ/tPTPcrxtCQ= + shallow-clone@^3.0.0: version "3.0.1" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/shallow-clone/-/shallow-clone-3.0.1.tgz" @@ -5548,6 +5887,16 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha1-VcsADM8dSHKL0jxoWgY5mM8aG2M= + +statuses@^2.0.1: + version "2.0.2" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha1-j3XuzvdlteHPzcCA2llAntQk44I= + stdin-discarder@^0.2.2: version "0.2.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/stdin-discarder/-/stdin-discarder-0.2.2.tgz" @@ -5923,6 +6272,11 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha1-O+NDIaiKgg7RvYDfqjPkefu43TU= + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ts-api-utils/-/ts-api-utils-2.1.0.tgz" @@ -6007,6 +6361,15 @@ type-fest@^4.2.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/type-fest/-/type-fest-4.41.0.tgz" integrity sha1-auHI5XMSc8K/H1itOcuuLJGkbFg= +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha1-ZPbPA/kvzkAVwrIkeT9r3UsGjJc= + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + type@^2.7.2: version "2.7.3" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/type/-/type-2.7.3.tgz" @@ -6119,6 +6482,11 @@ universalify@^2.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/universalify/-/universalify-2.0.1.tgz" integrity sha1-Fo78IYCWTmOG0GHglN9hr+I5sY0= +unpipe@1.0.0: + version "1.0.0" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unset-value@^1.0.0: version "1.0.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/unset-value/-/unset-value-1.0.0.tgz" @@ -6211,6 +6579,11 @@ value-or-function@^3.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/value-or-function/-/value-or-function-3.0.0.tgz" integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= +vary@^1.1.2: + version "1.1.2" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vinyl-fs@^3.0.0, vinyl-fs@^3.0.3: version "3.0.3" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vinyl-fs/-/vinyl-fs-3.0.3.tgz" @@ -6449,6 +6822,11 @@ wrappy@1: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +ws@^8.18.3: + version "8.18.3" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha1-tWuIq//eYnkcY5FwQAyT3LDJVHI= + xml2js@^0.5.0: version "0.5.0" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/xml2js/-/xml2js-0.5.0.tgz" diff --git a/playground/python/instrumented_script/main.py b/playground/python/instrumented_script/main.py index 36b440ef9e0..380680a4b22 100644 --- a/playground/python/instrumented_script/main.py +++ b/playground/python/instrumented_script/main.py @@ -6,3 +6,5 @@ # Write a basic log message. logging.getLogger(__name__).info("Hello world!") + +print("This is a test script that logs a message.") \ No newline at end of file diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 7d0b8c864f0..b78f49006b5 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -19,6 +19,7 @@ using Aspire.Hosting.Dcp.Model; using Aspire.Hosting.Eventing; using Aspire.Hosting.Utils; +using Humanizer; using Json.Patch; using k8s; using k8s.Autorest; @@ -33,6 +34,7 @@ namespace Aspire.Hosting.Dcp; internal sealed class DcpExecutor : IDcpExecutor, IConsoleLogsService, IAsyncDisposable { internal const string DebugSessionPortVar = "DEBUG_SESSION_PORT"; + internal const string DebugSessionSupportedResourceTypes = "DEBUG_SESSION_SUPPORTED_RESOURCE_TYPES"; internal const string DefaultAspireNetworkName = "default-aspire-network"; // Disposal of the DcpExecutor means shutting down watches and log streams, @@ -838,10 +840,41 @@ private void PreparePlainExecutables() // The working directory is always relative to the app host project directory (if it exists). exe.Spec.WorkingDirectory = executable.WorkingDirectory; - exe.Spec.ExecutionType = ExecutionType.Process; exe.Annotate(CustomResource.OtelServiceNameAnnotation, executable.Name); exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name); + + // Transform the type name into an identifier, stripping out + // the ending "AppResource" or "Resource" and converting from + // PascalCase to snake_case. + // e.g. "PythonAppResource" becomes "python", + // and "ProjectResource" becomes "project". + var resourceType = executable.GetType().Name + .RemoveSuffix("AppResource") + .RemoveSuffix("Resource") + .Underscore(); + + if (GetDebugSupportedResourceTypes().Contains(resourceType) && !string.IsNullOrEmpty(_configuration[DebugSessionPortVar])) + { + exe.Spec.ExecutionType = ExecutionType.IDE; + var projectLaunchConfiguration = new ProjectLaunchConfiguration(); + projectLaunchConfiguration.Type = resourceType; + projectLaunchConfiguration.ProjectPath = executable.WorkingDirectory; + + if (_configuration[KnownConfigNames.ExtensionEndpoint] is not null) + { + projectLaunchConfiguration.Mode = ProjectLaunchMode.Debug; + } + + exe.AnnotateAsObjectList(Executable.LaunchConfigurationsAnnotation, projectLaunchConfiguration); + } + else + { + exe.Spec.ExecutionType = ExecutionType.Process; + } + + executable.Annotations.Add(new EnvironmentCallbackAnnotation(ctx => ctx.Add(CustomResource.CommandNameEnvironmentVariable, executable.Command))); + SetInitialResourceState(executable, exe); var exeAppResource = new AppResource(executable, exe); @@ -882,6 +915,11 @@ private void PrepareProjectExecutables() var projectLaunchConfiguration = new ProjectLaunchConfiguration(); projectLaunchConfiguration.ProjectPath = projectMetadata.ProjectPath; + if (_configuration[KnownConfigNames.ExtensionEndpoint] is not null) + { + projectLaunchConfiguration.Mode = ProjectLaunchMode.Debug; + } + var projectArgs = new List(); if (!string.IsNullOrEmpty(_configuration[DebugSessionPortVar])) @@ -1904,4 +1942,21 @@ private static List BuildContainerMounts(IResource container) return volumeMounts; } + + /// + /// Returns a list of resource types that are supported for IDE launch. Always contains project + /// + private List GetDebugSupportedResourceTypes() + { + var types = _configuration[DebugSessionSupportedResourceTypes] is not { } supportedResourceTypes + ? ["project"] + : supportedResourceTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + + if (!types.Contains("project")) + { + types.Add("project"); + } + + return types; + } } diff --git a/src/Aspire.Hosting/Dcp/Model/Executable.cs b/src/Aspire.Hosting/Dcp/Model/Executable.cs index 6ecdba0d0c7..780331ab57c 100644 --- a/src/Aspire.Hosting/Dcp/Model/Executable.cs +++ b/src/Aspire.Hosting/Dcp/Model/Executable.cs @@ -276,9 +276,7 @@ internal static class ProjectLaunchMode internal sealed class ProjectLaunchConfiguration { [JsonPropertyName("type")] -#pragma warning disable CA1822 // We want this member to be non-static, as it is used in serialization. - public string Type => "project"; -#pragma warning restore CA1822 + public string Type { get; set; } = "project"; [JsonPropertyName("mode")] public string Mode { get; set; } = System.Diagnostics.Debugger.IsAttached ? ProjectLaunchMode.Debug : ProjectLaunchMode.NoDebug; diff --git a/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs b/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs index 4a490640a24..1fcc2c48094 100644 --- a/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs +++ b/src/Aspire.Hosting/Dcp/Model/ModelCommon.cs @@ -29,6 +29,7 @@ internal abstract class CustomResource : KubernetesObject, IMetadata Metadata.Annotations?.TryGetValue(ResourceNameAnnotation, out var value) is true ? value : null; diff --git a/src/Shared/StringUtils.cs b/src/Shared/StringUtils.cs index 609523a537c..eedf6eb679f 100644 --- a/src/Shared/StringUtils.cs +++ b/src/Shared/StringUtils.cs @@ -32,4 +32,14 @@ public static string Unescape(string value) { return HttpUtility.UrlDecode(value); } + + public static string RemoveSuffix(this string value, string suffix) + { + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(suffix); + + return value.EndsWith(suffix, StringComparison.Ordinal) + ? value[..^suffix.Length] + : value; + } } diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 8907bb8bc6c..aa1ef88306f 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -1231,6 +1231,84 @@ public async Task CancelTokenDuringStartup() Assert.True(tokenSource.IsCancellationRequested); } + [Fact] + public async Task PlainExecutable_ExtensionMode_SupportedDebugMode_RunsInIde() + { + // Arrange + var builder = DistributedApplication.CreateBuilder(); + builder.AddResource(new TestExecutableResource("test-working-directory")); + builder.AddResource(new TestOtherExecutableResource("test-working-directory-2")); + + // Simulate debug session port and extension endpoint (extension mode) + var configDict = new Dictionary + { + [DcpExecutor.DebugSessionPortVar] = "12345", + [DcpExecutor.DebugSessionSupportedResourceTypes] = "test_executable", + [KnownConfigNames.ExtensionEndpoint] = "http://localhost:1234" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + + // Act + await appExecutor.RunApplicationAsync(); + + // Assert + var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); + Assert.Equal(2, dcpExes.Count); + + var debuggableExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "TestExecutable"); + Assert.Equal(ExecutionType.IDE, debuggableExe.Spec.ExecutionType); + Assert.True(debuggableExe.TryGetAnnotationAsObjectList(Executable.LaunchConfigurationsAnnotation, out var launchConfigs1)); + var config1 = Assert.Single(launchConfigs1); + Assert.Equal(ProjectLaunchMode.Debug, config1.Mode); + Assert.Equal("test_executable", config1.Type); + Assert.Equal("test-working-directory", config1.ProjectPath); + + var nonDebuggableExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "TestOtherExecutable"); + Assert.Equal(ExecutionType.Process, nonDebuggableExe.Spec.ExecutionType); + Assert.False(nonDebuggableExe.TryGetAnnotationAsObjectList(Executable.LaunchConfigurationsAnnotation, out _)); + } + + [Fact] + public async Task PlainExecutable_NoExtensionMode_RunInProcess() + { + // Arrange + var builder = DistributedApplication.CreateBuilder(); + builder.AddResource(new TestExecutableResource("test-working-directory")); + builder.AddResource(new TestOtherExecutableResource("test-working-directory-2")); + + // Simulate no extension endpoint (no extension mode) + var configDict = new Dictionary + { + [KnownConfigNames.ExtensionEndpoint] = null + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + + // Act + await appExecutor.RunApplicationAsync(); + + // Assert + var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); + Assert.Equal(2, dcpExes.Count); + + var debuggableExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "TestExecutable"); + Assert.Equal(ExecutionType.Process, debuggableExe.Spec.ExecutionType); + Assert.False(debuggableExe.TryGetAnnotationAsObjectList(Executable.LaunchConfigurationsAnnotation, out _)); + + var nonDebuggableExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "TestOtherExecutable"); + Assert.Equal(ExecutionType.Process, nonDebuggableExe.Spec.ExecutionType); + Assert.False(nonDebuggableExe.TryGetAnnotationAsObjectList(Executable.LaunchConfigurationsAnnotation, out _)); + } + private static void HasKnownCommandAnnotations(IResource resource) { var commandAnnotations = resource.Annotations.OfType().ToList(); @@ -1289,6 +1367,10 @@ private sealed class TestProject : IProjectMetadata public LaunchSettings LaunchSettings { get; } = new(); } + private sealed class TestExecutableResource(string directory) : ExecutableResource("TestExecutable", "test", directory); + + private sealed class TestOtherExecutableResource(string directory) : ExecutableResource("TestOtherExecutable", "test-other", directory); + private sealed class CustomChildResource(string name, IResource parent) : Resource(name), IResourceWithParent { public IResource Parent => parent;