Skip to content

Fix process crash when testing Stderr#1449

Open
ericstj wants to merge 2 commits intomodelcontextprotocol:mainfrom
ericstj:fix1448
Open

Fix process crash when testing Stderr#1449
ericstj wants to merge 2 commits intomodelcontextprotocol:mainfrom
ericstj:fix1448

Conversation

@ericstj
Copy link
Contributor

@ericstj ericstj commented Mar 19, 2026

// WaitForExit(int) (as used by KillTree) does not. We must call
// this whether the process exited on its own or was killed above,
// otherwise ErrorDataReceived handlers may fire after Dispose().
if (HasExited(process))
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this change or the comment on it. If HasExited return false, how does this prevent ErrorDataReceived from firing after Dispose?

Copy link
Contributor Author

@ericstj ericstj Mar 20, 2026

Choose a reason for hiding this comment

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

Here's what Copilot says:

The guard is there because WaitForExit() (no-arg) blocks indefinitely if the process hasn't exited.

Consider the scenarios at that point in the code:

Scenario HasExited What happens
Process exited on its own true Call WaitForExit() to flush events ✓
KillTree killed it true Call WaitForExit() to flush events ✓
KillTree called but process didn't die (edge case) false Skip — avoids hanging forever
Process never started (processStarted=false) throws → true Caught by the outer try

Without the guard, an unkillable process (or a race between kill and exit check) would cause WaitForExit() to hang the calling thread indefinitely. The tradeoff is that in that rare edge case, some ErrorDataReceived events may not be fully dispatched before Dispose() — but that's better than a deadlock.

HasExited returns true when the process has exited, but the no-arg WaitForExit() does additional work beyond waiting for exit — it drains the async stdout/stderr output readers.

Here are the docs that confirm that indeed you must call WaitForExit() to flush redirected events.
https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=net-10.0#system-diagnostics-process-waitforexit(system-int32)

Seems we could have a better name such an API - like Flush() or something.

Copy link
Contributor

@stephentoub stephentoub Mar 20, 2026

Choose a reason for hiding this comment

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

I do not understand that response. My question wasn't about whether there needs to be a guard on the HasExited call... there already was a guard prior to this PR. This PR is just changing that guard from:

if (!processRunning && HasExited(process))

to

if (HasExited(process))

and the value of processRunning is not impacted at all based on whether KillTree was successful or not.

Moreover, the comment that was added suggests that "we must call [WaitForExit] [...] otherwise ErrorDataReceived handlers may fire after Dispose()" but there's nothing here preventing that race condition when we don't call WaitForExit because the process hasn't yet exited by the time we get here. So the logic is lost on me.

What am I missing?

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.

CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked Test crash in CI

2 participants