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