@@ -487,6 +487,7 @@ public static (int exitCode, string buildOutput) RunProcess(string path,
487487 _testOutput . WriteLine ( $ "WorkingDirectory: { workingDir } ") ;
488488 StringBuilder outputBuilder = new ( ) ;
489489 object syncObj = new ( ) ;
490+ bool isDisposed = false ;
490491
491492 var processStartInfo = new ProcessStartInfo
492493 {
@@ -542,11 +543,13 @@ public static (int exitCode, string buildOutput) RunProcess(string path,
542543 using CancellationTokenSource cts = new ( ) ;
543544 cts . CancelAfter ( timeoutMs ?? s_defaultPerTestTimeoutMs ) ;
544545
545- await process . WaitForExitAsync ( cts . Token ) ;
546-
547- if ( cts . IsCancellationRequested )
546+ try
547+ {
548+ await process . WaitForExitAsync ( cts . Token ) ;
549+ }
550+ catch ( OperationCanceledException )
548551 {
549- // process didn't exit
552+ // process didn't exit within timeout
550553 process . Kill ( entireProcessTree : true ) ;
551554 lock ( syncObj )
552555 {
@@ -560,6 +563,12 @@ public static (int exitCode, string buildOutput) RunProcess(string path,
560563 // https://learn.microsoft.com/dotnet/api/system.diagnostics.process.waitforexit?view=net-5.0#System_Diagnostics_Process_WaitForExit_System_Int32_
561564 process . WaitForExit ( ) ;
562565
566+ // Mark as disposed before detaching handlers to prevent further TestOutput access
567+ lock ( syncObj )
568+ {
569+ isDisposed = true ;
570+ }
571+
563572 process . ErrorDataReceived -= logStdErr ;
564573 process . OutputDataReceived -= logStdOut ;
565574 process . CancelErrorRead ( ) ;
@@ -573,17 +582,24 @@ public static (int exitCode, string buildOutput) RunProcess(string path,
573582 }
574583 catch ( Exception ex )
575584 {
576- _testOutput . WriteLine ( $ "-- exception -- { ex } ") ;
585+ // Mark as disposed before writing to avoid potential race condition
586+ lock ( syncObj )
587+ {
588+ isDisposed = true ;
589+ }
590+ TryWriteToTestOutput ( _testOutput , $ "-- exception -- { ex } ", outputBuilder ) ;
577591 throw ;
578592 }
579593
580594 void LogData ( string label , string ? message )
581595 {
582596 lock ( syncObj )
583597 {
598+ if ( isDisposed )
599+ return ;
584600 if ( message != null )
585601 {
586- _testOutput . WriteLine ( $ "{ label } { message } ") ;
602+ TryWriteToTestOutput ( _testOutput , $ "{ label } { message } ", outputBuilder , label ) ;
587603 }
588604 outputBuilder . AppendLine ( $ "{ label } { message } ") ;
589605 }
@@ -627,6 +643,24 @@ public static string AddItemsPropertiesToProject(string projectFile, string? ext
627643 return projectFile ;
628644 }
629645
646+ private static void TryWriteToTestOutput ( ITestOutputHelper testOutput , string message , StringBuilder ? outputBuffer = null , string ? warningPrefix = null )
647+ {
648+ try
649+ {
650+ testOutput . WriteLine ( message ) ;
651+ }
652+ catch ( InvalidOperationException )
653+ {
654+ // Test context has expired, but we still want to capture output in buffer
655+ // for potential debugging purposes
656+ if ( outputBuffer != null )
657+ {
658+ string prefix = warningPrefix ?? "" ;
659+ outputBuffer . AppendLine ( $ "{ prefix } [WARNING: Test context expired, subsequent output may be incomplete]") ;
660+ }
661+ }
662+ }
663+
630664 public void Dispose ( )
631665 {
632666 if ( _projectDir != null && _enablePerTestCleanup )
0 commit comments