Skip to content

Commit a479f07

Browse files
author
Nathan Turinski
committed
pull from remote
2 parents ac14622 + 2ebf2b6 commit a479f07

File tree

4 files changed

+78
-20
lines changed

4 files changed

+78
-20
lines changed

src/commands/pickFuncProcess.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,40 @@ import { getWorkspaceSetting } from '../vsCodeConfig/settings';
2121
const funcTaskReadyEmitter = new vscode.EventEmitter<vscode.WorkspaceFolder>();
2222
export const onDotnetFuncTaskReady = funcTaskReadyEmitter.event;
2323

24+
/**
25+
* Result returned from starting a function host process via the API.
26+
*/
27+
export interface IStartFuncProcessResult {
28+
/**
29+
* The process ID of the started function host.
30+
*/
31+
processId: string;
32+
/**
33+
* Whether the function host was successfully started.
34+
*/
35+
success: boolean;
36+
/**
37+
* Error message if the function host failed to start.
38+
*/
39+
error: string;
40+
/**
41+
* An async iterable stream of terminal output from the function host task.
42+
* This stream provides real-time access to the output of the `func host start` command,
43+
* allowing consumers to monitor host status, capture logs, and detect errors.
44+
*
45+
* The stream will be undefined if the host failed to start or if output streaming is not available.
46+
* Consumers should iterate over the stream asynchronously to read output lines as they are produced.
47+
* The stream remains active for the lifetime of the function host process.
48+
*/
49+
stream: AsyncIterable<string> | undefined;
50+
}
51+
2452
export async function startFuncProcessFromApi(
2553
buildPath: string,
2654
args: string[],
2755
env: { [key: string]: string }
28-
): Promise<{ processId: string; success: boolean; error: string, stream: AsyncIterable<string> | undefined }> {
29-
const result: {
30-
processId: string;
31-
success: boolean;
32-
error: string;
33-
stream: AsyncIterable<string> | undefined;
34-
} = {
56+
): Promise<IStartFuncProcessResult> {
57+
const result: IStartFuncProcessResult = {
3558
processId: '',
3659
success: false,
3760
error: '',

src/debug/FunctionHostDebugView.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
9292
return getHostErrorTreeItem(element);
9393
case 'hostTask':
9494
return getHostTaskTreeItem(element);
95-
default:
96-
// Exhaustive check
97-
return getNoHostTreeItem();
95+
default: {
96+
// Exhaustive check: if we reach here, the FuncHostDebugNode union is out of sync with this switch.
97+
throw new Error(`Unexpected FuncHostDebugNode kind: ${(element as { kind?: unknown }).kind}`);
98+
}
9899
}
99100
}
100101

src/funcCoreTools/funcHostErrorUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface FuncHostErrorContextOptions {
2121
}
2222

2323
// eslint-disable-next-line no-control-regex
24-
const redAnsiRegex = /\u001b\[(?:[0-9;]*31m|[0-9;]*91m|38;5;(9|1)m)/;
24+
const redAnsiRegex = /\u001b\[(?:[0-9;]*31m|[0-9;]*91m)/;
2525

2626
export function isFuncHostErrorLog(log: string): boolean {
2727
return redAnsiRegex.test(log);

src/funcCoreTools/funcHostTask.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export interface IRunningFuncTask {
3333
* This avoids repeatedly stealing focus / opening the view for every subsequent error.
3434
*/
3535
hasReportedLiveErrors?: boolean;
36+
/**
37+
* AbortController used to signal when the stream iteration should stop.
38+
* This prevents the async iteration loop from hanging indefinitely when the task ends.
39+
*/
40+
streamAbortController?: AbortController;
3641
}
3742

3843
function addErrorLog(task: IRunningFuncTask, rawChunk: string): void {
@@ -189,6 +194,7 @@ export function registerFuncHostTaskEvents(): void {
189194
logs,
190195
errorLogs: [],
191196
hasReportedLiveErrors: false,
197+
streamAbortController: new AbortController(),
192198
};
193199

194200
runningFuncTaskMap.set(e.execution.task.scope, runningFuncTask);
@@ -212,6 +218,13 @@ export function registerFuncHostTaskEvents(): void {
212218
context.errorHandling.suppressDisplay = true;
213219
context.telemetry.suppressIfSuccessful = true;
214220
if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) {
221+
const task = runningFuncTaskMap.get(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd);
222+
223+
// Abort the stream iteration to prevent it from hanging indefinitely
224+
if (task?.streamAbortController) {
225+
task.streamAbortController.abort();
226+
}
227+
215228
runningFuncTaskMap.delete(e.execution.task.scope, (e.execution.task.execution as vscode.ShellExecution).options?.cwd);
216229

217230
runningFuncTasksChangedEmitter.fire();
@@ -233,18 +246,39 @@ export function registerFuncHostTaskEvents(): void {
233246
return;
234247
}
235248

236-
for await (const chunk of task.stream ?? []) {
237-
task.logs.push(chunk);
249+
const maxLogEntries = 1000;
250+
251+
try {
252+
for await (const chunk of task.stream ?? []) {
253+
// Check if the stream iteration should be aborted
254+
if (task.streamAbortController?.signal.aborted) {
255+
break;
256+
}
238257

239-
// Keep track of errors for the Debug view.
240-
if (isFuncHostErrorLog(chunk)) {
241-
const beforeCount = task.errorLogs?.length ?? 0;
242-
addErrorLog(task, chunk);
243-
const afterCount = task.errorLogs?.length ?? 0;
244-
if (afterCount > beforeCount) {
245-
runningFuncTasksChangedEmitter.fire();
258+
task.logs.push(chunk);
259+
if (task.logs.length > maxLogEntries) {
260+
task.logs.splice(0, task.logs.length - maxLogEntries);
246261
}
262+
263+
// Keep track of errors for the Debug view.
264+
if (isFuncHostErrorLog(chunk)) {
265+
const beforeCount = task.errorLogs?.length ?? 0;
266+
addErrorLog(task, chunk);
267+
const afterCount = task.errorLogs?.length ?? 0;
268+
if (afterCount > beforeCount) {
269+
runningFuncTasksChangedEmitter.fire();
270+
}
271+
}
272+
}
273+
} catch (error) {
274+
// If the stream encounters an error or is aborted, gracefully exit the loop
275+
// This prevents the event handler from hanging indefinitely
276+
if (task.streamAbortController?.signal.aborted) {
277+
// Expected when the task ends - no need to log
278+
return;
247279
}
280+
// Log unexpected errors but don't throw to avoid crashing the extension
281+
console.error('Error reading func host task stream:', error);
248282
}
249283
});
250284

0 commit comments

Comments
 (0)