From 6e5b61cc5a000f275b923252e10fbaec553cd248 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 24 Oct 2025 10:55:11 -0700 Subject: [PATCH 01/26] Implementation for debug-isolated flag and streaming func CLI output --- package.json | 11 ++- src/commands/pickFuncProcess.ts | 98 ++++++++++++++++----- src/debug/FuncTaskProvider.ts | 5 ++ src/funcCoreTools/funcHostTask.ts | 8 +- src/utils/stream.ts | 56 ++++++++++++ vscode.proposed.terminalDataWriteEvent.d.ts | 29 ++++++ 6 files changed, 183 insertions(+), 24 deletions(-) create mode 100644 src/utils/stream.ts create mode 100644 vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index ffa8b5fcd..a3a515663 100644 --- a/package.json +++ b/package.json @@ -985,6 +985,12 @@ "properties": { "command": { "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } } } } @@ -1504,5 +1510,8 @@ "runWizardCommandWithoutExecutionCommandId": "azureFunctions.agent.runWizardCommandWithoutExecution", "runWizardCommandWithInputsCommandId": "azureFunctions.agent.runWizardCommandWithInputs", "getAgentBenchmarkConfigsCommandId": "azureFunctions.agent.getAgentBenchmarkConfigs" - } + }, + "enabledApiProposals": [ + "terminalDataWriteEvent" + ] } diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 9fe2417d9..088827be5 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -25,11 +25,17 @@ 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 +72,7 @@ export async function startFuncProcessFromApi( const taskInfo = await startFuncTask(context, workspaceFolder, buildPath, funcTask); result.processId = await pickChildProcess(taskInfo); result.success = true; + result.stream = taskInfo.streamHandler.stream; } catch (err) { const pError = parseError(err); result.error = pError.message; @@ -140,6 +147,11 @@ 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 debugModeOn = funcTask.name.includes('--dotnet-isolated-debug') && funcTask.name.includes('--enable-json-output'); + let eventDisposable: vscode.Disposable | undefined; + let parentPid: number | undefined; + let asyncStreamIsSet: boolean = false; + while (Date.now() < maxTime) { if (taskError !== undefined) { throw taskError; @@ -147,27 +159,54 @@ 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; + // set up the stream on first try to capture terminal output + if (!asyncStreamIsSet) { + vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { + const terminal = vscode.window.terminals.find(t => funcTask.name === t.name); + if (event.terminal === terminal) { + taskInfo.streamHandler.write(event.data); + } + }); + asyncStreamIsSet = true; + } + + if (debugModeOn) { + // if we are in dotnet isolated debug mode, we need to find the pid from the terminal output + if (!eventDisposable) { + // preserve the old pid to detect changes + parentPid = taskInfo.processId; + eventDisposable = await setEventPidByJsonOutput(taskInfo, funcTask.name); } - 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; + // if we are starting a dotnet isolated func host with json output enabled, we can find the pid directly from the output + if (taskInfo.processId !== parentPid) { + // we have to wait for the process id to be set from the terminal output + 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 +221,23 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo } } +async function setEventPidByJsonOutput(taskInfo: IRunningFuncTask, taskName: string): Promise { + const setPidByJsonOutputListener = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { + const terminal = vscode.window.terminals.find(t => taskName === t.name); + if (event.terminal === terminal) { + if (event.data.includes(`{ "name":"dotnet-worker-startup", "workerProcessId" :`)) { + const matches = event.data.match(/"workerProcessId"\s*:\s*(\d+)/); + if (matches && matches.length > 1) { + taskInfo.processId = Number(matches[1]); + setPidByJsonOutputListener.dispose(); + } + } + } + }); + + return setPidByJsonOutputListener; +} + type OSAgnosticProcess = { command: string | undefined; pid: number | string }; /** diff --git a/src/debug/FuncTaskProvider.ts b/src/debug/FuncTaskProvider.ts index e583003e9..a380d605a 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) { + command = `${command} ${args.join(' ')}`; + } + let commandLine: string = `${funcCliPath} ${command}`; if (language === ProjectLanguage.Python) { commandLine = venvUtils.convertToVenvCommand(commandLine, folder.uri.fsPath); diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 8ccdf5d00..eb5fd99c5 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -11,12 +11,14 @@ import { localSettingsFileName } from '../constants'; import { getLocalSettingsJson } from '../funcConfig/local.settings'; import { localize } from '../localize'; import { cpUtils } from '../utils/cpUtils'; +import { createAsyncStringStream, type AsyncStreamHandler } from '../utils/stream'; import { getWorkspaceSetting } from '../vsCodeConfig/settings'; export interface IRunningFuncTask { taskExecution: vscode.TaskExecution; processId: number; portNumber: string; + streamHandler: AsyncStreamHandler; } interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { @@ -92,7 +94,8 @@ export function registerFuncHostTaskEvents(): void { 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 streamHandler = createAsyncStringStream(); + const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler }; runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); funcTaskStartedEmitter.fire(e.execution.task.scope); } @@ -146,10 +149,11 @@ 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); + runningFuncTaskItem.streamHandler.end(); } } diff --git a/src/utils/stream.ts b/src/utils/stream.ts new file mode 100644 index 000000000..f02386efd --- /dev/null +++ b/src/utils/stream.ts @@ -0,0 +1,56 @@ +export type AsyncStreamHandler = { + stream: AsyncIterable; + write: (chunk: string) => void; + end: () => void; +}; + +export function createAsyncStringStream(): AsyncStreamHandler { + const queue: (string | null)[] = []; + let resolveNext: ((result: IteratorResult) => void) | null = null; + let done = false; + + const stream: AsyncIterable = { + [Symbol.asyncIterator](): AsyncIterator { + return { + next() { + return new Promise>(resolve => { + if (queue.length > 0) { + const value = queue.shift(); + if (value === null) { + resolve({ value: undefined, done: true }); + } else { + resolve({ value: value as string, done: false }); + } + } else if (done) { + resolve({ value: undefined, done: true }); + } else { + resolveNext = resolve; + } + }); + } + }; + } + }; + + function write(chunk: string) { + if (done) throw new Error("Cannot write to a ended stream"); + if (resolveNext) { + resolveNext({ value: chunk, done: false }); + resolveNext = null; + } else { + queue.push(chunk); + } + } + + function end() { + done = true; + if (resolveNext) { + resolveNext({ value: undefined, done: true }); + resolveNext = null; + } else { + queue.push(null); // sentinel for end + } + } + + return { stream, write, end }; +} diff --git a/vscode.proposed.terminalDataWriteEvent.d.ts b/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 000000000..c9c4c0e99 --- /dev/null +++ b/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/78502 + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event; + } +} From e9c255d71ed6bd70c3926b04ac5a71de24851131 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 24 Oct 2025 11:09:30 -0700 Subject: [PATCH 02/26] Update src/utils/stream.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/utils/stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/stream.ts b/src/utils/stream.ts index f02386efd..0678c68f0 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -33,7 +33,7 @@ export function createAsyncStringStream(): AsyncStreamHandler { }; function write(chunk: string) { - if (done) throw new Error("Cannot write to a ended stream"); + if (done) throw new Error("Cannot write to an ended stream"); if (resolveNext) { resolveNext({ value: chunk, done: false }); resolveNext = null; From 067e04d2742254db6416ad1b5595b960c4ff5883 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 24 Oct 2025 11:13:59 -0700 Subject: [PATCH 03/26] Address copilot feedback --- src/commands/pickFuncProcess.ts | 6 +++++- src/utils/stream.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 088827be5..3ebbd66b5 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -161,11 +161,15 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo if (taskInfo) { // set up the stream on first try to capture terminal output if (!asyncStreamIsSet) { - vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { + const outputReader = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { const terminal = vscode.window.terminals.find(t => funcTask.name === t.name); if (event.terminal === terminal) { taskInfo.streamHandler.write(event.data); } + + if (taskInfo.streamHandler.done) { + outputReader.dispose(); + } }); asyncStreamIsSet = true; } diff --git a/src/utils/stream.ts b/src/utils/stream.ts index f02386efd..cf65558b3 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -2,6 +2,7 @@ export type AsyncStreamHandler = { stream: AsyncIterable; write: (chunk: string) => void; end: () => void; + done: boolean; }; export function createAsyncStringStream(): AsyncStreamHandler { @@ -52,5 +53,5 @@ export function createAsyncStringStream(): AsyncStreamHandler { } } - return { stream, write, end }; + return { stream, write, end, done }; } From 606f1300a844d5e36e6dd784113038a09df199bc Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 24 Oct 2025 11:51:39 -0700 Subject: [PATCH 04/26] Refactor to set up stream in funcHostTasks --- src/commands/pickFuncProcess.ts | 16 ---------------- src/funcCoreTools/funcHostTask.ts | 21 ++++++++++++++++++++- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 3ebbd66b5..b12e8f212 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -150,7 +150,6 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo const debugModeOn = funcTask.name.includes('--dotnet-isolated-debug') && funcTask.name.includes('--enable-json-output'); let eventDisposable: vscode.Disposable | undefined; let parentPid: number | undefined; - let asyncStreamIsSet: boolean = false; while (Date.now() < maxTime) { if (taskError !== undefined) { @@ -159,21 +158,6 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo const taskInfo: IRunningFuncTask | undefined = runningFuncTaskMap.get(workspaceFolder, buildPath); if (taskInfo) { - // set up the stream on first try to capture terminal output - if (!asyncStreamIsSet) { - const outputReader = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { - const terminal = vscode.window.terminals.find(t => funcTask.name === t.name); - if (event.terminal === terminal) { - taskInfo.streamHandler.write(event.data); - } - - if (taskInfo.streamHandler.done) { - outputReader.dispose(); - } - }); - asyncStreamIsSet = true; - } - if (debugModeOn) { // if we are in dotnet isolated debug mode, we need to find the pid from the terminal output if (!eventDisposable) { diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index eb5fd99c5..e4c8db543 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -19,6 +19,7 @@ export interface IRunningFuncTask { processId: number; portNumber: string; streamHandler: AsyncStreamHandler; + outputReader: vscode.Disposable; } interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { @@ -95,7 +96,19 @@ export function registerFuncHostTaskEvents(): void { if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { const portNumber = await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope); const streamHandler = createAsyncStringStream(); - const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler }; + const terminalName = e.execution.task.name; + const outputReader = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { + const terminal = vscode.window.terminals.find(t => terminalName === t.name); + if (event.terminal === terminal) { + runningFuncTask.streamHandler.write(event.data); + } + + if (runningFuncTask.streamHandler.done) { + outputReader.dispose(); + } + }); + + const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler, outputReader }; runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); funcTaskStartedEmitter.fire(e.execution.task.scope); } @@ -154,6 +167,12 @@ export async function stopFuncTaskIfRunning(workspaceFolder: vscode.WorkspaceFol // Try to find the real func process by port first, fall back to shell PID await killFuncProcessByPortOrPid(runningFuncTaskItem, workspaceFolder); runningFuncTaskItem.streamHandler.end(); + runningFuncTaskItem.outputReader.dispose(); + } + + for await (const chunk of runningFuncTaskItem.streamHandler.stream) { + // Process each chunk of the stream + console.log(chunk); } } From 44e8382e009f3ba64cbb301414a24d7dd4b5a69f Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 24 Oct 2025 11:52:39 -0700 Subject: [PATCH 05/26] Delete test snippet --- src/funcCoreTools/funcHostTask.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index e4c8db543..73f5991c7 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -169,11 +169,6 @@ export async function stopFuncTaskIfRunning(workspaceFolder: vscode.WorkspaceFol runningFuncTaskItem.streamHandler.end(); runningFuncTaskItem.outputReader.dispose(); } - - for await (const chunk of runningFuncTaskItem.streamHandler.stream) { - // Process each chunk of the stream - console.log(chunk); - } } if (buildPath) { From 972654381465e81139c8539907e74b23ef240340 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 24 Oct 2025 11:53:17 -0700 Subject: [PATCH 06/26] Add headerto stream file --- src/utils/stream.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/stream.ts b/src/utils/stream.ts index cd53d9932..1f7d39d7b 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -1,8 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + export type AsyncStreamHandler = { stream: AsyncIterable; write: (chunk: string) => void; end: () => void; - done: boolean; }; export function createAsyncStringStream(): AsyncStreamHandler { @@ -53,5 +57,5 @@ export function createAsyncStringStream(): AsyncStreamHandler { } } - return { stream, write, end, done }; + return { stream, write, end }; } From d4f3b56a0bb232eb032c8cf599916363166eccbc Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 24 Oct 2025 11:57:02 -0700 Subject: [PATCH 07/26] Remove the done call --- src/funcCoreTools/funcHostTask.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 73f5991c7..b8d0d10bb 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -102,10 +102,6 @@ export function registerFuncHostTaskEvents(): void { if (event.terminal === terminal) { runningFuncTask.streamHandler.write(event.data); } - - if (runningFuncTask.streamHandler.done) { - outputReader.dispose(); - } }); const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler, outputReader }; From c7618c76513d15a105e0f8fe05e3f6e520dbb818 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 24 Oct 2025 12:05:34 -0700 Subject: [PATCH 08/26] Update src/debug/FuncTaskProvider.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/debug/FuncTaskProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/debug/FuncTaskProvider.ts b/src/debug/FuncTaskProvider.ts index a380d605a..ca24bfe81 100644 --- a/src/debug/FuncTaskProvider.ts +++ b/src/debug/FuncTaskProvider.ts @@ -105,7 +105,7 @@ 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) { + if (args.length > 0) { command = `${command} ${args.join(' ')}`; } From b8dd0bf9c179f62c5a84f669a63f19ef38b1909a Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Wed, 29 Oct 2025 13:31:09 -0700 Subject: [PATCH 09/26] WIP waiting for VSCode insiders fix --- src/funcCoreTools/funcHostTask.ts | 41 ++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index b8d0d10bb..d9df35da1 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -89,21 +89,56 @@ export function isFuncHostTask(task: vscode.Task): boolean { return /func (host )?start/i.test(commandLine || ''); } +export function isFuncHostTerminalShell(shell: vscode.TerminalShellExecutionStartEvent): boolean { + const commandLine: string | undefined = shell.execution && shell.execution.commandLine.value; + return /func (host )?start/i.test(commandLine || ''); +} + +const streamHandlerMap: Map = new Map(); export function registerFuncHostTaskEvents(): void { registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { + const startHandler = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => { + console.log(`Terminal name: ${terminalShellExecEvent.terminal.name}`); + console.log(`Task name: ${terminalShellExecEvent.execution.commandLine.value}`); + console.log(`Process ID: ${e.processId}`); + console.log(`Terminal PID: ${await terminalShellExecEvent.terminal.processId}`); + if (isFuncHostTerminalShell(terminalShellExecEvent)) { + if (!streamHandlerMap.has(e.processId.toString())) { + // only set it up the first time we are seeing this pid + const streamHandler = createAsyncStringStream(); + streamHandler.stream = terminalShellExecEvent.execution.read(); + streamHandlerMap.set(e.processId.toString(), streamHandler); + } + } + }); + 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 streamHandler = createAsyncStringStream(); const terminalName = e.execution.task.name; + // const terminal = vscode.window.terminals.find(t => terminalName === t.name); + const portNumber = await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope); + const endHandler = vscode.window.onDidEndTerminalShellExecution(async (terminalShellExecEvent) => { + // reserved for closing the event handlers + if (isFuncHostTerminalShell(terminalShellExecEvent)) { + for await (const chunk of runningFuncTask.streamHandler.stream) { + console.log(chunk); + } + + startHandler.dispose(); + endHandler.dispose(); + } + }); + const outputReader = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { const terminal = vscode.window.terminals.find(t => terminalName === t.name); if (event.terminal === terminal) { - runningFuncTask.streamHandler.write(event.data); + // runningFuncTask.streamHandler.write(event.data); } }); + const streamHandler = streamHandlerMap.get(e.processId.toString()) || createAsyncStringStream(); const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler, outputReader }; runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); funcTaskStartedEmitter.fire(e.execution.task.scope); From d673a73800ad692edbecdb604d8385ad6ee27549 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:24:48 -0700 Subject: [PATCH 10/26] Use onDidStartTerminalShellExecution API instead of proposed --- src/commands/pickFuncProcess.ts | 43 +++++++++------------ src/funcCoreTools/funcHostTask.ts | 62 ++++++++++--------------------- src/utils/stream.ts | 61 ------------------------------ 3 files changed, 37 insertions(+), 129 deletions(-) delete mode 100644 src/utils/stream.ts diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index b12e8f212..54834e395 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -72,7 +72,7 @@ export async function startFuncProcessFromApi( const taskInfo = await startFuncTask(context, workspaceFolder, buildPath, funcTask); result.processId = await pickChildProcess(taskInfo); result.success = true; - result.stream = taskInfo.streamHandler.stream; + result.stream = taskInfo.stream; } catch (err) { const pError = parseError(err); result.error = pError.message; @@ -148,8 +148,6 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo let statusRequestTimeout: number = intervalMs; const maxTime: number = Date.now() + timeoutInSeconds * 1000; const debugModeOn = funcTask.name.includes('--dotnet-isolated-debug') && funcTask.name.includes('--enable-json-output'); - let eventDisposable: vscode.Disposable | undefined; - let parentPid: number | undefined; while (Date.now() < maxTime) { if (taskError !== undefined) { @@ -160,15 +158,10 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo if (taskInfo) { if (debugModeOn) { // if we are in dotnet isolated debug mode, we need to find the pid from the terminal output - if (!eventDisposable) { - // preserve the old pid to detect changes - parentPid = taskInfo.processId; - eventDisposable = await setEventPidByJsonOutput(taskInfo, funcTask.name); - } - - // if we are starting a dotnet isolated func host with json output enabled, we can find the pid directly from the output - if (taskInfo.processId !== parentPid) { - // we have to wait for the process id to be set from the terminal output + // if there is no pid yet, keep waiting + const newPid = await setEventPidByJsonOutput(taskInfo); + if (newPid) { + taskInfo.processId = newPid; return taskInfo; } } else { @@ -209,21 +202,21 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo } } -async function setEventPidByJsonOutput(taskInfo: IRunningFuncTask, taskName: string): Promise { - const setPidByJsonOutputListener = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { - const terminal = vscode.window.terminals.find(t => taskName === t.name); - if (event.terminal === terminal) { - if (event.data.includes(`{ "name":"dotnet-worker-startup", "workerProcessId" :`)) { - const matches = event.data.match(/"workerProcessId"\s*:\s*(\d+)/); - if (matches && matches.length > 1) { - taskInfo.processId = Number(matches[1]); - setPidByJsonOutputListener.dispose(); - } +async function setEventPidByJsonOutput(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 setPidByJsonOutputListener; + } + return; } type OSAgnosticProcess = { command: string | undefined; pid: number | string }; diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index d9df35da1..5d712ad37 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -11,15 +11,16 @@ import { localSettingsFileName } from '../constants'; import { getLocalSettingsJson } from '../funcConfig/local.settings'; import { localize } from '../localize'; import { cpUtils } from '../utils/cpUtils'; -import { createAsyncStringStream, type AsyncStreamHandler } from '../utils/stream'; import { getWorkspaceSetting } from '../vsCodeConfig/settings'; export interface IRunningFuncTask { taskExecution: vscode.TaskExecution; processId: number; portNumber: string; - streamHandler: AsyncStreamHandler; - outputReader: vscode.Disposable; + // there is always an event handler listening to `onDidStartTerminalShellExecution` when a func task starts to populate stream + terminalEventReader: vscode.Disposable; + // stream for reading `func host start` output + stream: AsyncIterable | undefined; } interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { @@ -93,62 +94,39 @@ export function isFuncHostTerminalShell(shell: vscode.TerminalShellExecutionStar const commandLine: string | undefined = shell.execution && shell.execution.commandLine.value; return /func (host )?start/i.test(commandLine || ''); } - -const streamHandlerMap: Map = new Map(); +let latestTerminalShellExecutionEvent: vscode.TerminalShellExecutionStartEvent | undefined; export function registerFuncHostTaskEvents(): void { registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { - const startHandler = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => { - console.log(`Terminal name: ${terminalShellExecEvent.terminal.name}`); - console.log(`Task name: ${terminalShellExecEvent.execution.commandLine.value}`); - console.log(`Process ID: ${e.processId}`); - console.log(`Terminal PID: ${await terminalShellExecEvent.terminal.processId}`); - if (isFuncHostTerminalShell(terminalShellExecEvent)) { - if (!streamHandlerMap.has(e.processId.toString())) { - // only set it up the first time we are seeing this pid - const streamHandler = createAsyncStringStream(); - streamHandler.stream = terminalShellExecEvent.execution.read(); - streamHandlerMap.set(e.processId.toString(), streamHandler); - } - } + const terminalEventReader = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => { + latestTerminalShellExecutionEvent = terminalShellExecEvent; }); context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { - const terminalName = e.execution.task.name; - // const terminal = vscode.window.terminals.find(t => terminalName === t.name); const portNumber = await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope); - const endHandler = vscode.window.onDidEndTerminalShellExecution(async (terminalShellExecEvent) => { - // reserved for closing the event handlers - if (isFuncHostTerminalShell(terminalShellExecEvent)) { - for await (const chunk of runningFuncTask.streamHandler.stream) { - console.log(chunk); - } + const runningFuncTask = { + processId: e.processId, + taskExecution: e.execution, + portNumber, + terminalEventReader, + stream: latestTerminalShellExecutionEvent?.execution.read() + }; - startHandler.dispose(); - endHandler.dispose(); - } - }); - - const outputReader = vscode.window.onDidWriteTerminalData(async (event: vscode.TerminalDataWriteEvent) => { - const terminal = vscode.window.terminals.find(t => terminalName === t.name); - if (event.terminal === terminal) { - // runningFuncTask.streamHandler.write(event.data); - } - }); - - const streamHandler = streamHandlerMap.get(e.processId.toString()) || createAsyncStringStream(); - const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler, outputReader }; runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); funcTaskStartedEmitter.fire(e.execution.task.scope); } }); - 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 runningFuncTask = runningFuncTaskMap.get(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); + if (runningFuncTask) { + runningFuncTask.terminalEventReader.dispose(); + } runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); } }); @@ -197,8 +175,6 @@ export async function stopFuncTaskIfRunning(workspaceFolder: vscode.WorkspaceFol } else { // Try to find the real func process by port first, fall back to shell PID await killFuncProcessByPortOrPid(runningFuncTaskItem, workspaceFolder); - runningFuncTaskItem.streamHandler.end(); - runningFuncTaskItem.outputReader.dispose(); } } diff --git a/src/utils/stream.ts b/src/utils/stream.ts deleted file mode 100644 index 1f7d39d7b..000000000 --- a/src/utils/stream.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export type AsyncStreamHandler = { - stream: AsyncIterable; - write: (chunk: string) => void; - end: () => void; -}; - -export function createAsyncStringStream(): AsyncStreamHandler { - const queue: (string | null)[] = []; - let resolveNext: ((result: IteratorResult) => void) | null = null; - let done = false; - - const stream: AsyncIterable = { - [Symbol.asyncIterator](): AsyncIterator { - return { - next() { - return new Promise>(resolve => { - if (queue.length > 0) { - const value = queue.shift(); - if (value === null) { - resolve({ value: undefined, done: true }); - } else { - resolve({ value: value as string, done: false }); - } - } else if (done) { - resolve({ value: undefined, done: true }); - } else { - resolveNext = resolve; - } - }); - } - }; - } - }; - - function write(chunk: string) { - if (done) throw new Error("Cannot write to an ended stream"); - if (resolveNext) { - resolveNext({ value: chunk, done: false }); - resolveNext = null; - } else { - queue.push(chunk); - } - } - - function end() { - done = true; - if (resolveNext) { - resolveNext({ value: undefined, done: true }); - resolveNext = null; - } else { - queue.push(null); // sentinel for end - } - } - - return { stream, write, end }; -} From 2dfd68d32b2ee26e2bf6314eb5085566f15c233e Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:25:10 -0700 Subject: [PATCH 11/26] Remove proposed files --- package.json | 5 +--- vscode.proposed.terminalDataWriteEvent.d.ts | 29 --------------------- 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index a3a515663..ff10d81de 100644 --- a/package.json +++ b/package.json @@ -1510,8 +1510,5 @@ "runWizardCommandWithoutExecutionCommandId": "azureFunctions.agent.runWizardCommandWithoutExecution", "runWizardCommandWithInputsCommandId": "azureFunctions.agent.runWizardCommandWithInputs", "getAgentBenchmarkConfigsCommandId": "azureFunctions.agent.getAgentBenchmarkConfigs" - }, - "enabledApiProposals": [ - "terminalDataWriteEvent" - ] + } } diff --git a/vscode.proposed.terminalDataWriteEvent.d.ts b/vscode.proposed.terminalDataWriteEvent.d.ts deleted file mode 100644 index c9c4c0e99..000000000 --- a/vscode.proposed.terminalDataWriteEvent.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/78502 - - export interface TerminalDataWriteEvent { - /** - * The {@link Terminal} for which the data was written. - */ - readonly terminal: Terminal; - /** - * The data being written. - */ - readonly data: string; - } - - namespace window { - /** - * An event which fires when the terminal's child pseudo-device is written to (the shell). - * In other words, this provides access to the raw data stream from the process running - * within the terminal, including VT sequences. - */ - export const onDidWriteTerminalData: Event; - } -} From 8a751c8e2032648ee01beede4bb63eee9e86c0eb Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:34:07 -0700 Subject: [PATCH 12/26] Add note --- src/funcCoreTools/funcHostTask.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 5d712ad37..396a6bf85 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -98,6 +98,12 @@ let latestTerminalShellExecutionEvent: vscode.TerminalShellExecutionStartEvent | export function registerFuncHostTaskEvents(): void { registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { const terminalEventReader = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => { + /** + * NOTE: there is no reliable way to link a terminal to a task due to the name and PID not updating in real time, + * so just keep updating to the latest event since the func task and its dependencies run in the same + * terminal (the terminal that we want to output) + * New tasks will create new `terminalShellExecutionEvents`, so we don't need to worry about picking up output from other terminals + * */ latestTerminalShellExecutionEvent = terminalShellExecEvent; }); From e5adbb9bd2ef13ee6a995bf0bc6993e02c8e1bb4 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:35:56 -0700 Subject: [PATCH 13/26] Remove unused async moniker --- src/funcCoreTools/funcHostTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 396a6bf85..b59cacaef 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -137,7 +137,7 @@ export function registerFuncHostTaskEvents(): void { } }); - registerEvent('azureFunctions.onDidTerminateDebugSession', vscode.debug.onDidTerminateDebugSession, async (context: IActionContext, debugSession: vscode.DebugSession) => { + registerEvent('azureFunctions.onDidTerminateDebugSession', vscode.debug.onDidTerminateDebugSession, (context: IActionContext, debugSession: vscode.DebugSession) => { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; From 6454ca8d344bd9ada7d31150ef7427608de7c4fe Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:37:12 -0700 Subject: [PATCH 14/26] Whoops, wrong async --- src/funcCoreTools/funcHostTask.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index b59cacaef..015fd76be 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -125,7 +125,7 @@ export function registerFuncHostTaskEvents(): void { } }); - registerEvent('azureFunctions.onDidEndTask', vscode.tasks.onDidEndTaskProcess, async (context: IActionContext, e: vscode.TaskProcessEndEvent) => { + registerEvent('azureFunctions.onDidEndTask', vscode.tasks.onDidEndTaskProcess, (context: IActionContext, e: vscode.TaskProcessEndEvent) => { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { @@ -137,7 +137,7 @@ export function registerFuncHostTaskEvents(): void { } }); - registerEvent('azureFunctions.onDidTerminateDebugSession', vscode.debug.onDidTerminateDebugSession, (context: IActionContext, debugSession: vscode.DebugSession) => { + registerEvent('azureFunctions.onDidTerminateDebugSession', vscode.debug.onDidTerminateDebugSession, async (context: IActionContext, debugSession: vscode.DebugSession) => { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; From 544cd45f5a2827a3f9c602e17546e1bff682b641 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:47:07 -0700 Subject: [PATCH 15/26] Little bit of cleaning --- src/commands/pickFuncProcess.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 54834e395..83b7f79a4 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -20,6 +20,9 @@ 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, @@ -147,7 +150,8 @@ 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 debugModeOn = funcTask.name.includes('--dotnet-isolated-debug') && funcTask.name.includes('--enable-json-output'); + const funcShellExecution = funcTask.execution as vscode.ShellExecution; + const debugModeOn = funcShellExecution.commandLine?.includes(dotnetIsolatedDebugFlag) && funcTask.name.includes(enableJsonOutput); while (Date.now() < maxTime) { if (taskError !== undefined) { From 52780f727e746936087f0a6a2f8f4848796c3fab Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:50:22 -0700 Subject: [PATCH 16/26] Remove unusued function helper --- src/funcCoreTools/funcHostTask.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 015fd76be..63424676d 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -90,10 +90,6 @@ export function isFuncHostTask(task: vscode.Task): boolean { return /func (host )?start/i.test(commandLine || ''); } -export function isFuncHostTerminalShell(shell: vscode.TerminalShellExecutionStartEvent): boolean { - const commandLine: string | undefined = shell.execution && shell.execution.commandLine.value; - return /func (host )?start/i.test(commandLine || ''); -} let latestTerminalShellExecutionEvent: vscode.TerminalShellExecutionStartEvent | undefined; export function registerFuncHostTaskEvents(): void { registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { From 3dc0fc500a9b762fa626a6dc2eac552af17bdaf9 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 15:54:46 -0700 Subject: [PATCH 17/26] Last commit, I swears it --- src/commands/pickFuncProcess.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 83b7f79a4..8283c0b38 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -151,7 +151,7 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo let statusRequestTimeout: number = intervalMs; const maxTime: number = Date.now() + timeoutInSeconds * 1000; const funcShellExecution = funcTask.execution as vscode.ShellExecution; - const debugModeOn = funcShellExecution.commandLine?.includes(dotnetIsolatedDebugFlag) && funcTask.name.includes(enableJsonOutput); + const debugModeOn = funcShellExecution.commandLine?.includes(dotnetIsolatedDebugFlag) && funcShellExecution.commandLine?.includes(enableJsonOutput); while (Date.now() < maxTime) { if (taskError !== undefined) { From e36604c2eda7ef4aa6c0877b200fcb8b16e978de Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Mon, 3 Nov 2025 10:59:36 -0800 Subject: [PATCH 18/26] Move event handler, added note --- src/extension.ts | 3 ++- src/funcCoreTools/funcHostTask.ts | 32 +++++++++++++++---------------- 2 files changed, 17 insertions(+), 18 deletions(-) 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 63424676d..4c2ba218c 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -17,8 +17,6 @@ export interface IRunningFuncTask { taskExecution: vscode.TaskExecution; processId: number; portNumber: string; - // there is always an event handler listening to `onDidStartTerminalShellExecution` when a func task starts to populate stream - terminalEventReader: vscode.Disposable; // stream for reading `func host start` output stream: AsyncIterable | undefined; } @@ -91,28 +89,32 @@ export function isFuncHostTask(task: vscode.Task): boolean { } 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) => { + /** + * NOTE: there is no reliable way to link a terminal to a task due to the name and PID not updating in real time, + * so just keep updating to the latest event since the func task and its dependencies run in the same + * terminal (the terminal that we want to output) + * New tasks will create new `terminalShellExecutionEvents`, so we don't need to worry about picking up output from other terminals + * BUG: There's a current issue where if there is _only_ a func task in the tasks.json (as in it doesn't dependOn any other tasks), + * the onDidStartTerminalShellExecution does not fire at all. This should not impact most runtimes as they all have some sort of + * build task as a dependency. This is a bug on VS Code that I am working with Daniel to fix. + * */ + latestTerminalShellExecutionEvent = terminalShellExecEvent; + }); registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { - const terminalEventReader = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => { - /** - * NOTE: there is no reliable way to link a terminal to a task due to the name and PID not updating in real time, - * so just keep updating to the latest event since the func task and its dependencies run in the same - * terminal (the terminal that we want to output) - * New tasks will create new `terminalShellExecutionEvents`, so we don't need to worry about picking up output from other terminals - * */ - latestTerminalShellExecutionEvent = terminalShellExecEvent; - }); - 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, - terminalEventReader, stream: latestTerminalShellExecutionEvent?.execution.read() }; @@ -125,10 +127,6 @@ export function registerFuncHostTaskEvents(): void { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { - const runningFuncTask = runningFuncTaskMap.get(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); - if (runningFuncTask) { - runningFuncTask.terminalEventReader.dispose(); - } runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); } }); From 5bfbb42b54ced5dfcf06850784e17d59a3531db9 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Mon, 3 Nov 2025 11:03:54 -0800 Subject: [PATCH 19/26] Rename function due to PR feedback --- src/commands/pickFuncProcess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/pickFuncProcess.ts b/src/commands/pickFuncProcess.ts index 8283c0b38..b88cdaf68 100644 --- a/src/commands/pickFuncProcess.ts +++ b/src/commands/pickFuncProcess.ts @@ -163,7 +163,7 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo 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 setEventPidByJsonOutput(taskInfo); + const newPid = await getWorkerPidFromJsonOutput(taskInfo); if (newPid) { taskInfo.processId = newPid; return taskInfo; @@ -206,7 +206,7 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo } } -async function setEventPidByJsonOutput(taskInfo: IRunningFuncTask): Promise { +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; From bc365fa6b29ce1b8a7d9c76c7b2aaee33e621773 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 20 Nov 2025 16:36:40 -0800 Subject: [PATCH 20/26] Push up WIP --- .../debug/PostFuncDebugExecuteStep.ts | 48 +++++++++++++++++++ src/funcCoreTools/funcHostTask.ts | 38 ++++++++++----- 2 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 src/commands/debug/PostFuncDebugExecuteStep.ts diff --git a/src/commands/debug/PostFuncDebugExecuteStep.ts b/src/commands/debug/PostFuncDebugExecuteStep.ts new file mode 100644 index 000000000..af6a92ba5 --- /dev/null +++ b/src/commands/debug/PostFuncDebugExecuteStep.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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, type ExecuteActivityOutput, type IActionContext } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../localize"; + +export class PostFuncDebugExecuteStep extends AzureWizardExecuteStep { + public priority: number = 999; + public stepName: string = 'PostFuncDebugExecuteStep'; + + public async execute(_context: T): Promise { + // no-op + } + + public createSuccessOutput(context: T): ExecuteActivityOutput { + const terminateDebugSession: string = localize('connectMcpServer', 'Successfully terminated debug session.'); + return { + item: new ActivityChildItem({ + label: terminateDebugSession, + id: `${context.telemetry.properties.sessionId}-terminateDebugSession`, + activityType: ActivityChildType.Success, + contextValue: createContextValue([activityInfoContext, 'terminateDebugSession']), + // a little trick to remove the description timer on activity children + description: ' ' + }) + }; + } + + public createFailOutput(context: T): ExecuteActivityOutput { + const terminateDebugSession: string = localize('terminateDebugSessionFail', 'Failed to terminate debug session.'); + return { + item: new ActivityChildItem({ + label: terminateDebugSession, + id: `${context.telemetry.properties.sessionId}-terminateDebugSession-fail`, + activityType: ActivityChildType.Error, + contextValue: createContextValue([activityInfoContext, 'terminateDebugSessionFail']), + // a little trick to remove the description timer on activity children + description: ' ' + }) + }; + } + + public shouldExecute(context: T): boolean { + return true; + } +} diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 5673778c3..dbaae2806 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -3,13 +3,15 @@ * 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, registerEvent, type IActionContext } from '@microsoft/vscode-azext-utils'; import * as path from 'path'; import * as vscode from 'vscode'; 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'; @@ -83,26 +85,31 @@ 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) => { /** - * NOTE: there is no reliable way to link a terminal to a task due to the name and PID not updating in real time, - * so just keep updating to the latest event since the func task and its dependencies run in the same - * terminal (the terminal that we want to output) - * New tasks will create new `terminalShellExecutionEvents`, so we don't need to worry about picking up output from other terminals - * BUG: There's a current issue where if there is _only_ a func task in the tasks.json (as in it doesn't dependOn any other tasks), - * the onDidStartTerminalShellExecution does not fire at all. This should not impact most runtimes as they all have some sort of - * build task as a dependency. This is a bug on VS Code that I am working with Daniel to fix. + * This will pick up any terminal that starts a `func host start` command, 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; + if (isFuncShellEvent(terminalShellExecEvent)) { + latestTerminalShellExecutionEvent = terminalShellExecEvent; + } + }); registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => { context.errorHandling.suppressDisplay = true; @@ -111,7 +118,7 @@ export function registerFuncHostTaskEvents(): void { if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { const portNumber = await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope); - const runningFuncTask = { + const runningFuncTask: IRunningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, @@ -123,11 +130,18 @@ export function registerFuncHostTaskEvents(): void { } }); - 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)) { runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); + const wizardContext = Object.assign(context, await createActivityContext({ withChildren: true })); + const wizard = new AzureWizard(wizardContext, { + title: localize('funcTaskEnded', 'Function host task ended.'), + promptSteps: [], + executeSteps: [new PostFuncDebugExecuteStep()] + }); + await wizard.execute(); } }); From 5d5b38f661627ac656b7a9f1c449685552ef69e5 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 20 Nov 2025 16:37:05 -0800 Subject: [PATCH 21/26] Push up WIP --- src/funcCoreTools/funcHostTask.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index dbaae2806..c40b3bf89 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -134,6 +134,7 @@ export function registerFuncHostTaskEvents(): void { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { + runngFuncTask.map.get runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); const wizardContext = Object.assign(context, await createActivityContext({ withChildren: true })); const wizard = new AzureWizard(wizardContext, { From 577a57dcf50cdc9aa4045c33ef00e971c4660000 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Mon, 24 Nov 2025 19:18:06 -0800 Subject: [PATCH 22/26] WIP for postDebug copilot analysis --- package-lock.json | 23 ++++++- .../debug/PostFuncDebugExecuteStep.ts | 60 +++++++++++++++++++ src/funcCoreTools/funcHostTask.ts | 36 +++++++---- .../dotnet/executeDotnetTemplateCommand.ts | 2 +- src/tree/localProject/LocalProjectTreeItem.ts | 9 +-- 5 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 src/commands/debug/PostFuncDebugExecuteStep.ts diff --git a/package-lock.json b/package-lock.json index 4573244a7..37ee1b07d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1198,6 +1198,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" } @@ -1572,6 +1573,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", @@ -1861,7 +1863,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", @@ -1977,6 +1980,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", @@ -2011,6 +2015,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", @@ -2594,6 +2599,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" }, @@ -2648,6 +2654,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", @@ -3183,6 +3190,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3245,6 +3253,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" }, @@ -4548,6 +4557,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", @@ -4649,6 +4659,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", @@ -7652,6 +7663,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", @@ -10095,6 +10107,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", @@ -10402,7 +10415,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", @@ -10590,6 +10604,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10795,6 +10810,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" }, @@ -11056,6 +11072,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", @@ -11102,6 +11119,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", @@ -11203,6 +11221,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/src/commands/debug/PostFuncDebugExecuteStep.ts b/src/commands/debug/PostFuncDebugExecuteStep.ts new file mode 100644 index 000000000..fffa7ca7e --- /dev/null +++ b/src/commands/debug/PostFuncDebugExecuteStep.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from "@microsoft/vscode-azext-utils"; +import { stripVTControlCharacters } from "node:util"; + +export class PostFuncDebugExecuteStep extends AzureWizardExecuteStep { + public priority: number = 999; + public stepName: string = 'PostFuncDebugExecuteStep'; + + public constructor(readonly logs: string[]) { + super(); + } + + public async execute(context: T): Promise { + const errorLogs: string[] = []; + 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) { + this._logs = stripVTControlCharacters(errorLogs.join('\n')); + context.activityAttributes = context.activityAttributes || {}; + context.activityAttributes.logs = errorLogs.map(log => { return { content: stripVTControlCharacters(log) }; }); + throw new Error('Function host encountered errors during startup. See logs for details.'); + } + + return; + } + + public shouldExecute(_context: T): boolean { + return true; + } + + public createFailOutput(_context: T): ExecuteActivityOutput { + return { + item: new ActivityChildItem({ + label: 'Function host encountered errors during debugging. Click to have Copilot help diagnose the issue.', + id: `${randomUtils.getRandomHexString(8)}-terminateDebugSession-fail`, + activityType: ActivityChildType.Error, + contextValue: createContextValue([activityInfoContext, 'terminateDebugSessionFail']), + // a little trick to remove the description timer on activity children + description: ' ' + }) + }; + } +} diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 6228f4cdc..963b16c4a 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -21,6 +21,7 @@ export interface IRunningFuncTask { portNumber: string; // stream for reading `func host start` output stream: AsyncIterable | undefined; + logs: string[]; } interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { @@ -79,7 +80,7 @@ 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(); @@ -115,15 +116,17 @@ export function registerFuncHostTaskEvents(): void { if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { const portNumber = await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope); + const logs: string[] = []; const runningFuncTask: IRunningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, - stream: latestTerminalShellExecutionEvent?.execution.read() + 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 }); } }); @@ -131,20 +134,29 @@ export function registerFuncHostTaskEvents(): void { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { - const task = runningFuncTaskMap.get(e.execution.task.scope); - if (task && task.stream) { - for await (const streamLine of task.stream || []) { - console.log(streamLine); - } - } - runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); - const wizardContext = Object.assign(context, await createActivityContext({ withChildren: true })); + const task = runningFuncTaskMap.get(e.execution.task.scope!, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); + const wizardContext = Object.assign(context, await createActivityContext()); + wizardContext.activityTitle = localize('funcTaskEnded', 'Function host task ended.'); + const wizard = new AzureWizard(wizardContext, { title: localize('funcTaskEnded', 'Function host task ended.'), + promptSteps: [], - executeSteps: [new PostFuncDebugExecuteStep()] + executeSteps: [new PostFuncDebugExecuteStep(task?.logs ?? [])] }); await wizard.execute(); + 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); } }); diff --git a/src/templates/dotnet/executeDotnetTemplateCommand.ts b/src/templates/dotnet/executeDotnetTemplateCommand.ts index 5ce3495d3..8f025c702 100644 --- a/src/templates/dotnet/executeDotnetTemplateCommand.ts +++ b/src/templates/dotnet/executeDotnetTemplateCommand.ts @@ -62,7 +62,7 @@ async function getFramework(context: IActionContext, workingDirectory: string | } // Prioritize "LTS", then "Current", then "Preview" - const netVersions: string[] = ['6.0', '7.0', '8.0', '9.0']; + const netVersions: string[] = ['6.0', '7.0', '8.0', '9.0', '10.0']; const semVersions: SemVer[] = netVersions.map(v => semVerCoerce(v) as SemVer); let pickedVersion: SemVer | undefined; 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); From e8b519d3c310a7a4e4f77ab8f9659550844ad45d Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Tue, 25 Nov 2025 10:35:16 -0800 Subject: [PATCH 23/26] Add debug command activity --- src/commands/CommandAttributes.ts | 8 ++++++++ src/funcCoreTools/funcHostTask.ts | 2 ++ 2 files changed, 10 insertions(+) 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/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 963b16c4a..148fe67e3 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -6,6 +6,7 @@ import { AzureWizard, 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'; @@ -136,6 +137,7 @@ export function registerFuncHostTaskEvents(): void { if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { const task = runningFuncTaskMap.get(e.execution.task.scope!, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); const wizardContext = Object.assign(context, await createActivityContext()); + wizardContext.activityAttributes = CommandAttributes.Debug; wizardContext.activityTitle = localize('funcTaskEnded', 'Function host task ended.'); const wizard = new AzureWizard(wizardContext, { From e0e5c03fca649fc396054aded24dab03c5dc1e12 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Mon, 1 Dec 2025 09:44:44 -0800 Subject: [PATCH 24/26] WIP for copilot postdebug --- .../debug/PostFuncDebugExecuteStep.ts | 23 +++++++++++++------ src/funcCoreTools/funcHostTask.ts | 9 ++++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/commands/debug/PostFuncDebugExecuteStep.ts b/src/commands/debug/PostFuncDebugExecuteStep.ts index fffa7ca7e..f196ee3dd 100644 --- a/src/commands/debug/PostFuncDebugExecuteStep.ts +++ b/src/commands/debug/PostFuncDebugExecuteStep.ts @@ -3,12 +3,16 @@ * 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 } from "@microsoft/vscode-azext-utils"; +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 { +export class PostFuncDebugExecuteStep extends AzureWizardExecuteStep { public priority: number = 999; public stepName: string = 'PostFuncDebugExecuteStep'; + // public options: AzureWizardExecuteStepOptions = { + // continueOnFail: true + // } public constructor(readonly logs: string[]) { super(); @@ -32,10 +36,10 @@ export class PostFuncDebugExecuteStep extends } if (errorLogs.length > 0) { - this._logs = stripVTControlCharacters(errorLogs.join('\n')); context.activityAttributes = context.activityAttributes || {}; context.activityAttributes.logs = errorLogs.map(log => { return { content: stripVTControlCharacters(log) }; }); - throw new Error('Function host encountered errors during startup. See logs for details.'); + context.activityChildren = []; + throw new Error('This is from the error in execute'); } return; @@ -48,12 +52,17 @@ export class PostFuncDebugExecuteStep extends public createFailOutput(_context: T): ExecuteActivityOutput { return { item: new ActivityChildItem({ - label: 'Function host encountered errors during debugging. Click to have Copilot help diagnose the issue.', + label: 'Click to have Copilot help diagnose the issue.', id: `${randomUtils.getRandomHexString(8)}-terminateDebugSession-fail`, - activityType: ActivityChildType.Error, + activityType: ActivityChildType.Fail, contextValue: createContextValue([activityInfoContext, 'terminateDebugSessionFail']), + iconPath: new ThemeIcon('sparkle'), // a little trick to remove the description timer on activity children - description: ' ' + description: ' ', + command: { + "command": "azureResourceGroups.askAgentAboutActivityLogItem", + "title": "Ask Copilot", + } }) }; } diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 148fe67e3..70c5b51f8 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -136,7 +136,7 @@ export function registerFuncHostTaskEvents(): void { context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { const task = runningFuncTaskMap.get(e.execution.task.scope!, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); - const wizardContext = Object.assign(context, await createActivityContext()); + const wizardContext = Object.assign(context, await createActivityContext({ withChildren: true })); wizardContext.activityAttributes = CommandAttributes.Debug; wizardContext.activityTitle = localize('funcTaskEnded', 'Function host task ended.'); @@ -146,7 +146,12 @@ export function registerFuncHostTaskEvents(): void { promptSteps: [], executeSteps: [new PostFuncDebugExecuteStep(task?.logs ?? [])] }); - await wizard.execute(); + 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); } }); From adc4bc4f3369979408cf0ad0559d719c7c07947e Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Wed, 7 Jan 2026 15:47:18 -0800 Subject: [PATCH 25/26] Merge with main --- src/funcCoreTools/funcHostTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 70c5b51f8..7d642fc8e 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -87,7 +87,7 @@ export const onFuncTaskStarted = funcTaskStartedEmitter.event; export const buildPathToWorkspaceFolderMap = new Map(); const defaultFuncPort: string = '7071'; -const funcCommandRegex: RegExp = /(?:^|[\\/])(func(?:\.exe)?)\s+host\s+start$/i +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 funcCommandRegex.test(commandLine || ''); From df16af2153d681184b01a1bf29996a81d265d440 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Wed, 7 Jan 2026 15:58:45 -0800 Subject: [PATCH 26/26] Fix linter --- src/commands/debug/PostFuncDebugExecuteStep.ts | 1 + src/funcCoreTools/funcHostTask.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/debug/PostFuncDebugExecuteStep.ts b/src/commands/debug/PostFuncDebugExecuteStep.ts index f196ee3dd..cc31a945c 100644 --- a/src/commands/debug/PostFuncDebugExecuteStep.ts +++ b/src/commands/debug/PostFuncDebugExecuteStep.ts @@ -20,6 +20,7 @@ export class PostFuncDebugExecuteStep { 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, diff --git a/src/funcCoreTools/funcHostTask.ts b/src/funcCoreTools/funcHostTask.ts index 7d642fc8e..67c31ce45 100644 --- a/src/funcCoreTools/funcHostTask.ts +++ b/src/funcCoreTools/funcHostTask.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizard, 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'; @@ -135,7 +135,8 @@ export function registerFuncHostTaskEvents(): void { context.errorHandling.suppressDisplay = true; context.telemetry.suppressIfSuccessful = true; if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) { - const task = runningFuncTaskMap.get(e.execution.task.scope!, (e.execution.task.execution as vscode.ShellExecution).options?.cwd); + 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.');