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)