Skip to content

Commit db407f4

Browse files
Copilotnturinski
andauthored
Prevent infinite stream iteration when func host task terminates (#4887)
* Initial plan * Add AbortController to prevent infinite stream iteration Co-authored-by: nturinski <5290572+nturinski@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nturinski <5290572+nturinski@users.noreply.github.com>
1 parent 4213222 commit db407f4

File tree

1 file changed

+41
-12
lines changed

1 file changed

+41
-12
lines changed

src/funcCoreTools/funcHostTask.ts

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

3742
function addErrorLog(task: IRunningFuncTask, rawChunk: string): void {
@@ -188,6 +193,7 @@ export function registerFuncHostTaskEvents(): void {
188193
logs,
189194
errorLogs: [],
190195
hasReportedLiveErrors: false,
196+
streamAbortController: new AbortController(),
191197
};
192198

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

216229
runningFuncTasksChangedEmitter.fire();
@@ -234,21 +247,37 @@ export function registerFuncHostTaskEvents(): void {
234247

235248
const maxLogEntries = 1000;
236249

237-
for await (const chunk of task.stream ?? []) {
238-
task.logs.push(chunk);
239-
if (task.logs.length > maxLogEntries) {
240-
task.logs.splice(0, task.logs.length - maxLogEntries);
241-
}
250+
try {
251+
for await (const chunk of task.stream ?? []) {
252+
// Check if the stream iteration should be aborted
253+
if (task.streamAbortController?.signal.aborted) {
254+
break;
255+
}
242256

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

0 commit comments

Comments
 (0)