Skip to content

Fix ObjectDisposedException in GenerateSbom Task under MSBuild Server - #1529

Open
Chet Husk (baronfel) wants to merge 1 commit into
microsoft:mainfrom
baronfel:baronfel-fix-ansiconsole-reuse
Open

Fix ObjectDisposedException in GenerateSbom Task under MSBuild Server#1529
Chet Husk (baronfel) wants to merge 1 commit into
microsoft:mainfrom
baronfel:baronfel-fix-ansiconsole-reuse

Conversation

@baronfel

Copy link
Copy Markdown
Member

Summary

Fixes an ObjectDisposedException in Microsoft.Sbom.Targets.GenerateSbom when running under a reused MSBuild Server process (e.g., MSBuild Server node reuse across multiple builds in the same VS/CLI session).

Root cause, upstream context, and fix rationale are tracked in dotnet/msbuild#14691.

Fixes #712

Root cause

GenerateSbom builds a fresh DI container on every task invocation via AddSbomTool(), which calls ComponentDetection's AddComponentDetection(). ComponentDetection's DetectorProcessingService does not receive an explicit Spectre.Console.IAnsiConsole from the DI container, so it falls back to the process-wide static AnsiConsole.Console singleton, which is bound to whatever Console.Out was current the first time it was touched in the process.

This is a cardinal sin of MSBuild Tasks - they should not rely on ambient/environmental state. They should only interact with the build environment through the APIs/mechanisms provided by the IBuildEngineX APIs or the new TaskEnvironment contextual parameter.

Under MSBuild Server reuse, build 1 sets up a RedirectConsoleWriter around Console.Out and disposes it when build 1 finishes. Build 2 reuses the same process; ComponentDetection still holds the (now-disposed) writer via the static AnsiConsole.Console singleton, and any call into DetectorProcessingService.LogTabularOutput (which renders the "Detection Summary" table via Spectre) throws ObjectDisposedException, producing empty/failed SBOM generation on the second and subsequent invocations in the same process.

Fix

AddSbomTool() now registers a local, non-static IAnsiConsole in the service collection before calling AddComponentDetection(), so ComponentDetection resolves this instance instead of falling back to the static singleton. The console is created via AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(TextWriter.Null) }) — a fresh, per-invocation instance backed by TextWriter.Null rather than the process's Console.Out.

This is intentional and low-risk: the only thing ComponentDetection renders through IAnsiConsole is a decorative "Detection Summary" table (gated by !settings.NoSummary) in DetectorProcessingService.LogTabularOutput. The same data is already independently emitted through ILogger, and ServiceCollectionExtensions.CreateLogger() in this repo already excludes that verbose per-detector timing/summary data from the console sink on purpose (it's meant for the log file only). Discarding the Spectre summary table via TextWriter.Null therefore has no effect on the actual SBOM manifest, detected packages/files, or logged output — only on a summary table that was already suppressed from the console in this tool's logging configuration.

Testing

  • New unit tests in Microsoft.Sbom.Extensions.DependencyInjection.Tests assert that AddSbomTool() resolves an IAnsiConsole from the container that is not the static AnsiConsole.Console singleton, across repeated container builds.
  • New end-to-end regression tests in Microsoft.Sbom.Targets.Tests invoke GenerateSbom twice in-process, replacing and disposing Console.Out between invocations (simulating MSBuild Server's build-boundary RedirectConsoleWriter disposal). Both invocations succeed and produce a valid manifest with non-empty package data, on both net8.0 and net472. Before the fix, the second invocation throws ObjectDisposedException.
  • Full solution build passes (dotnet build and dotnet test on the affected projects).

Files changed

  • src/Microsoft.Sbom.Extensions.DependencyInjection/ServiceCollectionExtensions.cs — register invocation-local IAnsiConsole before AddComponentDetection().
  • src/Microsoft.Sbom.Extensions.DependencyInjection/Microsoft.Sbom.Extensions.DependencyInjection.csproj — explicit Spectre.Console package reference (central package management).
  • test/Microsoft.Sbom.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs — new DI resolution regression tests.
  • test/Microsoft.Sbom.Targets.Tests/AbstractGenerateSbomTaskTests.cs — new repeated in-process invocation regression test.
  • test/Microsoft.Sbom.Targets.Tests/GeneratedSbomValidator.cs — new lightweight package-data validator assertion used by the regression test.

Closes/relates to dotnet/msbuild#14691.

Register an invocation-local null-output IAnsiConsole so component detection cannot retain a disposed process Console.Out writer. Add repeated in-process generation coverage that verifies detected package data survives server reuse.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f64399fe-4312-4d2f-94af-9044fb5cf135
@baronfel
Chet Husk (baronfel) requested a review from a team as a code owner August 12, 2026 15:17
@baronfel Chet Husk (baronfel) changed the title Fix OBjectDisposedException in GenerateSbom Task under MSBuild Server Fix ObjectDisposedException in GenerateSbom Task under MSBuild Server Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SBOM Task Outputs Directly to Console Instead of Using MSBuild Logging APIs

1 participant