From 92787fbfee1e53b4c8fc0740025b441c91455df3 Mon Sep 17 00:00:00 2001 From: Ilya Reva Date: Tue, 19 Dec 2023 13:48:23 +0100 Subject: [PATCH] fix(tools): kill child process on timeout --- source/Nuke.Tooling/IProcess.cs | 11 +- source/Nuke.Tooling/NativeProcessExtension.cs | 114 ++++++++++++++++++ source/Nuke.Tooling/Process2.cs | 9 +- 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 source/Nuke.Tooling/NativeProcessExtension.cs 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; } }