From e05ae94081473a89c1fb6c68126e2421f01dd7ed Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 18:33:47 +0000
Subject: [PATCH 1/5] Initial plan
From e55fcaa2005701152567e62fd13da79cf28ead11 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 21 Nov 2025 18:54:10 +0000
Subject: [PATCH 2/5] Fix TerminalLogger IndexOutOfRangeException during binary
log replay with fewer nodes
Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com>
---
src/Build.UnitTests/TerminalLogger_Tests.cs | 42 +++++++++++++++++++
.../Logging/TerminalLogger/TerminalLogger.cs | 20 ++++++++-
2 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs
index 1d0b858e3aa..a239f96fcf9 100644
--- a/src/Build.UnitTests/TerminalLogger_Tests.cs
+++ b/src/Build.UnitTests/TerminalLogger_Tests.cs
@@ -942,5 +942,47 @@ public async Task ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier()
});
await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform();
}
+
+ [Fact]
+ public void ReplayBinaryLogWithFewerNodesThanOriginalBuild()
+ {
+ // This test validates that replaying a binary log with terminal logger
+ // using fewer nodes than the original build does not cause an IndexOutOfRangeException.
+ // See issue: https://github.com/dotnet/msbuild/issues/10596
+
+ using (TestEnvironment env = TestEnvironment.Create())
+ {
+ // Create a simple project
+ string contents = @"
+
+
+
+
+";
+ TransientTestFolder logFolder = env.CreateFolder(createFolder: true);
+ TransientTestFile projectFile = env.CreateFile(logFolder, "test.proj", contents);
+
+ string binlogPath = env.ExpectFile(".binlog").Path;
+
+ // Build with multiple nodes to create a binlog with higher node IDs
+ RunnerUtilities.ExecMSBuild($"{projectFile.Path} /m:4 /bl:{binlogPath}", out bool success, outputHelper: _outputHelper);
+ success.ShouldBeTrue();
+
+ // Replay the binlog with TerminalLogger using only 1 node
+ // This should NOT throw an IndexOutOfRangeException
+ var replayEventSource = new BinaryLogReplayEventSource();
+ using var outputWriter = new StringWriter();
+ using var mockTerminal = new Terminal(outputWriter);
+ var terminalLogger = new TerminalLogger(mockTerminal);
+
+ // Initialize with only 1 node (fewer than the original build)
+ terminalLogger.Initialize(replayEventSource, nodeCount: 1);
+
+ // This should complete without throwing an exception
+ Should.NotThrow(() => replayEventSource.Replay(binlogPath));
+
+ terminalLogger.Shutdown();
+ }
+ }
}
}
diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
index 9dbbfe83d13..1a8a4ef5bfc 100644
--- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
@@ -665,6 +665,7 @@ private void ProjectStarted(object sender, ProjectStartedEventArgs e)
{
_restoreContext = c;
int nodeIndex = NodeIndexForContext(e.BuildEventContext);
+ EnsureNodeCapacity(nodeIndex);
_nodes[nodeIndex] = new TerminalNodeStatus(e.ProjectFile!, targetFramework, runtimeIdentifier, "Restore", _projects[c].Stopwatch);
}
}
@@ -981,9 +982,24 @@ private void TargetStarted(object sender, TargetStartedEventArgs e)
private void UpdateNodeStatus(BuildEventContext buildEventContext, TerminalNodeStatus? nodeStatus)
{
int nodeIndex = NodeIndexForContext(buildEventContext);
+ EnsureNodeCapacity(nodeIndex);
_nodes[nodeIndex] = nodeStatus;
}
+ ///
+ /// Ensures that the array has enough capacity to accommodate the given index.
+ /// This is necessary for binary log replay scenarios where the replay may use fewer nodes than the original build.
+ ///
+ private void EnsureNodeCapacity(int nodeIndex)
+ {
+ if (nodeIndex >= _nodes.Length)
+ {
+ // Resize to accommodate the new index plus some extra capacity
+ int newSize = Math.Max(nodeIndex + 1, _nodes.Length * 2);
+ Array.Resize(ref _nodes, newSize);
+ }
+ }
+
///
/// The callback. Unused.
///
@@ -1100,7 +1116,9 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
if (hasProject && project!.IsTestProject)
{
- TerminalNodeStatus? node = _nodes[NodeIndexForContext(buildEventContext)];
+ int nodeIndex = NodeIndexForContext(buildEventContext);
+ EnsureNodeCapacity(nodeIndex);
+ TerminalNodeStatus? node = _nodes[nodeIndex];
// Consumes test update messages produced by VSTest and MSTest runner.
if (e is IExtendedBuildEventArgs extendedMessage)
From 2350a3669eaafd3bb9a7dc63e50e04f423aa6b70 Mon Sep 17 00:00:00 2001
From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com>
Date: Wed, 7 Jan 2026 13:23:05 +0100
Subject: [PATCH 3/5] Update src/Build/Logging/TerminalLogger/TerminalLogger.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Build/Logging/TerminalLogger/TerminalLogger.cs | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
index d2b25f535e4..e3f7ec3ddd1 100644
--- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
@@ -1061,8 +1061,14 @@ private void EnsureNodeCapacity(int nodeIndex)
if (nodeIndex >= _nodes.Length)
{
// Resize to accommodate the new index plus some extra capacity
- int newSize = Math.Max(nodeIndex + 1, _nodes.Length * 2);
- Array.Resize(ref _nodes, newSize);
+ lock (_lock)
+ {
+ if (nodeIndex >= _nodes.Length)
+ {
+ int newSize = Math.Max(nodeIndex + 1, _nodes.Length * 2);
+ Array.Resize(ref _nodes, newSize);
+ }
+ }
}
}
From 461f2bf893af63686c3c25b88044f2d4656b6a36 Mon Sep 17 00:00:00 2001
From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com>
Date: Wed, 7 Jan 2026 13:23:35 +0100
Subject: [PATCH 4/5] Update src/Build.UnitTests/TerminalLogger_Tests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Build.UnitTests/TerminalLogger_Tests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs
index e828ec90026..bf26867c3d7 100644
--- a/src/Build.UnitTests/TerminalLogger_Tests.cs
+++ b/src/Build.UnitTests/TerminalLogger_Tests.cs
@@ -1058,6 +1058,7 @@ public void ReplayBinaryLogWithFewerNodesThanOriginalBuild()
terminalLogger.Shutdown();
}
+ }
[Theory]
[InlineData(true)]
From e73419f9b7b9db3c5a78ddbe18ba2f4ba1c5ffad Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 7 Jan 2026 12:43:26 +0000
Subject: [PATCH 5/5] Limit EnsureNodeCapacity to replay mode and improve test
to use parallel builds
Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com>
---
src/Build.UnitTests/TerminalLogger_Tests.cs | 36 +++++++++++++++----
.../Logging/TerminalLogger/TerminalLogger.cs | 11 +++++-
2 files changed, 40 insertions(+), 7 deletions(-)
diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs
index bf26867c3d7..9d781c082a9 100644
--- a/src/Build.UnitTests/TerminalLogger_Tests.cs
+++ b/src/Build.UnitTests/TerminalLogger_Tests.cs
@@ -1027,20 +1027,44 @@ public void ReplayBinaryLogWithFewerNodesThanOriginalBuild()
using (TestEnvironment env = TestEnvironment.Create())
{
- // Create a simple project
- string contents = @"
+ // Create multiple projects that will build in parallel
+ TransientTestFolder logFolder = env.CreateFolder(createFolder: true);
+
+ // Create three simple projects
+ TransientTestFile project1 = env.CreateFile(logFolder, "project1.proj", @"
+
+
+
+
+");
+
+ TransientTestFile project2 = env.CreateFile(logFolder, "project2.proj", @"
+
+
+
+
+");
+
+ TransientTestFile project3 = env.CreateFile(logFolder, "project3.proj", @"
+
+
+
+
+");
+
+ // Create a solution file that builds all projects in parallel
+ string solutionContents = $@"
-
+
";
- TransientTestFolder logFolder = env.CreateFolder(createFolder: true);
- TransientTestFile projectFile = env.CreateFile(logFolder, "test.proj", contents);
+ TransientTestFile solutionFile = env.CreateFile(logFolder, "solution.proj", solutionContents);
string binlogPath = env.ExpectFile(".binlog").Path;
// Build with multiple nodes to create a binlog with higher node IDs
- RunnerUtilities.ExecMSBuild($"{projectFile.Path} /m:4 /bl:{binlogPath}", out bool success, outputHelper: _outputHelper);
+ RunnerUtilities.ExecMSBuild($"{solutionFile.Path} /m:4 /bl:{binlogPath}", out bool success, outputHelper: _outputHelper);
success.ShouldBeTrue();
// Replay the binlog with TerminalLogger using only 1 node
diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
index e3f7ec3ddd1..e89b370249f 100644
--- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
@@ -151,6 +151,11 @@ public EvalContext(BuildEventContext context)
///
private ProjectContext? _restoreContext;
+ ///
+ /// True if we're replaying a binary log. In this mode, we may encounter NodeIds higher than the initial node count.
+ ///
+ private bool _isReplayMode = false;
+
///
/// The thread that performs periodic refresh of the console output.
///
@@ -432,6 +437,9 @@ public void Initialize(IEventSource eventSource)
{
ParseParameters();
+ // Detect if we're in replay mode
+ _isReplayMode = eventSource is IBinaryLogReplaySource;
+
eventSource.BuildStarted += BuildStarted;
eventSource.BuildFinished += BuildFinished;
eventSource.ProjectStarted += ProjectStarted;
@@ -1058,7 +1066,8 @@ private void UpdateNodeStatus(BuildEventContext buildEventContext, TerminalNodeS
///
private void EnsureNodeCapacity(int nodeIndex)
{
- if (nodeIndex >= _nodes.Length)
+ // Only resize in replay mode - during normal builds, the node count is fixed
+ if (_isReplayMode && nodeIndex >= _nodes.Length)
{
// Resize to accommodate the new index plus some extra capacity
lock (_lock)