diff --git a/package-lock.json b/package-lock.json index 9a5abc202..70322c9e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1199,6 +1199,7 @@ "version": "0.2.11", "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureappsettings/-/vscode-azext-azureappsettings-0.2.11.tgz", "integrity": "sha512-zu6RedxoVEgOTyTqyeo5HHPowK3aEYyD9if96b3TF3fkiuxuRlR/19V0qmoKxF6w8nYYhuUDScL2i+zlcu9+xQ==", + "peer": true, "dependencies": { "@microsoft/vscode-azext-utils": "^3.4.2" } @@ -1543,6 +1544,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1832,7 +1834,8 @@ "version": "16.18.126", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", - "devOptional": true + "devOptional": true, + "peer": true }, "node_modules/@types/picomatch": { "version": "3.0.2", @@ -1948,6 +1951,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -1982,6 +1986,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -2565,6 +2570,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2619,6 +2625,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3154,6 +3161,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3217,6 +3225,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -4521,6 +4530,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4622,6 +4632,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -7629,6 +7640,7 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", @@ -10072,6 +10084,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10380,7 +10393,8 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", @@ -10568,6 +10582,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10773,6 +10788,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -11034,6 +11050,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -11080,6 +11097,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -11181,6 +11199,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/package.json b/package.json index 5cf12813a..9aef75313 100644 --- a/package.json +++ b/package.json @@ -990,6 +990,12 @@ "properties": { "command": { "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } } } } diff --git a/src/commands/CommandAttributes.ts b/src/commands/CommandAttributes.ts index f0f961bad..c4eaaed96 100644 --- a/src/commands/CommandAttributes.ts +++ b/src/commands/CommandAttributes.ts @@ -31,4 +31,12 @@ export class CommandAttributes { ], }; + static readonly Debug: ActivityAttributes = { + description: "Starts the Azure Functions host in debug mode, allowing you to set breakpoints and step through your function code locally using a debugger.", + troubleshooting: [ + "Function host fails to start — check the output logs for errors related to your function code or configuration.", + "Breakpoints are not being hit — ensure that the debugger is properly attached and that you're running the function host in debug mode.", + "Port conflicts — verify that the ports required for debugging (e.g., 9229 for Node.js) are not being used by other applications.", + ], + }; } diff --git a/src/commands/debug/PostFuncDebugExecuteStep.ts b/src/commands/debug/PostFuncDebugExecuteStep.ts new file mode 100644 index 000000000..cc31a945c --- /dev/null +++ b/src/commands/debug/PostFuncDebugExecuteStep.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ActivityChildItem, ActivityChildType, activityInfoContext, AzureWizardExecuteStep, createContextValue, randomUtils, type ExecuteActivityContext, type ExecuteActivityOutput, type IActionContext } from "@microsoft/vscode-azext-utils"; +import { stripVTControlCharacters } from "node:util"; +import { ThemeIcon } from "vscode"; + +export class PostFuncDebugExecuteStep extends AzureWizardExecuteStep { + public priority: number = 999; + public stepName: string = 'PostFuncDebugExecuteStep'; + // public options: AzureWizardExecuteStepOptions = { + // continueOnFail: true + // } + + public constructor(readonly logs: string[]) { + super(); + } + + public async execute(context: T): Promise { + const errorLogs: string[] = []; + // eslint-disable-next-line no-control-regex + const redAnsiRegex = /\x1b\[(?:[0-9;]*31m|[0-9;]*91m|38;5;(9|1)m)/; + const functionErrors = [ + /No job functions found/i, + /Worker was unable to load entry point/i, + /SyntaxError:/i, + /Cannot find module/i, + /Failed to start Worker Channel/i, + /Serialization and deserialization.*not supported/i + ]; + for (const log of this.logs) { + if (redAnsiRegex.test(log) || functionErrors.some(err => err.test(log))) { + errorLogs.push(log); + } + } + + if (errorLogs.length > 0) { + context.activityAttributes = context.activityAttributes || {}; + context.activityAttributes.logs = errorLogs.map(log => { return { content: stripVTControlCharacters(log) }; }); + context.activityChildren = []; + throw new Error('This is from the error in execute'); + } + + return; + } + + public shouldExecute(_context: T): boolean { + return true; + } + + public createFailOutput(_context: T): ExecuteActivityOutput { + return { + item: new ActivityChildItem({ + label: 'Click to have Copilot help diagnose the issue.', + id: `${randomUtils.getRandomHexString(8)}-terminateDebugSession-fail`, + activityType: ActivityChildType.Fail, + contextValue: createContextValue([activityInfoContext, 'terminateDebugSessionFail']), + iconPath: new ThemeIcon('sparkle'), + // a little trick to remove the description timer on activity children + description: ' ', + command: { + "command": "azureResourceGroups.askAgentAboutActivityLogItem", + "title": "Ask Copilot", + } + }) + }; + } +} diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 9fe2417d9..b88cdaf68 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -20,16 +20,25 @@ import { getWorkspaceSetting } from '../vsCodeConfig/settings'; const funcTaskReadyEmitter = new vscode.EventEmitter(); export const onDotnetFuncTaskReady = funcTaskReadyEmitter.event; +// flag used by func core tools to indicate to wait for the debugger to attach before starting the worker +const dotnetIsolatedDebugFlag = '--dotnet-isolated-debug'; +const enableJsonOutput = '--enable-json-output'; export async function startFuncProcessFromApi( buildPath: string, args: string[], env: { [key: string]: string } -): Promise<{ processId: string; success: boolean; error: string }> { - const result = { +): Promise<{ processId: string; success: boolean; error: string, stream: AsyncIterable | undefined }> { + const result: { + processId: string; + success: boolean; + error: string; + stream: AsyncIterable | undefined; + } = { processId: '', success: false, - error: '' + error: '', + stream: undefined }; let funcHostStartCmd: string = 'func host start'; @@ -66,6 +75,7 @@ export async function startFuncProcessFromApi( const taskInfo = await startFuncTask(context, workspaceFolder, buildPath, funcTask); result.processId = await pickChildProcess(taskInfo); result.success = true; + result.stream = taskInfo.stream; } catch (err) { const pError = parseError(err); result.error = pError.message; @@ -140,6 +150,9 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo const funcPort: string = await getFuncPortFromTaskOrProject(context, funcTask, workspaceFolder); let statusRequestTimeout: number = intervalMs; const maxTime: number = Date.now() + timeoutInSeconds * 1000; + const funcShellExecution = funcTask.execution as vscode.ShellExecution; + const debugModeOn = funcShellExecution.commandLine?.includes(dotnetIsolatedDebugFlag) && funcShellExecution.commandLine?.includes(enableJsonOutput); + while (Date.now() < maxTime) { if (taskError !== undefined) { throw taskError; @@ -147,27 +160,38 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo const taskInfo: IRunningFuncTask | undefined = runningFuncTaskMap.get(workspaceFolder, buildPath); if (taskInfo) { - for (const scheme of ['http', 'https']) { - const statusRequest: AzExtRequestPrepareOptions = { url: `${scheme}://localhost:${funcPort}/admin/host/status`, method: 'GET' }; - if (scheme === 'https') { - statusRequest.rejectUnauthorized = false; + if (debugModeOn) { + // if we are in dotnet isolated debug mode, we need to find the pid from the terminal output + // if there is no pid yet, keep waiting + const newPid = await getWorkerPidFromJsonOutput(taskInfo); + if (newPid) { + taskInfo.processId = newPid; + return taskInfo; } - - try { - // wait for status url to indicate functions host is running - const response = await sendRequestWithTimeout(context, statusRequest, statusRequestTimeout, undefined); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - if (response.parsedBody.state.toLowerCase() === 'running') { - funcTaskReadyEmitter.fire(workspaceFolder); - return taskInfo; + } else { + // otherwise, we have to wait for the status url to indicate the host is running + for (const scheme of ['http', 'https']) { + const statusRequest: AzExtRequestPrepareOptions = { url: `${scheme}://localhost:${funcPort}/admin/host/status`, method: 'GET' }; + if (scheme === 'https') { + statusRequest.rejectUnauthorized = false; } - } catch (error) { - if (requestUtils.isTimeoutError(error)) { - // Timeout likely means localhost isn't ready yet, but we'll increase the timeout each time it fails just in case it's a slow computer that can't handle a request that fast - statusRequestTimeout *= 2; - context.telemetry.measurements.maxStatusTimeout = statusRequestTimeout; - } else { - // ignore + + try { + // wait for status url to indicate functions host is running + const response = await sendRequestWithTimeout(context, statusRequest, statusRequestTimeout, undefined); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (response.parsedBody.state.toLowerCase() === 'running') { + funcTaskReadyEmitter.fire(workspaceFolder); + return taskInfo; + } + } catch (error) { + if (requestUtils.isTimeoutError(error)) { + // Timeout likely means localhost isn't ready yet, but we'll increase the timeout each time it fails just in case it's a slow computer that can't handle a request that fast + statusRequestTimeout *= 2; + context.telemetry.measurements.maxStatusTimeout = statusRequestTimeout; + } else { + // ignore + } } } } @@ -182,6 +206,23 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo } } +async function getWorkerPidFromJsonOutput(taskInfo: IRunningFuncTask): Promise { + // if there is no stream yet or if the output doesn't include the workerProcessId yet, then keep waiting + if (!taskInfo.stream) { + return; + } + + for await (const chunk of taskInfo.stream) { + if (chunk.includes(`{ "name":"dotnet-worker-startup", "workerProcessId" :`)) { + const matches = chunk.match(/"workerProcessId"\s*:\s*(\d+)/); + if (matches && matches.length > 1) { + return Number(matches[1]); + } + } + } + return; +} + type OSAgnosticProcess = { command: string | undefined; pid: number | string }; /** diff --git a/src/debug/FuncTaskProvider.ts b/src/debug/FuncTaskProvider.ts index e583003e9..ca24bfe81 100644 --- a/src/debug/FuncTaskProvider.ts +++ b/src/debug/FuncTaskProvider.ts @@ -104,6 +104,11 @@ export class FuncTaskProvider implements TaskProvider { private async createTask(context: IActionContext, command: string, folder: WorkspaceFolder, projectRoot: string | undefined, language: string | undefined, definition?: TaskDefinition): Promise { const funcCliPath = await getFuncCliPath(context, folder); + const args = (definition?.args || []) as string[]; + if (args.length > 0) { + command = `${command} ${args.join(' ')}`; + } + let commandLine: string = `${funcCliPath} ${command}`; if (language === ProjectLanguage.Python) { commandLine = venvUtils.convertToVenvCommand(commandLine, folder.uri.fsPath); diff --git a/src/extension.ts b/src/extension.ts index 47be7118f..b8eb3e5e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,7 +28,7 @@ import { PowerShellDebugProvider } from './debug/PowerShellDebugProvider'; import { PythonDebugProvider } from './debug/PythonDebugProvider'; import { handleUri } from './downloadAzureProject/handleUri'; import { ext } from './extensionVariables'; -import { registerFuncHostTaskEvents } from './funcCoreTools/funcHostTask'; +import { registerFuncHostTaskEvents, terminalEventReader } from './funcCoreTools/funcHostTask'; import { validateFuncCoreToolsInstalled } from './funcCoreTools/validateFuncCoreToolsInstalled'; import { validateFuncCoreToolsIsLatest } from './funcCoreTools/validateFuncCoreToolsIsLatest'; import { getResourceGroupsApi } from './getExtensionApi'; @@ -154,4 +154,5 @@ export async function activateInternal(context: vscode.ExtensionContext, perfSta export async function deactivateInternal(): Promise { await emulatorClient.disposeAsync(); + terminalEventReader?.dispose(); } diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 26557be2e..67c31ce45 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerEvent, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { AzureWizard, nonNullValue, registerEvent, type IActionContext } from '@microsoft/vscode-azext-utils'; import * as path from 'path'; import * as vscode from 'vscode'; +import { CommandAttributes } from '../commands/CommandAttributes'; import { tryGetFunctionProjectRoot } from '../commands/createNewProject/verifyIsProject'; +import { PostFuncDebugExecuteStep } from '../commands/debug/PostFuncDebugExecuteStep'; import { localSettingsFileName } from '../constants'; import { getLocalSettingsJson } from '../funcConfig/local.settings'; import { localize } from '../localize'; +import { createActivityContext } from '../utils/activityUtils'; import { cpUtils } from '../utils/cpUtils'; import { getWorkspaceSetting } from '../vsCodeConfig/settings'; @@ -17,6 +20,9 @@ export interface IRunningFuncTask { taskExecution: vscode.TaskExecution; processId: number; portNumber: string; + // stream for reading `func host start` output + stream: AsyncIterable | undefined; + logs: string[]; } interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { @@ -75,37 +81,93 @@ class RunningFunctionTaskMap { export const runningFuncTaskMap: RunningFunctionTaskMap = new RunningFunctionTaskMap(); -const funcTaskStartedEmitter = new vscode.EventEmitter(); +const funcTaskStartedEmitter = new vscode.EventEmitter<{ scope: vscode.WorkspaceFolder | vscode.TaskScope, execution?: vscode.ShellExecution }>(); export const onFuncTaskStarted = funcTaskStartedEmitter.event; export const buildPathToWorkspaceFolderMap = new Map(); const defaultFuncPort: string = '7071'; +const funcCommandRegex: RegExp = /(func(?:\.exe)?)\s+host\s+start/i; export function isFuncHostTask(task: vscode.Task): boolean { const commandLine: string | undefined = task.execution && (task.execution).commandLine; - return /(func(?:\.exe)?)\s+host\s+start/i.test(commandLine || ''); + return funcCommandRegex.test(commandLine || ''); } +export function isFuncShellEvent(event: vscode.TerminalShellExecutionStartEvent): boolean { + const commandLine = event.execution && event.execution.commandLine; + return funcCommandRegex.test(commandLine.value || ''); +} + + +let latestTerminalShellExecutionEvent: vscode.TerminalShellExecutionStartEvent | undefined; +export let terminalEventReader: vscode.Disposable; export function registerFuncHostTaskEvents(): void { + // we need to register this listener before the func host task starts, so we can capture the terminal output stream + terminalEventReader = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => { + /** + * This will pick up any terminal that, including those started outside of tasks (e.g. via the command palette). + * But we don't actually access the terminal stream until the `func host start` task starts, at which time this will be pointing to the correct terminal + * */ + latestTerminalShellExecutionEvent = terminalShellExecEvent; + }); registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; + + if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { const portNumber = await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope); - const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber }; + const logs: string[] = []; + const runningFuncTask: IRunningFuncTask = { + processId: e.processId, + taskExecution: e.execution, + portNumber, + stream: latestTerminalShellExecutionEvent?.execution.read(), + logs + }; + runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); - funcTaskStartedEmitter.fire(e.execution.task.scope); + funcTaskStartedEmitter.fire({ scope: e.execution.task.scope, execution: e.execution.task.execution as vscode.ShellExecution }); } }); - registerEvent('azureFunctions.onDidEndTask', vscode.tasks.onDidEndTaskProcess, (context: IActionContext, e: vscode.TaskProcessEndEvent) => { + registerEvent('azureFunctions.onDidEndTask', vscode.tasks.onDidEndTaskProcess, async (context: IActionContext, e: vscode.TaskProcessEndEvent) => { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { + const scope = nonNullValue(e.execution.task.scope); + const task = runningFuncTaskMap.get(scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); + const wizardContext = Object.assign(context, await createActivityContext({ withChildren: true })); + wizardContext.activityAttributes = CommandAttributes.Debug; + wizardContext.activityTitle = localize('funcTaskEnded', 'Function host task ended.'); + + const wizard = new AzureWizard(wizardContext, { + title: localize('funcTaskEnded', 'Function host task ended.'), + + promptSteps: [], + executeSteps: [new PostFuncDebugExecuteStep(task?.logs ?? [])] + }); + try { + await wizard.execute(); + } catch (error) { + // swallow errors + console.log(error); + } runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); } }); + onFuncTaskStarted(async ({ scope, execution }) => { + const task = runningFuncTaskMap.get(scope, execution?.options?.cwd); + if (!task) { + return; + } + + for await (const chunk of task.stream ?? []) { + task.logs.push(chunk); + } + }); + registerEvent('azureFunctions.onDidTerminateDebugSession', vscode.debug.onDidTerminateDebugSession, async (context: IActionContext, debugSession: vscode.DebugSession) => { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; @@ -146,7 +208,7 @@ export async function stopFuncTaskIfRunning(workspaceFolder: vscode.WorkspaceFol for (const runningFuncTaskItem of runningFuncTask) { if (!runningFuncTaskItem) break; if (terminate) { - runningFuncTaskItem.taskExecution.terminate() + runningFuncTaskItem.taskExecution.terminate(); } else { // Try to find the real func process by port first, fall back to shell PID await killFuncProcessByPortOrPid(runningFuncTaskItem, workspaceFolder); diff --git a/src/tree/localProject/LocalProjectTreeItem.ts b/src/tree/localProject/LocalProjectTreeItem.ts index ab0f4c35e..d9dcaeb0b 100644 --- a/src/tree/localProject/LocalProjectTreeItem.ts +++ b/src/tree/localProject/LocalProjectTreeItem.ts @@ -58,8 +58,9 @@ export class LocalProjectTreeItem extends LocalProjectTreeItemBase implements Di this._disposables.push(createRefreshFileWatcher(this, path.join(this.effectiveProjectPath, '*', functionJsonFileName))); this._disposables.push(createRefreshFileWatcher(this, path.join(this.effectiveProjectPath, localSettingsFileName))); - this._disposables.push(onFuncTaskStarted(async scope => this.onFuncTaskChanged(scope))); - this._disposables.push(onDotnetFuncTaskReady(async scope => this.onFuncTaskChanged(scope))); + this._disposables.push(onFuncTaskStarted(async event => this.onFuncTaskChanged(event))); + // this._disposables.push(onFuncTaskStarted(async scope => this.onFuncTaskChanged(scope))); + this._disposables.push(onDotnetFuncTaskReady(async scope => this.onFuncTaskChanged({ scope }))); this._localFunctionsTreeItem = new LocalFunctionsTreeItem(this); this._localSettingsTreeItem = new AppSettingsTreeItem(this, new LocalSettingsClientProvider(this.workspaceFolder), ext.prefix, { @@ -123,9 +124,9 @@ export class LocalProjectTreeItem extends LocalProjectTreeItemBase implements Di await this.project.setApplicationSetting(context, key, value); } - private async onFuncTaskChanged(scope: WorkspaceFolder | TaskScope | undefined): Promise { + private async onFuncTaskChanged(event: { scope: WorkspaceFolder | TaskScope | undefined }): Promise { await callWithTelemetryAndErrorHandling('onFuncTaskChanged', async (context: IActionContext) => { - if (this.workspaceFolder === scope) { + if (this.workspaceFolder === event.scope) { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; await this.refresh(context);