Skip to content

refactor: modernize RunProcessAsync with WaitForExitAsync#1790

Open
JamieMagee wants to merge 1 commit intomainfrom
cleanup/process-waitforexitasync
Open

refactor: modernize RunProcessAsync with WaitForExitAsync#1790
JamieMagee wants to merge 1 commit intomainfrom
cleanup/process-waitforexitasync

Conversation

@JamieMagee
Copy link
Copy Markdown
Member

What

Rewrites CommandLineInvocationService.RunProcessAsync to use the modern .NET process APIs:

  • Process.WaitForExitAsync(CancellationToken) (.NET 5+) instead of a TaskCompletionSource driven by the Process.Exited event.
  • StreamReader.ReadToEndAsync(CancellationToken) (.NET 7+) instead of new Task(() => ReadToEnd()).Start() (an anti-pattern that scheduled cold tasks on the default scheduler).
  • Cancellation now kills the entire process tree (process.Kill(entireProcessTree: true)) and re-throws OperationCanceledException.

The two private overloads collapse into a single signature with an optional CancellationToken. The .cmd/.batcmd /C shim is preserved unchanged.

Why

The previous implementation predates WaitForExitAsync and used several patterns that are no longer recommended:

  • new Task(() => ...).Start() — cold tasks bypass async/await scheduling and are easy to get wrong.
  • Task.WaitAll(t1, t2) inside an event handler — synchronously blocks a threadpool thread.
  • A TaskCompletionSource to bridge the event-based completion — entirely replaced by WaitForExitAsync.

The new code is roughly half the size, propagates cancellation correctly, and avoids blocking threadpool threads.

Risk

This is the highest-risk PR in the cleanup series — every detector that shells out (gradle, maven, mvn-cli, pip, go, rust, vcpkg, pnpm, etc.) goes through this code path. The full test suite passes locally.

Part 6 of a 6-PR cleanup series removing .NET-Framework-era anachronisms.

Replace the legacy TaskCompletionSource + cold-Task pattern in CommandLineInvocationService.RunProcessAsync with Process.WaitForExitAsync(CancellationToken) (.NET 5+) and StreamReader.ReadToEndAsync(CancellationToken) (.NET 7+). Cancellation now kills the entire process tree and propagates OperationCanceledException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 17, 2026 18:25
@JamieMagee JamieMagee requested a review from a team as a code owner April 17, 2026 18:25
@JamieMagee JamieMagee requested a review from AMaini503 April 17, 2026 18:25
Copy link
Copy Markdown
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

Refactors CommandLineInvocationService.RunProcessAsync in the Common library to use modern .NET Process async APIs for more reliable non-blocking execution and improved cancellation handling across all detectors that shell out to external tools.

Changes:

  • Replaces Process.Exited + TaskCompletionSource completion with Process.WaitForExitAsync(CancellationToken).
  • Replaces new Task(...).Start() stream draining with concurrent StreamReader.ReadToEndAsync(...).
  • On cancellation, kills the entire process tree and rethrows OperationCanceledException; removes redundant overload.
Show a summary per file
File Description
src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs Modernizes process execution, output/error capture, and cancellation behavior for all command-line invocations.

Copilot's findings

Comments suppressed due to low confidence (1)

src/Microsoft.ComponentDetection.Common/CommandLineInvocationService.cs:182

  • The updated cancellation behavior (waiting via WaitForExitAsync, killing the entire process tree on cancel, and rethrowing OperationCanceledException) is not covered by the existing CommandLineInvocationServiceTests. Adding tests that (1) cancel a long-running process and assert the process is terminated and (2) ensure late cancellation after process exit doesn’t turn a successful run into an OperationCanceledException would help prevent regressions in this high-impact code path.
        try
        {
            await process.WaitForExitAsync(cancellationToken);
        }
        catch (OperationCanceledException)
        {
            try
            {
                process.Kill(entireProcessTree: true);
            }
            catch (InvalidOperationException)
            {
                // Process already exited.
            }

            throw;
        }
  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment on lines +163 to +164
var stdOutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
var stdErrTask = process.StandardError.ReadToEndAsync(cancellationToken);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

ReadToEndAsync(cancellationToken) can throw OperationCanceledException if the token is canceled after WaitForExitAsync completes but before the stream-read tasks finish. That can cause this method to surface cancellation even though the process already exited successfully. Consider not passing the external cancellation token to the output/error reads (or switch to a separate token that is only canceled when you actually intend to abort the read), and rely on cancellation to kill the process instead.

This issue also appears on line 166 of the same file.

Suggested change
var stdOutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
var stdErrTask = process.StandardError.ReadToEndAsync(cancellationToken);
// Do not pass the external cancellation token here: cancellation is handled by
// WaitForExitAsync below, which kills the process. Once the process exits, these
// reads should be allowed to complete so we do not surface cancellation after a
// successful process exit.
var stdOutTask = process.StandardOutput.ReadToEndAsync();
var stdErrTask = process.StandardError.ReadToEndAsync();

Copilot uses AI. Check for mistakes.
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.

2 participants