Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -95,46 +95,88 @@ private bool GetHasExited(bool refresh)
}

private void KillTree(ref List<Exception>? exceptions)
{
// First, stop and collect the entire process tree before killing any process.
// This is necessary because killing a parent process may cause children with
// PR_SET_PDEATHSIG (KillOnParentExit) to be killed by the kernel immediately,
// making it impossible to discover their descendants afterward.
List<Process> stoppedProcesses = [];
try
{
StopTree(ref exceptions, stoppedProcesses);
}
catch (Exception ex)
{
(exceptions ??= new List<Exception>()).Add(ex);
}
finally
{
// Kill all stopped processes even if StopTree threw partway through.
foreach (Process process in stoppedProcesses)
{
int killResult = Interop.Sys.Kill(process._processId, Interop.Sys.GetPlatformSignalNumber(PosixSignal.SIGKILL));
if (killResult != 0)
{
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
// Ignore 'process no longer exists' error.
if (errorInfo.Error != Interop.Error.ESRCH)
{
(exceptions ??= new List<Exception>()).Add(new Win32Exception(errorInfo.RawErrno));
}
}

if (process != this)
{
process.Dispose();
}
}
}
}
Comment thread
adamsitnik marked this conversation as resolved.

/// <summary>
/// Recursively stops all processes in the tree (depth-first) and collects them into a flat list.
/// Stopping (SIGSTOP) before enumerating children ensures we can discover the entire tree
/// before any kill signals cause cascading terminations.
/// </summary>
/// <returns>
/// <see langword="true"/> if the process was successfully stopped and added to
/// <paramref name="stoppedProcesses"/>; <see langword="false"/> if the process had already
/// exited or could not be stopped, in which case the caller is responsible for disposing it.
/// </returns>
private bool StopTree(ref List<Exception>? exceptions, List<Process> stoppedProcesses)
{
// If the process has exited, we can no longer determine its children.
// If we know the process has exited, stop already.
if (GetHasExited(refresh: false))
{
return;
return false;
}

// Stop the process, so it won't start additional children.
// This is best effort: kill can return before the process is stopped.
int stopResult = Interop.Sys.Kill(_processId, Interop.Sys.GetPlatformSIGSTOP());
if (stopResult != 0)
{
Interop.Error error = Interop.Sys.GetLastError();
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
// Ignore 'process no longer exists' error.
if (error != Interop.Error.ESRCH)
if (errorInfo.Error != Interop.Error.ESRCH)
{
(exceptions ??= new List<Exception>()).Add(new Win32Exception());
(exceptions ??= new List<Exception>()).Add(new Win32Exception(errorInfo.RawErrno));
}
return;
return false;
}

List<Process> children = GetChildProcesses();
stoppedProcesses.Add(this);

int killResult = Interop.Sys.Kill(_processId, Interop.Sys.GetPlatformSignalNumber(PosixSignal.SIGKILL));
if (killResult != 0)
List<Process> children = GetChildProcesses();
foreach (Process childProcess in children)
{
Interop.Error error = Interop.Sys.GetLastError();
// Ignore 'process no longer exists' error.
if (error != Interop.Error.ESRCH)
if (!childProcess.StopTree(ref exceptions, stoppedProcesses))
{
(exceptions ??= new List<Exception>()).Add(new Win32Exception());
childProcess.Dispose();
}
}
Comment thread
adamsitnik marked this conversation as resolved.

foreach (Process childProcess in children)
{
childProcess.KillTree(ref exceptions);
childProcess.Dispose();
}
return true;
}

/// <summary>Discards any information about the associated process.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

Expand Down Expand Up @@ -145,6 +146,65 @@ public void KillOnParentExit_KillsTheChild_WhenParentIsKilled(bool enabled, bool
}
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true)]
[InlineData(false)]
public void KillEntireProcessTree_KillsGrandchild_WhenIntermediateChildHasKillOnParentExit(bool enabled)
{
// Mimics the scenario: Process A -> Process B (KillOnParentExit) -> Process C
// Killing A with entireProcessTree:true should kill C regardless of KillOnParentExit setting.
RemoteInvokeOptions parentOptions = new() { CheckExitCode = false };
parentOptions.StartInfo.RedirectStandardOutput = true;
parentOptions.StartInfo.RedirectStandardInput = true;

using RemoteInvokeHandle parentHandle = RemoteExecutor.Invoke(
(enabledStr) =>
{
// This is "Process A". Start "Process B" with KillOnParentExit.
// Process B will start "Process C" and report C's PID.
using Process child = CreateProcess(() =>
{
// This is "Process B". Start "Process C" (long-running, no KillOnParentExit).
using Process grandChild = CreateProcessLong();
grandChild.Start();
Console.WriteLine(grandChild.Id);

// Block until killed
Thread.Sleep(Timeout.Infinite);
return RemoteExecutor.SuccessExitCode;
});
child.StartInfo.KillOnParentExit = bool.Parse(enabledStr);
child.StartInfo.RedirectStandardOutput = true;
child.Start();

// Read grandchild PID from Process B and forward to test
string grandChildPidStr = child.StandardOutput.ReadLine();
Console.WriteLine(grandChildPidStr);

// Block until killed
Thread.Sleep(Timeout.Infinite);
},
enabled.ToString(),
parentOptions);

int grandChildPid = int.Parse(parentHandle.Process.StandardOutput.ReadLine());
using Process grandchild = Process.GetProcessById(grandChildPid);

try
{
// Kill Process A with entireProcessTree: true
parentHandle.Process.Kill(entireProcessTree: true);

Assert.True(parentHandle.Process.WaitForExit(WaitInMS));
// The grandchild (Process C) should also be killed
Assert.True(grandchild.WaitForExit(WaitInMS));
}
finally
{
grandchild.Kill();
}
Comment thread
adamsitnik marked this conversation as resolved.
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true, true)]
[InlineData(true, false)]
Expand Down
Loading