diff --git a/source/Nuke.Tooling/IProcess.cs b/source/Nuke.Tooling/IProcess.cs index d7364ced6..18d9d57b5 100644 --- a/source/Nuke.Tooling/IProcess.cs +++ b/source/Nuke.Tooling/IProcess.cs @@ -54,10 +54,19 @@ public interface IProcess : IDisposable /// /// Calls . /// - void Kill(bool entireProcessTree = false); + 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 45b79005e..56a2eaeb0 100644 --- a/source/Nuke.Tooling/Process2.cs +++ b/source/Nuke.Tooling/Process2.cs @@ -42,9 +42,14 @@ public void Dispose() _process.Dispose(); } - public void Kill(bool entireProcessTree = false) + public void Kill() { - _process.Kill(entireProcessTree); + _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(entireProcessTree: true); + _process.KillTree(); return hasExited; } }