You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
On .NET Core 3.x and .NET 5–7, cancelling a command leaves its child processes running, though README says the whole process tree is terminated on .NET Core 3.0+ #134
README.md (line 182) says: "On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated."
The package doesn't ship builds for .NET Core 3.x or for net5.0, net6.0 or net7.0. ktsu.Sdk sets TargetFrameworks to net10.0;net9.0;net8.0;netstandard2.0;netstandard2.1, and the 1.9.x nupkgs contain only those lib/ folders. An app on .NET Core 3.1 or on .NET 5, 6 or 7 therefore resolves the netstandard2.1 build, which compiles Kill to a plain process.Kill() (RunCommand.cs#L578-L584). On those runtimes a cancelled run kills only the direct child and every grandchild keeps running, even though Process.Kill(bool entireProcessTree) exists on all of them. It just can't be called statically from the netstandard2.1 reference surface.
CLAUDE.md (line 49) also still lists net7.0, net6.0 and net5.0 as targets.
Repro
This was run on Linux, loading each build in a .NET runtime:
usingvarcts=newCancellationTokenSource(300);try{awaitRunCommand.ExecuteAsync("sh",["-c","sleep 37; echo done"],newOutputHandler(),cts.Token);}catch(OperationCanceledException){}awaitTask.Delay(500);// then: pgrep -a -f '^sleep 37$'
Real commands spawn build servers, test hosts and npm children. On these runtimes those processes outlive the cancellation, although the README promises they are cleaned up.
Suggested fix
In the NETSTANDARD2_0 || NETSTANDARD2_1 branch, look up the overload once at runtime and use it when it is present:
privatestaticreadonlyMethodInfo?KillTree=typeof(Process).GetMethod("Kill",[typeof(bool)]);privatestaticvoidKill(Processprocess){if(KillTreeis not null){try{KillTree.Invoke(process,[true]);}catch(TargetInvocationExceptione)when(e.InnerExceptionis not null){ExceptionDispatchInfo.Capture(e.InnerException).Throw();}}else{process.Kill();}}
Unwrapping TargetInvocationException keeps TryKill's existing handling of AggregateException, InvalidOperationException and Win32Exception (#107) working. On .NET Framework and Mono the method is absent, so behaviour there doesn't change.
Those runtimes are out of support, so fixing only the documentation is a valid alternative. In that case the README needs to say that tree kill applies only to the net8.0+ builds.
Acceptance criteria
When loaded on a .NET Core 3.0+ runtime, the netstandard2.1 and netstandard2.0 builds kill grandchildren on cancellation, matching the net8+ builds. Alternatively, the README states exactly which builds get tree kill.
A test covers the reflection path and asserts that a sleep grandchild is gone after cancellation.
The README's tree-termination sentence and CLAUDE.md's "Process-tree termination" note and target list match what actually ships.
Category: Bug (a mismatch between the docs and the shipped behaviour)
Priority: Low. Only .NET Core 3.x and .NET 5–7 are affected, and all of them are out of support. The net8.0+ builds already kill the whole tree. Leftover grandchildren are a real cost on those runtimes, but few consumers should still be on them.
Area / suggested assignment: The Kill helper under NETSTANDARD2_0 || NETSTANDARD2_1 in RunCommand/RunCommand.cs, plus the README's tree-termination sentence and CLAUDE.md's target list.
Next step: Correct the README and CLAUDE.md now; that change is cheap and accurate either way. The reflection-based Kill(true) is a small follow-up if supporting those runtimes is worth a test that runs on a .NET Core 3+ host against the netstandard build.
Activity
matt-edmondson commentedon Nov 2, 2026
Triage
Killhelper underNETSTANDARD2_0 || NETSTANDARD2_1inRunCommand/RunCommand.cs, plus the README's tree-termination sentence and CLAUDE.md's target list.TargetInvocationExceptionunwrap does.Kill(true)is a small follow-up if supporting those runtimes is worth a test that runs on a .NET Core 3+ host against the netstandard build.Generated by Claude Code