Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 37 additions & 46 deletions src/ModelContextProtocol.Core/ProcessHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,25 @@ namespace ModelContextProtocol;
/// </summary>
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>
/// Kills a process and all of its child processes (entire process tree) with a specified timeout.
/// </summary>
/// <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>
/// <remarks>
/// On Windows, this uses the "taskkill" command with the /T flag to terminate the process tree.
/// On non-Windows platforms, it recursively identifies and terminates child processes.
/// On .NET Core 3.0+ this uses <c>Process.Kill(entireProcessTree: true)</c>.
/// 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.
/// This is particularly useful for applications that spawn child processes (like Node.js)
/// that wouldn't be terminated automatically when the parent process exits.
/// </remarks>
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;
if (_isWindows)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
RunProcessAndWaitForExit(
"taskkill",
Expand All @@ -53,50 +42,50 @@ public static void KillTree(this Process process, TimeSpan timeout)
{
KillProcessUnix(childId, 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.
// 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
// to try and flush what it can and stop. If it cannot do that in a reasonable time frame then we will just ignore it.
process.WaitForExit((int)timeout.TotalMilliseconds);
}

#if NETSTANDARD2_0
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
{
var exitcode = RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out var stdout);
int exitcode = RunProcessAndWaitForExit("pgrep", $"-P {parentId}", timeout, out var stdout);

if (exitcode == 0 && !string.IsNullOrEmpty(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))
{
children.Add(id);

// Recursively get the children
GetAllChildIdsUnix(id, children, timeout);
}
}
}
}

private static void KillProcessUnix(int processId, TimeSpan timeout)
{
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out var _);
}
private static void KillProcessUnix(int processId, TimeSpan timeout) =>
RunProcessAndWaitForExit("kill", $"-TERM {processId}", timeout, out _);

private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout)
{
Expand All @@ -112,19 +101,21 @@ private static int RunProcessAndWaitForExit(string fileName, string arguments, T

stdout = null;

var process = Process.Start(startInfo);
if (process == null)
return -1;

if (process.WaitForExit((int)timeout.TotalMilliseconds))
{
stdout = process.StandardOutput.ReadToEnd();
}
else
if (Process.Start(startInfo) is { } process)
{
process.Kill();
if (process.WaitForExit((int)timeout.TotalMilliseconds))
{
stdout = process.StandardOutput.ReadToEnd();
}
else
{
process.Kill();
}

return process.ExitCode;
}

return process.ExitCode;
return -1;
}
#endif
}