diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs
index 3abc9faaaab..50b967db660 100644
--- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs
+++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs
@@ -70,6 +70,17 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket
private bool _cancelRequested = false;
private string _serverBusyMutexName = default!;
+ // Snapshot of process-global Console state captured once at server startup.
+ // Restored at the end of each request to prevent loggers (e.g. BaseConsoleLogger
+ // which sets ForegroundColor in SetColor() but only sometimes restores via ResetColor)
+ // or per-request ConsoleConfiguration application from leaking visible state into the
+ // next request or into the idle server's state.
+ private ConsoleColor _originalForegroundColor;
+ private ConsoleColor _originalBackgroundColor;
+ private bool _originalConsoleColorsCaptured;
+ private System.Text.Encoding _originalOutputEncoding = default!;
+ private bool _originalOutputEncodingCaptured;
+
public OutOfProcServerNode(BuildCallback buildFunction)
{
_buildFunction = buildFunction;
@@ -93,6 +104,31 @@ public OutOfProcServerNode(BuildCallback buildFunction)
/// The reason for shutting down.
public NodeEngineShutdownReason Run(out Exception? shutdownException)
{
+ // Capture process-global Console state once. Restoring per-request keeps reused
+ // server requests from observing each other's color/encoding mutations.
+ // Wrapped in try/catch because Console.ForegroundColor/BackgroundColor throw on
+ // some redirected/headless setups (e.g. CI without a TTY).
+ try
+ {
+ _originalForegroundColor = Console.ForegroundColor;
+ _originalBackgroundColor = Console.BackgroundColor;
+ _originalConsoleColorsCaptured = true;
+ }
+ catch
+ {
+ _originalConsoleColorsCaptured = false;
+ }
+
+ try
+ {
+ _originalOutputEncoding = Console.OutputEncoding;
+ _originalOutputEncodingCaptured = true;
+ }
+ catch
+ {
+ _originalOutputEncodingCaptured = false;
+ }
+
ServerNodeHandshake handshake = new(
CommunicationsUtilities.GetHandshakeOptions(taskHost: false, taskHostParameters: TaskHostParameters.Empty, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture()));
@@ -363,6 +399,53 @@ private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command)
}
private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command)
+ {
+ try
+ {
+ HandleServerNodeBuildCommandCore(command);
+ }
+ finally
+ {
+ RestoreConsoleStateAfterRequest();
+ }
+ }
+
+ ///
+ /// Restore process-global Console state to the snapshot captured at server startup.
+ /// Called from a finally block in so that
+ /// per-request mutations (loggers writing to Console.ForegroundColor, the per-request
+ /// application of ConsoleConfiguration.BackgroundColor, etc.) do not leak across
+ /// requests in a reused server. See investigation #9379 (LOG-2 / W1c).
+ ///
+ private void RestoreConsoleStateAfterRequest()
+ {
+ if (_originalConsoleColorsCaptured)
+ {
+ try
+ {
+ Console.ForegroundColor = _originalForegroundColor;
+ Console.BackgroundColor = _originalBackgroundColor;
+ }
+ catch
+ {
+ // Console color mutation can throw on redirected stdout in CI; safe to ignore.
+ }
+ }
+
+ if (_originalOutputEncodingCaptured)
+ {
+ try
+ {
+ Console.OutputEncoding = _originalOutputEncoding;
+ }
+ catch
+ {
+ // Some hosts disallow OutputEncoding mutation after Console initialization.
+ }
+ }
+ }
+
+ private void HandleServerNodeBuildCommandCore(ServerNodeBuildCommand command)
{
CommunicationsUtilities.Trace($"Building with MSBuild server with command line {command.CommandLine}");
using var serverBusyMutex = ServerNamedMutex.OpenOrCreateMutex(name: _serverBusyMutexName, createdNew: out var holdsMutex);