-
Notifications
You must be signed in to change notification settings - Fork 146
Implementation for debug-isolated flag and streaming func CLI output #4765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
6e5b61c
e9c255d
067e04d
d046ac9
606f130
44e8382
9726543
d4f3b56
c7618c7
b8dd0bf
d673a73
2dfd68d
5035c9a
8a751c8
e5adbb9
6454ca8
544cd45
52780f7
3dc0fc5
e36604c
5bfbb42
a9c1f08
e1ee414
bc365fa
5d5b38f
192b107
577a57d
e8b519d
e0e5c03
8786288
adc4bc4
df16af2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,16 +20,25 @@ import { getWorkspaceSetting } from '../vsCodeConfig/settings'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const funcTaskReadyEmitter = new vscode.EventEmitter<vscode.WorkspaceFolder>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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<string> | undefined }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stream: AsyncIterable<string> | 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,34 +150,48 @@ 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) && funcTask.name.includes(enableJsonOutput); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (Date.now() < maxTime) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (taskError !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw taskError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 setEventPidByJsonOutput(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 setEventPidByJsonOutput(taskInfo: IRunningFuncTask): Promise<number | undefined> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+216
to
+221
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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]); | |
| } | |
| } | |
| let obj: unknown; | |
| try { | |
| obj = JSON.parse(chunk); | |
| } catch { | |
| continue; // Not valid JSON, skip this chunk | |
| } | |
| if ( | |
| typeof obj === 'object' && | |
| obj !== null && | |
| (obj as any).name === "dotnet-worker-startup" && | |
| typeof (obj as any).workerProcessId === "number" | |
| ) { | |
| return (obj as any).workerProcessId; | |
| } |
Copilot
AI
Oct 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The for await loop will consume the entire stream and never exit if the expected JSON output is not found, causing the function to hang indefinitely. The stream reading should have a timeout or break condition, and the loop at line 156 in startFuncTask should handle this case to avoid waiting forever.
| 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; | |
| const TIMEOUT_MS = 10000; // 10 seconds | |
| let timeoutHandle: NodeJS.Timeout; | |
| return await Promise.race([ | |
| (async () => { | |
| 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) { | |
| clearTimeout(timeoutHandle); | |
| return Number(matches[1]); | |
| } | |
| } | |
| } | |
| return undefined; | |
| })(), | |
| new Promise<number | undefined>(resolve => { | |
| timeoutHandle = setTimeout(() => { | |
| resolve(undefined); | |
| }, TIMEOUT_MS); | |
| }) | |
| ]); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Task> { | ||
| const funcCliPath = await getFuncCliPath(context, folder); | ||
| const args = (definition?.args || []) as string[]; | ||
| if (args.length > 0) { | ||
| command = `${command} ${args.join(' ')}`; | ||
| } | ||
|
Comment on lines
+107
to
+110
|
||
|
|
||
| let commandLine: string = `${funcCliPath} ${command}`; | ||
| if (language === ProjectLanguage.Python) { | ||
| commandLine = venvUtils.convertToVenvCommand(commandLine, folder.uri.fsPath); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,10 @@ 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<string> | undefined; | ||
| } | ||
|
|
||
| interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { | ||
|
|
@@ -86,13 +90,32 @@ export function isFuncHostTask(task: vscode.Task): boolean { | |
| 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) => { | ||
| 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 }; | ||
| const runningFuncTask = { | ||
| processId: e.processId, | ||
| taskExecution: e.execution, | ||
| portNumber, | ||
| terminalEventReader, | ||
| stream: latestTerminalShellExecutionEvent?.execution.read() | ||
|
||
| }; | ||
|
|
||
| runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); | ||
| funcTaskStartedEmitter.fire(e.execution.task.scope); | ||
| } | ||
|
|
@@ -102,6 +125,10 @@ 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); | ||
| } | ||
| }); | ||
|
|
@@ -146,7 +173,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); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.