Coordinate Copilot CLI stderr pump cleanup#1136
Open
xoofx wants to merge 4 commits intogithub:mainfrom
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes the .NET SDK’s Copilot CLI stderr reader an owned part of the connection lifecycle to avoid fire-and-forget task races during shutdown/startup failures, and to prevent unobserved task exceptions.
Changes:
- Replaces the previous background stderr read loop with an owned
ProcessStderrPumpthat has explicit cancellation, completion tracking, and buffered stderr capture. - Coordinates connection cleanup to cancel/await/dispose the stderr pump before disposing the CLI process.
- Updates CLI startup to create the pump and to cancel/observe it during startup-failure cleanup.
Comments suppressed due to low confidence (1)
dotnet/src/Client.cs:230
- If connection startup fails after spawning the CLI (e.g., ConnectToServerAsync throws, protocol negotiation fails, or ConfigureSessionFsAsync throws), the returned Process/StderrPump are never cleaned up because StartCoreAsync doesn't wrap those subsequent awaits in a try/catch/finally. This can leave an orphaned copilot process and a running stderr pump, and later StopAsync/DisposeAsync won't clean up because _connectionTask is faulted. Consider capturing cliProcess/stderrPump in locals and, on any exception after StartCliServerAsync returns, cancel the pump, attempt to kill the process, await pump completion (with a bounded timeout), then dispose both before rethrowing (and ideally reset _connectionTask).
var (cliProcess, portOrNull, stderrPump) = await StartCliServerAsync(_options, _logger, ct);
_actualPort = portOrNull;
result = ConnectToServerAsync(cliProcess, portOrNull is null ? null : "localhost", portOrNull, stderrPump, ct);
}
var connection = await result;
// Verify protocol version compatibility
await VerifyProtocolVersionAsync(connection, ct);
await ConfigureSessionFsAsync(ct);
|
Great approved |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hello,
Discovered this bug when embedding the .NET SDK in an application. The fire-and-forget task had an exception (the CLI process already exited) and I had a
TaskSchedule.UnobservedTaskExceptionthat was reacting to this.Summary
Fixes unsafe stderr pumping in the .NET
CopilotClientby making the stderr reader an owned part of the connection lifecycle.Previously, the CLI stderr reader was started as a fire-and-forget task. That meant cleanup could dispose the underlying
Processwhile the background task was still reading fromStandardError, creating a race during shutdown or failed startup.Problem
CopilotClientstarts a child Copilot CLI process and continuously reads its stderr so that CLI diagnostics can be logged and included in connection failure messages.Before this change, that stderr pump had a few lifecycle issues:
_ = Task.Run(...), so it was never stored or observedProcess.HasExited, which can throw or race if another path disposes the processThis can happen during normal client shutdown, force-stop, startup failures, failed protocol negotiation, or any path where the CLI process exits while cleanup is running.
Fix
This PR introduces an owned
ProcessStderrPumpand attaches it to the connection state.The pump now:
Connection cleanup now coordinates shutdown by:
Startup failure cleanup also cancels and observes the pump before disposing the process.
Purpose
The purpose of this fix is to make Copilot CLI process cleanup deterministic and safe. The stderr reader should not outlive the connection/process it depends on, and process disposal should not race against a background task that is still reading from the process streams.
Verification
dotnet build dotnet/src/GitHub.Copilot.SDK.csproj -c Release --no-restore -v minimaldotnet test dotnet/test/GitHub.Copilot.SDK.Test.csproj -c Release --no-restore -v minimal --filter Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start