What's wrong
RunAsync's non-elevated branch races outputReader.Start() against process.WaitForExitAsync(cancellationToken) via Task.WhenAll (RunCommand/RunCommand.cs), with cancellation wired to TryKill(process). AsyncProcessStreamReader.ReadAndCallback (RunCommand/AsyncProcessStreamReader.cs:75) calls streamReader.ReadAsync(buffer, 0, buffer.Length) with no CancellationToken — it only returns on new data or pipe EOF.
Why it matters (failure scenario)
If the launched command spawns a child that inherits the stdout/stderr handles and outlives the immediate process — e.g. sh -c "nohup sleep 300 >&1 &", or any double-fork/setsid pattern — TryKill's process.Kill(entireProcessTree: true) kills the direct child but the detached grandchild keeps a write handle open on the pipe. EOF never arrives, so ReadAsync never returns, outputReader.Start() never completes, and Task.WhenAll — and therefore the whole ExecuteAsync call — hangs indefinitely even though the caller cancelled it. This directly contradicts the library's own documented cancellation contract ("cancellation kills the process" and lets the await proceed). It's worse on netstandard2.0/2.1 targets, where only a single-process kill is available, so even a non-detached immediate child survives and triggers the same hang.
Suggested fix / acceptance criteria
Bound the stream read by the cancellation token (pass it into ReadAsync, or race it with Task.WhenAny against a cancellation-linked task) so a cancelled call always returns promptly regardless of whether the pipe's write end is still held open by an orphaned descendant. Add a regression test that spawns a detached/background child inheriting stdout, cancels immediately, and asserts ExecuteAsync returns (throwing OperationCanceledException) within a bounded time instead of hanging.
What's wrong
RunAsync's non-elevated branch racesoutputReader.Start()againstprocess.WaitForExitAsync(cancellationToken)viaTask.WhenAll(RunCommand/RunCommand.cs), with cancellation wired toTryKill(process).AsyncProcessStreamReader.ReadAndCallback(RunCommand/AsyncProcessStreamReader.cs:75) callsstreamReader.ReadAsync(buffer, 0, buffer.Length)with noCancellationToken— it only returns on new data or pipe EOF.Why it matters (failure scenario)
If the launched command spawns a child that inherits the stdout/stderr handles and outlives the immediate process — e.g.
sh -c "nohup sleep 300 >&1 &", or any double-fork/setsidpattern —TryKill'sprocess.Kill(entireProcessTree: true)kills the direct child but the detached grandchild keeps a write handle open on the pipe. EOF never arrives, soReadAsyncnever returns,outputReader.Start()never completes, andTask.WhenAll— and therefore the wholeExecuteAsynccall — hangs indefinitely even though the caller cancelled it. This directly contradicts the library's own documented cancellation contract ("cancellation kills the process" and lets the await proceed). It's worse on netstandard2.0/2.1 targets, where only a single-process kill is available, so even a non-detached immediate child survives and triggers the same hang.Suggested fix / acceptance criteria
Bound the stream read by the cancellation token (pass it into
ReadAsync, or race it withTask.WhenAnyagainst a cancellation-linked task) so a cancelled call always returns promptly regardless of whether the pipe's write end is still held open by an orphaned descendant. Add a regression test that spawns a detached/background child inheriting stdout, cancels immediately, and assertsExecuteAsyncreturns (throwingOperationCanceledException) within a bounded time instead of hanging.