Skip to content

Report eval errors through cli loggers#12095

Merged
baronfel merged 18 commits intomainfrom
report-eval-errors-through-cli-loggers
Sep 9, 2025
Merged

Report eval errors through cli loggers#12095
baronfel merged 18 commits intomainfrom
report-eval-errors-through-cli-loggers

Conversation

@baronfel
Copy link
Member

@baronfel baronfel commented Jun 30, 2025

Fixes #11372

Context

Eval-time errors are missed because the eval-time ProjectCollection currently is only seeded with single-node loggers, not the central-node loggers that are created for DistributedLoggerRecords. In general, our loggers today are either-or: in single-node mode they register a single logger, but in multi-node mode (the default for dotnet commands) the loggers register as distributed loggers. This means that msbuild.exe-using users will see eval errors, but dotnet users will not.

There is one notable exception here - the binary logger only ever registers as a central logger, because in this configuration absorbs all messages from all node automatically due to an implicit node-forwarding setup that the engine configures.

Changes Made

Add central loggers to the set of loggers registered with the main eval-time ProjectCollection

After this change, it became apparent that the TerminalLogger was getting double-messages sent to it

image

This led me to investigate the logging service, where logger registration occurs. There are two logging services in play here:

  • the eval-time LoggingService
  • the build-time LoggingService

The eval-time logging service is primed with Loggers from the eval-time ProjectCollection. However, the eval-time ProjectCollection uses the loggers and forwarding loggers passed into it, but it wraps them as ReusableLoggers. This is a kind of logger that 'buffers' the underlying logger from the fact that evaluation and execution are two separate phases. Doing this allows the inner logger to register to a single event source (the ReusableLogger itself) and internally the ReusableLogger switches between eval-time and execution-time event sinks.

The build-time LoggingService uses the list of Loggers from the eval-time ProjectCollection (threaded through the BuildParameters class), but crucially it uses a different view of the ForwardedLoggers. This view just has the 'inner' CentralLoggers for each configured forwarded logger.

What this means is that during LoggingService initialization, when it checks if a logger is registered already, it sees a TerminalLogger registered (which happens here), and a ReusableLogger containing that same TerminalLogger asking to be registered (which happens here). To fix this, we:

Ensure that ReusableLogger's OriginalLoggers that are already registered with a LoggingService are not re-registered

With this change, both single-proc and multi-proc build report the same amount of events:

image

With this done I could look at the eval errors themselves. they were now reporting like this:

image

This is gross and has duplicated information, so now we:

Make Eval-time errors report more nicely in TerminalLogger

image

Testing

Added a TerminalLogger test case for the formatting.
Updated existing test cases for internal changes related to this work.

Notes

We may want to consider making the ReusableLogger public API - I think we've had issues in this area in the SDK (cc @jjonescz?) that this may have solved.

@baronfel baronfel force-pushed the report-eval-errors-through-cli-loggers branch from b1b7383 to f3c695d Compare August 10, 2025 15:56
@baronfel baronfel marked this pull request as ready for review August 10, 2025 16:28
Copilot AI review requested due to automatic review settings August 10, 2025 16:28
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes eval-time errors not being reported through CLI loggers by ensuring central loggers from distributed logger records are included in evaluation-time project collections. Previously, only single-node loggers were registered for evaluation, causing multi-node environments (default for dotnet commands) to miss eval errors while single-node environments (msbuild.exe) would see them.

Key changes:

  • Add central loggers to eval-time ProjectCollection logger registration
  • Prevent duplicate logger registration when ReusableLoggers wrap already-registered loggers
  • Improve eval error formatting in TerminalLogger by detecting MSBuild placeholder locations

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/MSBuild/XMake.cs Modified to include central loggers from distributed logger records in evaluation logger array
src/MSBuild.UnitTests/XMake_Tests.cs Updated test expectations and added better error reporting for message importance tests
src/Build/Microsoft.Build.csproj Added ReusableLogger.cs to project compilation
src/Build/Logging/TerminalLogger/TerminalLogger.cs Enhanced error formatting and added method to get minimum message importance
src/Build/Logging/ReusableLogger.cs Moved ReusableLogger class from ProjectCollection to separate file with IEventSource5 support
src/Build/Definition/ProjectCollection.cs Removed ReusableLogger inner class and updated references to use new location
src/Build/BackEnd/Components/Logging/LoggingService.cs Enhanced duplicate logger detection for ReusableLoggers and updated TerminalLogger handling

@baronfel baronfel force-pushed the report-eval-errors-through-cli-loggers branch 2 times, most recently from b48dd72 to ab2fc64 Compare August 20, 2025 22:32
@baronfel baronfel enabled auto-merge (squash) August 20, 2025 22:32
@baronfel
Copy link
Member Author

the only failing test now:

Microsoft.Build.Engine.UnitTests.BackEnd.TaskHostFactory_Tests.TaskNodesDieAfterBuild(taskHostFactorySpecified: False, envVariableSpecified: True)

Zero insight into this one :-/

@YuliiaKovalova
Copy link
Member

the only failing test now:

Microsoft.Build.Engine.UnitTests.BackEnd.TaskHostFactory_Tests.TaskNodesDieAfterBuild(taskHostFactorySpecified: False, envVariableSpecified: True)

Zero insight into this one :-/

it's not you, it's sidecartaskhost

Copy link
Member

@JanProvaznik JanProvaznik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changes look fine, but I don't think it fully fixes the issue

I checked out the branch, did build.cmd

but still I see the discrepancy it's supposed to address

Image (foo.csproj is just <Project></Project>

@JanProvaznik
Copy link
Member

curiously for MSBuild.exe it works, but has a formatting discrepancy :(
image

@baronfel baronfel force-pushed the report-eval-errors-through-cli-loggers branch from ab2fc64 to 981b23a Compare August 21, 2025 13:36
@baronfel
Copy link
Member Author

I made a quick script to test permutations on the bootstrap msbuild variants from this PR.

Runners: dotnet build, dotnet msbuild, msbuild.exe
Variations: TL on or off, M 1 or M 2

results:
image

To me, these look broadly fine - the console log being indented a bit is an artifact of the way the console logger prepends the node-specific logs with an indent + the node id. I'm not super concerned about changing the console logger's output here for greater usability/prettiness like I am the terminal logger.

ps1 script for repro ```pwsh # Script: test.ps1 # Run MSBuild variants to capture logger output.

param(
[switch]$ShowFailed
)

Determine script directory

$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent }

Project path in this workspace

$project = Join-Path $scriptDir 'foo.csproj'

The exact extra property argument requested (kept with the space as provided)

$fixedProperty = '/p:"foo =bar"'

Three invocation styles to test

$invocations = @(
@{ Name = 'dotnet-build'; Exe = 'D:\Code\msbuild\artifacts\bin\bootstrap\core\dotnet.exe'; Verb = 'build' },
@{ Name = 'dotnet-msbuild'; Exe = 'D:\Code\msbuild\artifacts\bin\bootstrap\core\dotnet.exe'; Verb = 'msbuild' },
@{ Name = 'net472-msbuild'; Exe = 'D:\Code\msbuild\artifacts\bin\bootstrap\net472\MSBuild\Current\Bin\msbuild.exe'; Verb = $null }
)

Options to vary

$tlOptions = @('on','off')
$mOptions = @('1','2')

Output folder for logs

$logDir = Join-Path $scriptDir 'msbuild-logs'
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory | Out-Null }

$results = [System.Collections.Generic.List[object]]::new()
$runIndex = 0

foreach ($inv in $invocations) {
foreach ($tl in $tlOptions) {
foreach ($m in $mOptions) {
$runIndex++
$runName = '{0:D2}_{1}_tl-{2}_m-{3}' -f $runIndex, $inv.Name, $tl, $m
$logFile = Join-Path $logDir ($runName + '.log')

        $args = @()
        if ($inv.Verb) { $args += $inv.Verb }
        $args += $project
        $args += "/tl:$tl"
        $args += "/m:$m"
        # Add the quoted property so the invocation shows it quoted and the command-line parser receives it as intended
        $args += $fixedProperty

        Write-Host "[$runIndex] Running: $($inv.Exe) $([string]::Join(' ', $args))"

        $start = Get-Date
        try {
            $rawOutput = & $inv.Exe @args 2>&1 | Out-String
        } catch {
            $rawOutput = $_ | Out-String
        }

        $rawOutput | Out-File -FilePath $logFile -Encoding utf8

        $results.Add([pscustomobject]@{
            Run = $runName
            Exe = $inv.Exe
            Args = [string]::Join(' ', $args)
            Log = $logFile
        })
    }
}

}

Write-Host "`nSummary:"
$results | Format-Table -AutoSize

if ($ShowFailed) {
$failed = $results | Where-Object { $.ExitCode -ne 0 }
if ($failed) {
Write-Host "nFailed runs (showing last 200 lines):" foreach ($f in $failed) { Write-Host "n== $($f.Run) ($($f.Log)) ==" -ForegroundColor Yellow
Get-Content $f.Log -Tail 200 | ForEach-Object { Write-Host $
}
}
} else {
Write-Host "No failed runs."
}
}

Write-Host "`nLogs written to: $logDir"

</details>

@baronfel
Copy link
Member Author

the only failing test now:

Microsoft.Build.Engine.UnitTests.BackEnd.TaskHostFactory_Tests.TaskNodesDieAfterBuild(taskHostFactorySpecified: False, envVariableSpecified: True)

Zero insight into this one :-/

it's not you, it's sidecartaskhost

@SimaTian is this test issue known? can you please take a look?

@baronfel baronfel force-pushed the report-eval-errors-through-cli-loggers branch from 7668c2f to 9382032 Compare August 22, 2025 14:28
Copy link
Member

@JanProvaznik JanProvaznik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the concern I raised was me using the script validating the change wrong, lgtm

@baronfel
Copy link
Member Author

baronfel commented Sep 4, 2025

@dotnet/kitten are we clear to merge this one then?

@baronfel baronfel merged commit b6f853d into main Sep 9, 2025
9 checks passed
@baronfel baronfel deleted the report-eval-errors-through-cli-loggers branch September 9, 2025 09:03
YuliiaKovalova pushed a commit that referenced this pull request Jan 15, 2026
### Summary

Fixes the MSB1025 ArgumentNullException error that occurs when using the
`-dfl` (distributed file logger) flag with MSBuild 18. The issue was
introduced in PR #12095 where distributed logger central loggers were
passed to ProjectCollection without filtering out null values.

### Customer Impact

Users running `msbuild -dfl` on MSBuild 18/VS 2026 encounter a blocking
error:
```
MSBUILD : error MSB1025: An internal failure occurred while running MSBuild.
System.ArgumentNullException: Value cannot be null. (Parameter 'logger')
```

This prevents builds from running when using the distributed file
logger, which is commonly used for multi-node build logging. The `-dfl`
flag worked correctly in MSBuild 17 and earlier versions.

**Root Cause**: When using `-dfl`, the DistributedLoggerRecord
intentionally has a null CentralLogger (as documented in XMake.cs line
4298-4299). PR #12095 introduced code that includes these central
loggers in the evaluationLoggers array without filtering nulls, causing
ProjectCollection.RegisterLoggerInternal() to throw
ArgumentNullException.

### Regression?

- **Yes** - Regression introduced in MSBuild 18 by PR #12095
- **Last working version**: MSBuild 17

### Testing

**Code Changes:**
1. **XMake.cs (line 1396)**: Added `.Where(l => l is not null)` to
filter out null central loggers from the evaluationLoggers array
2. **XMake_Tests.cs**: Added regression test
`TestNullCentralLoggerInDistributedLoggerRecord` to verify
DistributedLoggerRecords with null CentralLogger don't cause exceptions

**Verification:**
- ✅ New unit test `TestNullCentralLoggerInDistributedLoggerRecord`
passes
- ✅ Build succeeded with no warnings or errors
- ✅ Manual testing: `msbuild simple.proj -dfl -t:Build` now works
correctly and creates distributed log file
- ✅ Code review found no issues
- ✅ No security vulnerabilities detected

### Risk

**Low** - This is a minimal, surgical fix that:
- Only filters out null loggers, which are intentionally null for
distributed file loggers
- Does not change behavior for non-null loggers
- Restores MSBuild 17 functionality
- Includes regression test to prevent future breakage

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>


----

*This section details on the original issue you should resolve*

<issue_title>MsBuild 18/VS 2026 MSB1025 error when using
DistributedFileLogger - The 'logger' parameter cannot be
null.</issue_title>
<issue_description>### Issue Description

When running `msbuild -dfl` i receive such error


`MSBUILD : error MSB1025: podczas uruchamiania programu MSBuild wystąpił
błąd wewnętrzny.
System.ArgumentNullException: Parametr „logger” nie może być zerowy.
w Microsoft.Build.Shared.ErrorUtilities.ThrowArgumentNull(String
parameterName, String resourceName)
w
Microsoft.Build.Evaluation.ProjectCollection.RegisterLoggerInternal(ILogger
logger)
w
Microsoft.Build.Evaluation.ProjectCollection.RegisterLoggers(IEnumerable`1
loggers)
w Microsoft.Build.Evaluation.ProjectCollection..ctor(IDictionary`2
globalProperties, IEnumerable`1 loggers, IEnumerable`1 remoteLoggers,
ToolsetDefinitionLocations toolsetDefinitionLocations, Int32
maxNodeCount, Boolean onlyLogCriticalEvents, Boolean
loadProjectsReadOnly, Boolean useAsynchronousLogging, Boolean
reuseProjectRootElementCache, Boolean enableTargetOutputLogging)
w Microsoft.Build.CommandLine.MSBuildApp.BuildProject(String
projectFile, String[] targets, String toolsVersion, Dictionary`2
globalProperties, Dictionary`2 restoreProperties, ILogger[] loggers,
LoggerVerbosity verbosity, DistributedLoggerRecord[]
distributedLoggerRecords, Boolean needToValidateProject, String
schemaFile, Int32 cpuCount, Boolean multiThreaded, Boolean
enableNodeReuse, TextWriter preprocessWriter, TextWriter targetsWriter,
Boolean detailedSummary, ISet`1 warningsAsErrors, ISet`1
warningsNotAsErrors, ISet`1 warningsAsMessages, Boolean enableRestore,
ProfilerLogger profilerLogger, Boolean enableProfiler, Boolean
interactive, ProjectIsolationMode isolateProjects, GraphBuildOptions
graphBuildOptions, Boolean lowPriority, Boolean question, Boolean
isTaskAndTargetItemLoggingRequired, Boolean isBuildCheckEnabled,
String[] inputResultsCaches, String outputResultsCache, Boolean
saveProjectResult, BuildResult& result, Boolean reportFileAccesses,
String commandLine)
   w Microsoft.Build.CommandLine.MSBuildApp.Execute(String commandLine)`





### Steps to Reproduce

to reproduce i create simple .net framework console solution and i run
`msbuild -dfl` in solution directory

### Expected Behavior

Solution should build as in previous version of msbuild (17)


### Actual Behavior

Blocking error
`MSBUILD : error MSB1025: podczas uruchamiania programu MSBuild wystąpił
błąd wewnętrzny.
System.ArgumentNullException: Parametr „logger” nie może być zerowy.`

### Analysis

I tried to find any information in documentation about breaking changes
in DistributedFileLogger but with no success.

Trying different fileLoggerParameters did not help

Switching to fileLogger works as expected

### Versions & Configurations

Wersja programu MSBuild 18.0.5+e22287bf1 dla .NET Framework
18.0.5.56406

ran from C:\Program Files\Microsoft Visual
Studio\18\Professional\MSBuild\Current\Bin\amd64\MSBuild.rsp
and
C:\Program Files (x86)\Microsoft Visual
Studio\18\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.rsp

Windows 11 </issue_description>

<agent_instructions>Yes, this bug is very likely caused by the change in
PR #12095.
Here's the problematic code introduced in src/MSBuild/XMake.cs:
csharpILogger[] evaluationLoggers =
    [
        // all of the loggers that are single-node only
        .. loggers,
// all of the central loggers for multi-node systems. These need to be
resilient to multiple calls
        // to Initialize
        .. distributedLoggerRecords.Select(d => d.CentralLogger)
    ];

projectCollection = new ProjectCollection(
    globalProperties,
evaluationLoggers, // <-- now includes distributed logger central
loggers
    ...
The problem: When you use -dfl (distributed file logger), the
DistributedLoggerRecord.CentralLogger can be null. The code now blindly
passes all CentralLogger values into the ProjectCollection constructor
without filtering out nulls.
When ProjectCollection.RegisterLoggers() iterates over these loggers and
calls RegisterLoggerInternal(), it hits:
csharpprivate void RegisterLoggerInternal(ILogger logger)
{
ErrorUtilities.VerifyThrowArgumentNull(logger); // <-- throws here when
logger is null
    ...
}
The fix should filter out null loggers:
csharpILogger[] evaluationLoggers =
    [
        .. loggers,
        .. distributedLoggerRecords
            .Select(d => d.CentralLogger)
            .Where(l => l != null)  // <-- add this filter
    ];</agent_instructions>

## Comments on the Issue (you are @copilot in this section)

<comments>
<comment_new><author>@YuliiaKovalova</author><body>
Hi @kkapuscinski ,

Thank you for reporting the issue! It's likely to connected to
#12095 change.
We are working on the fix.

cc: @baronfel </body></comment_new>
</comments>


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes #13032

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for
you](https://github.com/dotnet/msbuild/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No output to terminal if a malformed argument is provided

5 participants