Use Process.Kill(entireProcessTree: true) on .NET for faster process termination (#1187)

This commit is contained in:
Stephen Toub 2026-01-29 14:29:08 -05:00 • committed by GitHub
commit aaf2246d53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,36 +8,25 @@ namespace ModelContextProtocol;
/// </summary> /// </summary>
internal static class ProcessHelper internal static class ProcessHelper
{ {
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
/// <summary>
/// Kills a process and all of its child processes (entire process tree).
/// </summary>
/// <param name="process">The process to terminate along with its child processes.</param>
/// <remarks>
/// This method uses a default timeout of 30 seconds when waiting for processes to exit.
/// On Windows, this uses the "taskkill" command with the /T flag.
/// On non-Windows platforms, it recursively identifies and terminates child processes.
/// </remarks>
public static void KillTree(this Process process) => process.KillTree(_defaultTimeout);
/// <summary> /// <summary>
/// Kills a process and all of its child processes (entire process tree) with a specified timeout. /// Kills a process and all of its child processes (entire process tree) with a specified timeout.
/// </summary> /// </summary>
/// <param name="process">The process to terminate along with its child processes.</param> /// <param name="process">The process to terminate along with its child processes.</param>
/// <param name="timeout">The maximum time to wait for the processes to exit.</param> /// <param name="timeout">The maximum time to wait for the processes to exit.</param>
/// <remarks> /// <remarks>
/// On Windows, this uses the "taskkill" command with the /T flag to terminate the process tree. /// On .NET Core 3.0+ this uses <c>Process.Kill(entireProcessTree: true)</c>.
/// On non-Windows platforms, it recursively identifies and terminates child processes. /// On .NET Standard 2.0, it uses platform-specific commands (taskkill on Windows, pgrep/kill on Unix).
/// The method waits for the specified timeout for processes to exit before continuing. /// The method waits for the specified timeout for processes to exit before continuing.
/// This is particularly useful for applications that spawn child processes (like Node.js) /// This is particularly useful for applications that spawn child processes (like Node.js)
/// that wouldn't be terminated automatically when the parent process exits. /// that wouldn't be terminated automatically when the parent process exits.
/// </remarks> /// </remarks>
public static void KillTree(this Process process, TimeSpan timeout) public static void KillTree(this Process process, TimeSpan timeout)
{ {
#if NETSTANDARD2_0
// Process.Kill(entireProcessTree) is not available on .NET Standard 2.0.
// Use platform-specific commands to kill the process tree.
var pid = process.Id; var pid = process.Id;
if (_isWindows) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
RunProcessAndWaitForExit( RunProcessAndWaitForExit(
"taskkill", "taskkill",
@ -53,8 +42,20 @@ internal static class ProcessHelper
{ {
KillProcessUnix(childId, timeout); KillProcessUnix(childId, timeout);
} }
KillProcessUnix(pid, timeout); KillProcessUnix(pid, timeout);
} }
#else
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// Process has already exited
return;
}
#endif
// wait until the process finishes exiting/getting killed. // wait until the process finishes exiting/getting killed.
// We don't want to wait forever here because the task is already supposed to be dieing, we just want to give it long enough // We don't want to wait forever here because the task is already supposed to be dieing, we just want to give it long enough
@ -62,26 +63,20 @@ internal static class ProcessHelper
process.WaitForExit((int)timeout.TotalMilliseconds); process.WaitForExit((int)timeout.TotalMilliseconds);
} }
#if NETSTANDARD2_0
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout) private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
{ {
var exitcode = RunProcessAndWaitForExit( int exitcode = RunProcessAndWaitForExit("pgrep", $"-P {parentId}", timeout, out var stdout);
"pgrep",
$"-P {parentId}",
timeout,
out var stdout);
if (exitcode == 0 && !string.IsNullOrEmpty(stdout)) if (exitcode == 0 && !string.IsNullOrEmpty(stdout))
{ {
using var reader = new StringReader(stdout); using var reader = new StringReader(stdout);
while (true) while (reader.ReadLine() is string text)
{ {
var text = reader.ReadLine();
if (text == null)
return;
if (int.TryParse(text, out var id)) if (int.TryParse(text, out var id))
{ {
children.Add(id); children.Add(id);
// Recursively get the children // Recursively get the children
GetAllChildIdsUnix(id, children, timeout); GetAllChildIdsUnix(id, children, timeout);
} }
@ -89,14 +84,8 @@ internal static class ProcessHelper
} }
} }
private static void KillProcessUnix(int processId, TimeSpan timeout) private static void KillProcessUnix(int processId, TimeSpan timeout) =>
{ RunProcessAndWaitForExit("kill", $"-TERM {processId}", timeout, out _);
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out var _);
}
private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout) private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout)
{ {
@ -112,19 +101,21 @@ internal static class ProcessHelper
stdout = null; stdout = null;
var process = Process.Start(startInfo); if (Process.Start(startInfo) is { } process)
if (process == null) {
return -1; if (process.WaitForExit((int)timeout.TotalMilliseconds))
{
stdout = process.StandardOutput.ReadToEnd();
}
else
{
process.Kill();
}
if (process.WaitForExit((int)timeout.TotalMilliseconds)) return process.ExitCode;
{
stdout = process.StandardOutput.ReadToEnd();
}
else
{
process.Kill();
} }
return process.ExitCode; return -1;
} }
#endif
} }