Skip to content

Commit 9dab980

Browse files
Fix IIS/Windows Service console race condition (#7691) (#7793) (#7810)
* Fix IIS/Windows Service console race condition (#7691) - Detect when running in IIS/Windows Service environments where Console.Out and Console.Error are redirected to the same StreamWriter.Null singleton - Skip console output entirely in these environments to prevent race conditions that cause IndexOutOfRangeException and cascade failures - Improve DefaultLogger error handling to prevent feedback loops - Add unit tests for non-console scenarios The race condition occurs because: 1. IIS/Services redirect both Console.Out and Console.Error to StreamWriter.Null 2. StreamWriter.Null is a singleton, not thread-safe for concurrent access 3. Multiple threads writing to both streams cause IndexOutOfRangeException 4. Console output goes nowhere in these environments anyway Fixes #7691 * Refine console detection and simplify error handling - Make console detection more precise: only skip output when both Console.Out AND Console.Error point to StreamWriter.Null (the exact race condition scenario) - Remove unnecessary try-catch in DefaultLogger.Print() since Tell() is unlikely to throw - Keep improved error message for debugging when logger is not initialized
1 parent 66f7f1e commit 9dab980

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="StandardOutWriterSpec.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
8+
using System;
9+
using System.IO;
10+
using System.Threading.Tasks;
11+
using Akka.TestKit;
12+
using Akka.Util;
13+
using Xunit;
14+
using Xunit.Abstractions;
15+
16+
namespace Akka.Tests.Loggers
17+
{
18+
/// <summary>
19+
/// Tests for StandardOutWriter to ensure it handles IIS/Windows Service environments correctly
20+
/// where Console.Out and Console.Error may be redirected to StreamWriter.Null
21+
/// </summary>
22+
public class StandardOutWriterSpec : AkkaSpec
23+
{
24+
public StandardOutWriterSpec(ITestOutputHelper output) : base(output)
25+
{
26+
}
27+
28+
[Fact]
29+
public void StandardOutWriter_should_handle_concurrent_writes_without_race_conditions()
30+
{
31+
// This test simulates the concurrent access pattern that causes issues in IIS
32+
// In normal test environments this won't reproduce the issue, but it ensures
33+
// our fix doesn't break normal console operation
34+
35+
var tasks = new Task[100];
36+
37+
for (int i = 0; i < tasks.Length; i++)
38+
{
39+
var taskId = i;
40+
tasks[i] = Task.Run(() =>
41+
{
42+
for (int j = 0; j < 10; j++)
43+
{
44+
// These calls should not throw even under concurrent access
45+
StandardOutWriter.WriteLine($"Task {taskId} - Line {j}");
46+
StandardOutWriter.Write($"Task {taskId} - Write {j} ");
47+
}
48+
});
49+
}
50+
51+
// Should complete without throwing IndexOutOfRangeException
52+
Assert.True(Task.WaitAll(tasks, TimeSpan.FromSeconds(5)));
53+
}
54+
55+
[Fact]
56+
public void StandardOutWriter_should_not_throw_when_console_is_redirected()
57+
{
58+
// Save original streams
59+
var originalOut = Console.Out;
60+
var originalError = Console.Error;
61+
62+
try
63+
{
64+
// Simulate IIS/Windows Service environment by redirecting to null
65+
Console.SetOut(StreamWriter.Null);
66+
Console.SetError(StreamWriter.Null);
67+
68+
// These should not throw even when console is redirected to null
69+
StandardOutWriter.WriteLine("This should not throw");
70+
StandardOutWriter.Write("Neither should this");
71+
72+
// Test with colors (which would normally fail in IIS)
73+
StandardOutWriter.WriteLine("Colored output", ConsoleColor.Red);
74+
StandardOutWriter.Write("Colored write", ConsoleColor.Blue, ConsoleColor.Yellow);
75+
}
76+
finally
77+
{
78+
// Restore original streams
79+
Console.SetOut(originalOut);
80+
Console.SetError(originalError);
81+
}
82+
}
83+
84+
[Fact]
85+
public void StandardOutWriter_should_handle_null_and_empty_messages()
86+
{
87+
// Should not throw
88+
StandardOutWriter.WriteLine(null);
89+
StandardOutWriter.WriteLine("");
90+
StandardOutWriter.Write(null);
91+
StandardOutWriter.Write("");
92+
}
93+
}
94+
}

src/core/Akka/Event/DefaultLogger.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ protected override bool Receive(object message)
4646
protected virtual void Print(LogEvent logEvent)
4747
{
4848
if (_stdoutLogger == null)
49-
throw new Exception("Logger has not been initialized yet.");
49+
{
50+
// Include context about the failed log event to help with debugging
51+
var logDetails = $"[{logEvent.LogLevel()}] {logEvent.LogSource}: {logEvent.Message}";
52+
throw new Exception($"Logger has not been initialized yet. Failed to log: {logDetails}");
53+
}
5054

5155
_stdoutLogger.Tell(logEvent);
5256
}

src/core/Akka/Util/StandardOutWriter.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//-----------------------------------------------------------------------
77

88
using System;
9+
using System.IO;
910

1011
namespace Akka.Util
1112
{
@@ -16,6 +17,34 @@ namespace Akka.Util
1617
public static class StandardOutWriter
1718
{
1819
private static readonly object _lock = new();
20+
private static readonly bool _isConsoleAvailable = DetectConsoleAvailability();
21+
22+
/// <summary>
23+
/// Detects whether a real console is available for output.
24+
/// In environments like IIS and Windows Services, console output is redirected to StreamWriter.Null,
25+
/// which is a singleton. When multiple threads write to both Console.Out and Console.Error
26+
/// (which point to the same StreamWriter.Null instance), it causes race conditions.
27+
///
28+
/// Since console output goes nowhere in these environments anyway, we skip it entirely
29+
/// to prevent the race condition and improve performance.
30+
/// </summary>
31+
private static bool DetectConsoleAvailability()
32+
{
33+
// Specifically detect the IIS/Windows Service scenario where both Console.Out
34+
// and Console.Error point to the SAME StreamWriter.Null singleton instance.
35+
// This is the exact condition that causes the race condition.
36+
// Note: We check both because in these environments, both are always set to the same instance
37+
if (Console.Out == StreamWriter.Null && Console.Error == StreamWriter.Null)
38+
return false;
39+
40+
// Also check Environment.UserInteractive for additional safety
41+
// This returns false for Windows Services and IIS in .NET Framework
42+
// (though less reliable in .NET Core, the StreamWriter.Null check above is the key)
43+
if (!Environment.UserInteractive)
44+
return false;
45+
46+
return true;
47+
}
1948

2049
/// <summary>
2150
/// Writes the specified <see cref="string"/> value to the standard output stream. Optionally
@@ -46,6 +75,16 @@ public static void WriteLine(string message, ConsoleColor? foregroundColor = nul
4675
private static void WriteToConsole(string message, ConsoleColor? foregroundColor = null,
4776
ConsoleColor? backgroundColor = null, bool line = true)
4877
{
78+
// Skip console output in IIS, Windows Services, and other non-console environments.
79+
// In these environments:
80+
// 1. Console output is redirected to StreamWriter.Null (goes nowhere anyway)
81+
// 2. Both Console.Out and Console.Error point to the same StreamWriter.Null singleton
82+
// 3. Concurrent writes to both streams cause race conditions and IndexOutOfRangeException
83+
// 4. Skipping output entirely prevents the race condition and improves performance
84+
// See: https://github.com/akkadotnet/akka.net/issues/7691
85+
if (!_isConsoleAvailable)
86+
return;
87+
4988
lock (_lock)
5089
{
5190
ConsoleColor? fg = null;

0 commit comments

Comments
 (0)