-
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 8 commits
6e5b61c
e9c255d
067e04d
d046ac9
606f130
44e8382
9726543
d4f3b56
c7618c7
b8dd0bf
d673a73
2dfd68d
5035c9a
8a751c8
e5adbb9
6454ca8
544cd45
52780f7
3dc0fc5
e36604c
5bfbb42
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 |
|---|---|---|
|
|
@@ -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<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 +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,34 +147,54 @@ 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; | ||
|
|
||
| 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 (!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 +209,23 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo | |
| } | ||
| } | ||
|
|
||
| async function setEventPidByJsonOutput(taskInfo: IRunningFuncTask, taskName: string): Promise<vscode.Disposable> { | ||
| 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 }; | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,12 +11,15 @@ 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; | ||
| } | ||
|
|
||
| interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration { | ||
|
|
@@ -92,7 +95,16 @@ 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 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); | ||
| } | ||
| }); | ||
|
||
|
|
||
| const runningFuncTask = { processId: e.processId, taskExecution: e.execution, portNumber, streamHandler, outputReader }; | ||
| runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask); | ||
| funcTaskStartedEmitter.fire(e.execution.task.scope); | ||
| } | ||
|
|
@@ -146,10 +158,12 @@ 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(); | ||
| runningFuncTaskItem.outputReader.dispose(); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * 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<string>; | ||
| write: (chunk: string) => void; | ||
| end: () => void; | ||
| }; | ||
|
|
||
| export function createAsyncStringStream(): AsyncStreamHandler { | ||
| const queue: (string | null)[] = []; | ||
| let resolveNext: ((result: IteratorResult<string>) => void) | null = null; | ||
| let done = false; | ||
|
|
||
| const stream: AsyncIterable<string> = { | ||
| [Symbol.asyncIterator](): AsyncIterator<string> { | ||
| return { | ||
| next() { | ||
| return new Promise<IteratorResult<string>>(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 }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TerminalDataWriteEvent>; | ||
| } | ||
| } |
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.
Checking for command-line flags in the task name is fragile and unclear. Task names are meant for display purposes. Consider checking the actual command line or task definition properties instead.