From 24a5e93b67ad65d218b2aaa2a9504820e038894b Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Sat, 26 Nov 2016 21:37:53 +0100 Subject: [PATCH] Decouple Some process management logic to a standalone ProcessHelper class --- src/Core/ServiceWrapper/Main.cs | 71 +-------------- src/Core/WinSWCore/Util/ProcessHelper.cs | 109 +++++++++++++++++++++++ src/Core/WinSWCore/Util/SigIntHelper.cs | 58 ++++++++++++ src/Core/WinSWCore/WinSWCore.csproj | 2 + 4 files changed, 170 insertions(+), 70 deletions(-) create mode 100644 src/Core/WinSWCore/Util/ProcessHelper.cs create mode 100644 src/Core/WinSWCore/Util/SigIntHelper.cs diff --git a/src/Core/ServiceWrapper/Main.cs b/src/Core/ServiceWrapper/Main.cs index dcfa141..3aaf0a9 100644 --- a/src/Core/ServiceWrapper/Main.cs +++ b/src/Core/ServiceWrapper/Main.cs @@ -321,7 +321,7 @@ namespace winsw try { WriteEvent("ProcessKill " + _process.Id); - StopProcessAndChildren(_process.Id); + ProcessHelper.StopProcessAndChildren(_process.Id, _descriptor.StopTimeout, _descriptor.StopParentProcessFirst); ExtensionManager.FireOnProcessTerminated(_process); } catch (InvalidOperationException) @@ -370,75 +370,6 @@ namespace winsw WriteEvent("Finished " + _descriptor.Id); } - private void StopProcessAndChildren(int pid) - { - var childPids = GetChildPids(pid); - - if (_descriptor.StopParentProcessFirst) - { - StopProcess(pid); - foreach (var childPid in childPids) - { - StopProcessAndChildren(childPid); - } - } - else - { - foreach (var childPid in childPids) - { - StopProcessAndChildren(childPid); - } - StopProcess(pid); - } - } - - private List GetChildPids(int pid) - { - var searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid); - var childPids = new List(); - foreach (var mo in searcher.Get()) - { - var childProcessId = mo["ProcessID"]; - WriteEvent("Found child process: " + childProcessId + " Name: " + mo["Name"]); - childPids.Add(Convert.ToInt32(childProcessId)); - } - return childPids; - } - - private void StopProcess(int pid) - { - WriteEvent("Stopping process " + pid); - Process proc; - try - { - proc = Process.GetProcessById(pid); - } - catch (ArgumentException) - { - WriteEvent("Process " + pid + " is already stopped"); - return; - } - - WriteEvent("Send SIGINT " + pid); - bool successful = SigIntHelper.SendSIGINTToProcess(proc, _descriptor.StopTimeout); - if (successful) - { - WriteEvent("SIGINT to" + pid + " successful"); - } - else - { - try - { - WriteEvent("SIGINT to " + pid + " failed - Killing as fallback", Level.Warn); - proc.Kill(); - } - catch (ArgumentException) - { - // Process already exited. - } - } - } - private void WaitForProcessToExit(Process processoWait) { SignalShutdownPending(); diff --git a/src/Core/WinSWCore/Util/ProcessHelper.cs b/src/Core/WinSWCore/Util/ProcessHelper.cs new file mode 100644 index 0000000..0decf70 --- /dev/null +++ b/src/Core/WinSWCore/Util/ProcessHelper.cs @@ -0,0 +1,109 @@ +using log4net; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Management; +using System.Text; + +namespace winsw.Util +{ + /// + /// Provides helper classes for Process Management + /// + /// Since WinSW 2.0 + public class ProcessHelper + { + 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 searcher = new ManagementObjectSearcher("Select * From Win32_Process Where ParentProcessID=" + pid); + var childPids = new List(); + foreach (var mo in searcher.Get()) + { + var childProcessId = mo["ProcessID"]; + Logger.Info("Found child process: " + childProcessId + " Name: " + mo["Name"]); + childPids.Add(Convert.ToInt32(childProcessId)); + } + 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; + } + + Logger.Info("Send SIGINT " + pid); + bool successful = SigIntHelper.SendSIGINTToProcess(proc, stopTimeout); + if (successful) + { + Logger.Info("SIGINT to" + pid + " successful"); + } + else + { + try + { + Logger.Warn("SIGINT to " + pid + " failed - Killing as fallback"); + proc.Kill(); + } + catch (ArgumentException) + { + // Process already exited. + } + } + + //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) + { + var childPids = GetChildPids(pid); + + if (stopParentProcessFirst) + { + StopProcess(pid, stopTimeout); + foreach (var childPid in childPids) + { + StopProcessAndChildren(childPid, stopTimeout, stopParentProcessFirst); + } + } + else + { + foreach (var childPid in childPids) + { + StopProcessAndChildren(childPid, stopTimeout, stopParentProcessFirst); + } + StopProcess(pid, stopTimeout); + } + } + + // TODO: Also move StartProcess methods once LogEvent()/WriteEvent() mess gets solved + } +} diff --git a/src/Core/WinSWCore/Util/SigIntHelper.cs b/src/Core/WinSWCore/Util/SigIntHelper.cs new file mode 100644 index 0000000..87e19f6 --- /dev/null +++ b/src/Core/WinSWCore/Util/SigIntHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace winsw.Util +{ + public static class SigIntHelper + { + private const string KERNEL32 = "kernel32.dll"; + + [DllImport(KERNEL32, SetLastError = true)] + private static extern bool AttachConsole(uint dwProcessId); + + [DllImport(KERNEL32, SetLastError = true, ExactSpelling = true)] + private static extern bool FreeConsole(); + + [DllImport(KERNEL32)] + private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add); + // Delegate type to be used as the Handler Routine for SCCH + private delegate Boolean ConsoleCtrlDelegate(CtrlTypes CtrlType); + + // Enumerated type for the control messages sent to the handler routine + private enum CtrlTypes : uint + { + CTRL_C_EVENT = 0, + CTRL_BREAK_EVENT, + CTRL_CLOSE_EVENT, + CTRL_LOGOFF_EVENT = 5, + CTRL_SHUTDOWN_EVENT + } + + [DllImport(KERNEL32)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId); + + /// + /// Uses the native funciton "AttachConsole" to attach the thread to the executing process to try to trigger a CTRL_C event (SIGINT). If the application + /// doesn't honor the event and shut down gracefully, the. wait period will time out after 15 seconds. + /// + /// The process to attach to and send the SIGINT + /// True if the process shut down successfully to the SIGINT, false if it did not. + public static bool SendSIGINTToProcess(Process process, TimeSpan shutdownTimeout) + { + if (AttachConsole((uint)process.Id)) + { + //Disable Ctrl-C handling for our program + SetConsoleCtrlHandler(null, true); + GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0); + + process.WaitForExit((int)shutdownTimeout.TotalMilliseconds); + + return process.HasExited; + } + + return false; + } + } +} diff --git a/src/Core/WinSWCore/WinSWCore.csproj b/src/Core/WinSWCore/WinSWCore.csproj index 6ea87bb..fd8b153 100644 --- a/src/Core/WinSWCore/WinSWCore.csproj +++ b/src/Core/WinSWCore/WinSWCore.csproj @@ -62,6 +62,8 @@ + +