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)