diff --git a/TUnit.Engine/Logging/BufferedTextWriter.cs b/TUnit.Engine/Logging/BufferedTextWriter.cs index 83c6ad97c0..d41545f129 100644 --- a/TUnit.Engine/Logging/BufferedTextWriter.cs +++ b/TUnit.Engine/Logging/BufferedTextWriter.cs @@ -282,17 +282,29 @@ public override void Flush() // Flush all thread-local buffers FlushAllThreadBuffers(); + // Collect content to write without holding the lock + var contentToWrite = new List(); + _lock.EnterWriteLock(); try { - // Process any queued content - ProcessFlushQueue(); - _target.Flush(); + // Dequeue all content while holding the lock + while (_flushQueue.TryDequeue(out var content)) + { + contentToWrite.Add(content); + } } finally { _lock.ExitWriteLock(); } + + // Write content and flush outside the lock to avoid deadlock + foreach (var content in contentToWrite) + { + _target.Write(content); + } + _target.Flush(); } public override async Task FlushAsync() @@ -350,7 +362,28 @@ private void FlushBuffer(StringBuilder buffer) // Process queue if it's getting large if (_flushQueue.Count > 10) { - ProcessFlushQueue(); + // Collect content to write without holding the lock + var contentToWrite = new List(); + + _lock.EnterWriteLock(); + try + { + // Dequeue content while holding the lock + while (_flushQueue.TryDequeue(out var queuedContent) && contentToWrite.Count < 20) + { + contentToWrite.Add(queuedContent); + } + } + finally + { + _lock.ExitWriteLock(); + } + + // Write content outside the lock to avoid deadlock + foreach (var contentItem in contentToWrite) + { + _target.Write(contentItem); + } } } @@ -385,15 +418,28 @@ private void AutoFlush(object? state) { FlushAllThreadBuffers(); + // Collect content to write without holding the lock + var contentToWrite = new List(); + _lock.EnterWriteLock(); try { - ProcessFlushQueue(); + // Dequeue all content while holding the lock + while (_flushQueue.TryDequeue(out var content)) + { + contentToWrite.Add(content); + } } finally { _lock.ExitWriteLock(); } + + // Write content outside the lock to avoid deadlock + foreach (var content in contentToWrite) + { + _target.Write(content); + } } catch { @@ -408,10 +454,17 @@ protected override void Dispose(bool disposing) _flushTimer?.Dispose(); FlushAllThreadBuffers(); + // Collect content to write without holding the lock + var contentToWrite = new List(); + _lock.EnterWriteLock(); try { - ProcessFlushQueue(); + // Dequeue all content while holding the lock + while (_flushQueue.TryDequeue(out var content)) + { + contentToWrite.Add(content); + } _disposed = true; } finally @@ -419,6 +472,12 @@ protected override void Dispose(bool disposing) _lock.ExitWriteLock(); } + // Write content outside the lock + foreach (var content in contentToWrite) + { + _target.Write(content); + } + _threadLocalBuffer?.Dispose(); _lock?.Dispose(); } diff --git a/TUnit.Engine/Logging/StandardErrorConsoleInterceptor.cs b/TUnit.Engine/Logging/StandardErrorConsoleInterceptor.cs index 25e97b5702..41b921f2cc 100644 --- a/TUnit.Engine/Logging/StandardErrorConsoleInterceptor.cs +++ b/TUnit.Engine/Logging/StandardErrorConsoleInterceptor.cs @@ -15,7 +15,12 @@ internal class StandardErrorConsoleInterceptor : OptimizedConsoleInterceptor static StandardErrorConsoleInterceptor() { - DefaultError = Console.Error; + // Get the raw stream without SyncTextWriter synchronization wrapper + // BufferedTextWriter already provides thread safety, so we avoid double-locking + DefaultError = new StreamWriter(Console.OpenStandardError()) + { + AutoFlush = true + }; } public StandardErrorConsoleInterceptor(VerbosityService verbosityService) : base(verbosityService) diff --git a/TUnit.Engine/Logging/StandardOutConsoleInterceptor.cs b/TUnit.Engine/Logging/StandardOutConsoleInterceptor.cs index e7409621dc..9d981ceca8 100644 --- a/TUnit.Engine/Logging/StandardOutConsoleInterceptor.cs +++ b/TUnit.Engine/Logging/StandardOutConsoleInterceptor.cs @@ -15,7 +15,12 @@ internal class StandardOutConsoleInterceptor : OptimizedConsoleInterceptor static StandardOutConsoleInterceptor() { - DefaultOut = Console.Out; + // Get the raw stream without SyncTextWriter synchronization wrapper + // BufferedTextWriter already provides thread safety, so we avoid double-locking + DefaultOut = new StreamWriter(Console.OpenStandardOutput()) + { + AutoFlush = true + }; } public StandardOutConsoleInterceptor(VerbosityService verbosityService) : base(verbosityService)