diff --git a/source/Nuke.Tooling/IProcess.cs b/source/Nuke.Tooling/IProcess.cs
index 365d26b53..18d9d57b5 100644
--- a/source/Nuke.Tooling/IProcess.cs
+++ b/source/Nuke.Tooling/IProcess.cs
@@ -57,7 +57,16 @@ public interface IProcess : IDisposable
void Kill();
///
- /// Waits for the process to exit. If the process is not exiting within a given timeout, is called.
+ /// Calls platform specific code to kill entire process tree.
+ ///
+ ///
+ /// For Windows it call "taskkill /T /F /PID {process.Id}"
+ /// For Unix it call combination of "pgrep and kill commands"
+ ///
+ void KillTree();
+
+ ///
+ /// Waits for the process to exit. If the process is not exiting within a given timeout, is called.
///
///
/// Returns true, if the process exited on its own.
diff --git a/source/Nuke.Tooling/NativeProcessExtension.cs b/source/Nuke.Tooling/NativeProcessExtension.cs
new file mode 100644
index 000000000..920ed986e
--- /dev/null
+++ b/source/Nuke.Tooling/NativeProcessExtension.cs
@@ -0,0 +1,114 @@
+// Copyright 2023 Maintainers of NUKE.
+// Distributed under the MIT License.
+// https://github.com/nuke-build/nuke/blob/master/LICENSE
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Nuke.Common.Tooling;
+
+// https://raw.githubusercontent.com/dotnet/cli/master/test/Microsoft.DotNet.Tools.Tests.Utilities/Extensions/ProcessExtensions.cs
+internal static class NativeProcessExtension
+{
+ private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
+
+ internal static void KillTree(this Process process)
+ {
+ process.KillTree(_defaultTimeout);
+ }
+
+ internal static void KillTree(this Process process, TimeSpan timeout)
+ {
+ string stdout;
+ if (_isWindows)
+ {
+ RunProcessAndWaitForExit(
+ "taskkill",
+ $"/T /F /PID {process.Id}",
+ timeout,
+ out stdout);
+ }
+ else
+ {
+ var children = new HashSet();
+ GetAllChildIdsUnix(process.Id, children, timeout);
+ foreach (var childId in children)
+ {
+ KillProcessUnix(childId, timeout);
+ }
+ KillProcessUnix(process.Id, timeout);
+ }
+ }
+
+ private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout)
+ {
+ string stdout;
+ var exitCode = RunProcessAndWaitForExit(
+ "pgrep",
+ $"-P {parentId}",
+ timeout,
+ out stdout);
+
+ if (exitCode == 0 && !string.IsNullOrEmpty(stdout))
+ {
+ using (var reader = new StringReader(stdout))
+ {
+ while (true)
+ {
+ var text = reader.ReadLine();
+ if (text == null)
+ {
+ return;
+ }
+
+ int id;
+ if (int.TryParse(text, out id))
+ {
+ children.Add(id);
+ // Recursively get the children
+ GetAllChildIdsUnix(id, children, timeout);
+ }
+ }
+ }
+ }
+ }
+
+ private static void KillProcessUnix(int processId, TimeSpan timeout)
+ {
+ string stdout;
+ RunProcessAndWaitForExit(
+ "kill",
+ $"-TERM {processId}",
+ timeout,
+ out stdout);
+ }
+
+ private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ UseShellExecute = false
+ };
+
+ var process = Process.Start(startInfo);
+
+ stdout = null;
+ if (process.WaitForExit((int)timeout.TotalMilliseconds))
+ {
+ stdout = process.StandardOutput.ReadToEnd();
+ }
+ else
+ {
+ process.Kill();
+ }
+
+ return process.ExitCode;
+ }
+}
diff --git a/source/Nuke.Tooling/Process2.cs b/source/Nuke.Tooling/Process2.cs
index fb3f1f8ca..56a2eaeb0 100644
--- a/source/Nuke.Tooling/Process2.cs
+++ b/source/Nuke.Tooling/Process2.cs
@@ -44,7 +44,12 @@ public void Dispose()
public void Kill()
{
- _process.Kill();
+ _process.Kill();
+ }
+
+ public void KillTree()
+ {
+ _process.KillTree();
}
public bool WaitForExit()
@@ -53,7 +58,7 @@ public bool WaitForExit()
// use _process.StartTime
var hasExited = _process.WaitForExit(_timeout ?? -1);
if (!hasExited)
- _process.Kill();
+ _process.KillTree();
return hasExited;
}
}