diff --git a/src/WinSW.Core/Native/Libraries.cs b/src/WinSW.Core/Native/Libraries.cs index e2df5ab..10f5cc4 100644 --- a/src/WinSW.Core/Native/Libraries.cs +++ b/src/WinSW.Core/Native/Libraries.cs @@ -4,5 +4,6 @@ { internal const string Advapi32 = "advapi32.dll"; internal const string Kernel32 = "kernel32.dll"; + internal const string NtDll = "ntdll.dll"; } } diff --git a/src/WinSW.Core/Native/ProcessApis.cs b/src/WinSW.Core/Native/ProcessApis.cs index 40ff013..15b5af1 100644 --- a/src/WinSW.Core/Native/ProcessApis.cs +++ b/src/WinSW.Core/Native/ProcessApis.cs @@ -26,12 +26,38 @@ namespace WinSW.Native [DllImport(Libraries.Kernel32)] internal static extern IntPtr GetCurrentProcess(); + [DllImport(Libraries.NtDll)] + internal static extern int NtQueryInformationProcess( + IntPtr processHandle, + PROCESSINFOCLASS processInformationClass, + out PROCESS_BASIC_INFORMATION processInformation, + int processInformationLength, + IntPtr returnLength = default); + [DllImport(Libraries.Advapi32, SetLastError = true)] internal static extern bool OpenProcessToken( IntPtr processHandle, TokenAccessLevels desiredAccess, out IntPtr tokenHandle); + internal enum PROCESSINFOCLASS + { + ProcessBasicInformation = 0, + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct PROCESS_BASIC_INFORMATION + { +#pragma warning disable SA1306 // Field names should begin with lower-case letter + private readonly IntPtr Reserved1; + private readonly IntPtr PebBaseAddress; + private readonly IntPtr Reserved2_1; + private readonly IntPtr Reserved2_2; + internal readonly IntPtr UniqueProcessId; + internal readonly IntPtr InheritedFromUniqueProcessId; +#pragma warning restore SA1306 // Field names should begin with lower-case letter + } + internal struct PROCESS_INFORMATION { public IntPtr ProcessHandle; diff --git a/src/WinSW.Core/Util/ProcessHelper.cs b/src/WinSW.Core/Util/ProcessHelper.cs index 60240d9..1be3550 100644 --- a/src/WinSW.Core/Util/ProcessHelper.cs +++ b/src/WinSW.Core/Util/ProcessHelper.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Management; +using System.Runtime.InteropServices; using System.Threading; using log4net; using WinSW.Native; +using static WinSW.Native.ConsoleApis; +using static WinSW.Native.ProcessApis; namespace WinSW.Util { @@ -17,113 +20,160 @@ namespace WinSW.Util { private static readonly ILog Logger = LogManager.GetLogger(typeof(ProcessHelper)); - /// - /// Gets all children of the specified process. - /// - /// Process PID - /// List of child process PIDs - public static List GetChildPids(int pid) - { - var childPids = new List(); - - try - { - string query = "SELECT * FROM Win32_Process WHERE ParentProcessID = " + pid; - using var searcher = new ManagementObjectSearcher(query); - using var results = searcher.Get(); - foreach (var wmiObject in results) - { - object childProcessId = wmiObject["ProcessID"]; - Logger.Info("Found child process: " + childProcessId + " Name: " + wmiObject["Name"]); - childPids.Add(Convert.ToInt32(childProcessId)); - } - } - catch (Exception ex) - { - Logger.Warn("Failed to locate children of the process with PID=" + pid + ". Child processes won't be terminated", ex); - } - - return childPids; - } - - /// - /// Stops the process. - /// If the process cannot be stopped within the stop timeout, it gets killed - /// - /// PID of the process - /// Stop timeout - public static void StopProcess(int pid, TimeSpan stopTimeout) - { - Logger.Info("Stopping process " + pid); - Process proc; - try - { - proc = Process.GetProcessById(pid); - } - catch (ArgumentException ex) - { - Logger.Info("Process " + pid + " is already stopped", ex); - return; - } - - // (bool sent, bool exited) - var result = SignalHelper.SendCtrlCToProcess(proc, stopTimeout); - bool exited = result.Value; - if (!exited) - { - try - { - bool sent = result.Key; - if (sent) - { - Logger.Warn("Process " + pid + " did not respond to Ctrl+C signal - Killing as fallback"); - } - - proc.Kill(); - } - catch (Exception ex) - { - if (!proc.HasExited) - { - throw; - } - - // Process already exited. - Logger.Warn("Ignoring exception from killing process because it has exited", ex); - } - } - - // TODO: Propagate error if process kill fails? Currently we use the legacy behavior - } - - /// - /// Terminate process and its children. - /// By default the child processes get terminated first. - /// - /// Process PID - /// Stop timeout (for each process) - /// If enabled, the perent process will be terminated before its children on all levels - public static void StopProcessAndChildren(int pid, TimeSpan stopTimeout, bool stopParentProcessFirst) + public static void StopProcessTree(Process process, TimeSpan stopTimeout, bool stopParentProcessFirst) { if (!stopParentProcessFirst) { - foreach (int childPid in GetChildPids(pid)) + foreach (var child in GetChildren(process)) { - StopProcessAndChildren(childPid, stopTimeout, stopParentProcessFirst); + StopProcessTree(child, stopTimeout, stopParentProcessFirst); } } - StopProcess(pid, stopTimeout); + StopProcess(process, stopTimeout); if (stopParentProcessFirst) { - foreach (int childPid in GetChildPids(pid)) + foreach (var child in GetChildren(process)) { - StopProcessAndChildren(childPid, stopTimeout, stopParentProcessFirst); + StopProcessTree(child, stopTimeout, stopParentProcessFirst); } } } + private static void StopProcess(Process process, TimeSpan stopTimeout) + { + Logger.Debug($"Stopping process {process.Id}..."); + + if (process.HasExited) + { + goto Exited; + } + + if (SendCtrlC(process) is not bool sent) + { + goto Exited; + } + + if (!sent) + { + try + { + sent = process.CloseMainWindow(); + } + catch (InvalidOperationException) + { + goto Exited; + } + } + + if (sent) + { + if (process.WaitForExit((int)stopTimeout.TotalMilliseconds)) + { + Logger.Debug($"Process {process.Id} canceled with code {process.ExitCode}."); + return; + } + } + +#if NET + process.Kill(); +#else + try + { + process.Kill(); + } + catch when (process.HasExited) + { + } +#endif + + Logger.Debug($"Process {process.Id} terminated."); + return; + + Exited: + Logger.Debug($"Process {process.Id} has already exited."); + } + + private static unsafe List GetChildren(Process process) + { + var startTime = process.StartTime; + int processId = process.Id; + + var children = new List(); + + foreach (var other in Process.GetProcesses()) + { + try + { + if (other.StartTime <= startTime) + { + goto Next; + } + + var handle = other.Handle; + + if (NtQueryInformationProcess( + handle, + PROCESSINFOCLASS.ProcessBasicInformation, + out var information, + sizeof(PROCESS_BASIC_INFORMATION)) != 0) + { + goto Next; + } + + if ((int)information.InheritedFromUniqueProcessId == processId) + { + Logger.Debug($"Found child process {other.Id}."); + children.Add(other); + continue; + } + + Next: + other.Dispose(); + } + catch (Exception e) when (e is InvalidOperationException || e is Win32Exception) + { + other.Dispose(); + } + } + + return children; + } + + private static bool? SendCtrlC(Process process) + { + if (!AttachConsole(process.Id)) + { + int error = Marshal.GetLastWin32Error(); + switch (error) + { + // The process does not have a console. + case Errors.ERROR_INVALID_HANDLE: + return false; + + // The process has exited. + case Errors.ERROR_INVALID_PARAMETER: + return null; + + // The calling process is already attached to a console. + case Errors.ERROR_ACCESS_DENIED: + default: + Logger.Warn("Failed to attach to console. " + new Win32Exception(error).Message); + return false; + } + } + + // Don't call GenerateConsoleCtrlEvent immediately after SetConsoleCtrlHandler. + // A delay was observed as of Windows 10, version 2004 and Windows Server 2019. + _ = GenerateConsoleCtrlEvent(CtrlEvents.CTRL_C_EVENT, 0); + + bool succeeded = FreeConsole(); + Debug.Assert(succeeded); + + return true; + } + /// /// Starts a process and asynchronosly waits for its termination. /// Once the process exits, the callback will be invoked. @@ -170,7 +220,7 @@ namespace WinSW.Util } } - bool succeeded = ConsoleApis.SetConsoleCtrlHandler(null, false); // inherited + bool succeeded = SetConsoleCtrlHandler(null, false); // inherited Debug.Assert(succeeded); try @@ -179,7 +229,7 @@ namespace WinSW.Util } finally { - succeeded = ConsoleApis.SetConsoleCtrlHandler(null, true); + succeeded = SetConsoleCtrlHandler(null, true); Debug.Assert(succeeded); } diff --git a/src/WinSW.Core/Util/SignalHelper.cs b/src/WinSW.Core/Util/SignalHelper.cs deleted file mode 100644 index 66a3350..0000000 --- a/src/WinSW.Core/Util/SignalHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.InteropServices; -using log4net; -using WinSW.Native; - -namespace WinSW.Util -{ - internal static class SignalHelper - { - private static readonly ILog Logger = LogManager.GetLogger(typeof(SignalHelper)); - - // (bool sent, bool exited) - internal static KeyValuePair SendCtrlCToProcess(Process process, TimeSpan shutdownTimeout) - { - if (!ConsoleApis.AttachConsole(process.Id)) - { - int error = Marshal.GetLastWin32Error(); - Logger.Warn("Failed to attach to console. " + error switch - { - Errors.ERROR_ACCESS_DENIED => "WinSW is already attached to a console.", // TODO: test mode - Errors.ERROR_INVALID_HANDLE => "The process does not have a console.", - Errors.ERROR_INVALID_PARAMETER => "The process has exited.", - _ => new Win32Exception(error).Message // unreachable - }); - - return new KeyValuePair(false, error == Errors.ERROR_INVALID_PARAMETER); - } - - // Don't call GenerateConsoleCtrlEvent immediately after SetConsoleCtrlHandler. - // A delay was observed as of Windows 10, version 2004 and Windows Server 2019. - _ = ConsoleApis.GenerateConsoleCtrlEvent(ConsoleApis.CtrlEvents.CTRL_C_EVENT, 0); - - bool succeeded = ConsoleApis.FreeConsole(); - Debug.Assert(succeeded); - - return new KeyValuePair(true, process.WaitForExit((int)shutdownTimeout.TotalMilliseconds)); - } - } -} diff --git a/src/WinSW.Plugins/RunawayProcessKillerExtension.cs b/src/WinSW.Plugins/RunawayProcessKillerExtension.cs index b20493c..648648d 100644 --- a/src/WinSW.Plugins/RunawayProcessKillerExtension.cs +++ b/src/WinSW.Plugins/RunawayProcessKillerExtension.cs @@ -296,7 +296,7 @@ namespace WinSW.Plugins bldr.Append(proc); Logger.Warn(bldr.ToString()); - ProcessHelper.StopProcessAndChildren(pid, this.StopTimeout, this.StopParentProcessFirst); + ProcessHelper.StopProcessTree(proc, this.StopTimeout, this.StopParentProcessFirst); } /// diff --git a/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs b/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs index 51cb7d8..f534f47 100644 --- a/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs +++ b/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs @@ -150,7 +150,7 @@ extensions: if (!proc.HasExited) { Console.Error.WriteLine("Test: Killing runaway process with ID=" + proc.Id); - ProcessHelper.StopProcessAndChildren(proc.Id, TimeSpan.FromMilliseconds(100), false); + ProcessHelper.StopProcessTree(proc, TimeSpan.FromMilliseconds(100), false); if (!proc.HasExited) { // The test is failed here anyway, but we add additional diagnostics info diff --git a/src/WinSW/WrapperService.cs b/src/WinSW/WrapperService.cs index 558bc0c..a0587e7 100644 --- a/src/WinSW/WrapperService.cs +++ b/src/WinSW/WrapperService.cs @@ -351,7 +351,7 @@ namespace WinSW try { Log.Debug("ProcessKill " + this.process.Id); - ProcessHelper.StopProcessAndChildren(this.process.Id, this.descriptor.StopTimeout, this.descriptor.StopParentProcessFirst); + ProcessHelper.StopProcessTree(this.process, this.descriptor.StopTimeout, this.descriptor.StopParentProcessFirst); this.ExtensionManager.FireOnProcessTerminated(this.process); } catch (InvalidOperationException)