diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs
index 575b727ad46..9d781c082a9 100644
--- a/src/Build.UnitTests/TerminalLogger_Tests.cs
+++ b/src/Build.UnitTests/TerminalLogger_Tests.cs
@@ -1018,6 +1018,72 @@ 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 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 = $@"
+
+
+
+
+";
+ 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($"{solutionFile.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();
+ }
+ }
+
[Theory]
[InlineData(true)]
[InlineData(false)]
diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
index dffb6064a90..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;
@@ -732,6 +740,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);
}
}
@@ -1047,9 +1056,31 @@ 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)
+ {
+ // 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)
+ {
+ if (nodeIndex >= _nodes.Length)
+ {
+ int newSize = Math.Max(nodeIndex + 1, _nodes.Length * 2);
+ Array.Resize(ref _nodes, newSize);
+ }
+ }
+ }
+ }
+
///
/// The callback. Unused.
///
@@ -1185,7 +1216,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)