@@ -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
3843function 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