diff --git a/src/WinSW.Core/Configuration/XmlServiceConfig.cs b/src/WinSW.Core/Configuration/XmlServiceConfig.cs index ebb3dbc..6021a9f 100644 --- a/src/WinSW.Core/Configuration/XmlServiceConfig.cs +++ b/src/WinSW.Core/Configuration/XmlServiceConfig.cs @@ -708,6 +708,8 @@ namespace WinSW /// public override TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", base.StopTimeout); + public int StopTimeoutInMs => (int)this.StopTimeout.TotalMilliseconds; + /// /// Desired process priority or null if not specified. /// diff --git a/src/WinSW.Core/Util/ProcessExtensions.cs b/src/WinSW.Core/Util/ProcessExtensions.cs index 07fb212..f0c9b5f 100644 --- a/src/WinSW.Core/Util/ProcessExtensions.cs +++ b/src/WinSW.Core/Util/ProcessExtensions.cs @@ -2,35 +2,38 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Runtime.InteropServices; using log4net; +using WinSW.Native; +using static WinSW.Native.ConsoleApis; using static WinSW.Native.ProcessApis; namespace WinSW.Util { public static class ProcessExtensions { - private static readonly ILog Logger = LogManager.GetLogger(typeof(ProcessExtensions)); + private static readonly ILog Log = LogManager.GetLogger(typeof(ProcessExtensions)); - public static void StopTree(this Process process, TimeSpan stopTimeout) + public static void StopTree(this Process process, int millisecondsTimeout) { - StopPrivate(process, stopTimeout); + StopPrivate(process, millisecondsTimeout); foreach (Process child in GetChildren(process)) { using (child) { - StopTree(child, stopTimeout); + StopTree(child, millisecondsTimeout); } } } - internal static void StopDescendants(this Process process, TimeSpan stopTimeout) + internal static void StopDescendants(this Process process, int millisecondsTimeout) { foreach (Process child in GetChildren(process)) { using (child) { - StopTree(child, stopTimeout); + StopTree(child, millisecondsTimeout); } } } @@ -64,7 +67,7 @@ namespace WinSW.Util if ((int)information.InheritedFromUniqueProcessId == processId) { - Logger.Info($"Found child process '{other.Format()}'."); + Log.Debug($"Found child process '{other.Format()}'."); children.Add(other); continue; } @@ -84,22 +87,41 @@ namespace WinSW.Util // true => canceled // false => terminated // null => finished - internal static bool? Stop(this Process process, TimeSpan stopTimeout) + internal static bool? Stop(this Process process, int millisecondsTimeout) { if (process.HasExited) { return null; } - // (bool sent, bool exited) - KeyValuePair result = SignalHelper.SendCtrlCToProcess(process, stopTimeout); - bool exited = result.Value; - if (exited) + if (!(SendCtrlC(process) is bool sent)) { - bool sent = result.Key; - return sent ? true : (bool?)null; + return null; } + if (!sent) + { + try + { + sent = process.CloseMainWindow(); + } + catch (InvalidOperationException) + { + return null; + } + } + + if (sent) + { + if (process.WaitForExit(millisecondsTimeout)) + { + return true; + } + } + +#if NETCOREAPP + process.Kill(); +#else try { process.Kill(); @@ -107,41 +129,88 @@ namespace WinSW.Util catch when (process.HasExited) { } +#endif return false; } - private static void StopPrivate(Process process, TimeSpan stopTimeout) + private static void StopPrivate(Process process, int millisecondsTimeout) { - Logger.Info("Stopping process " + process.Id); + Log.Debug($"Stopping process '{process.Format()}'..."); if (process.HasExited) { - Logger.Info("Process " + process.Id + " is already stopped"); - return; + goto Exited; } - // (bool sent, bool exited) - KeyValuePair result = SignalHelper.SendCtrlCToProcess(process, stopTimeout); - bool exited = result.Value; - if (!exited) + if (!(SendCtrlC(process) is bool sent)) { - bool sent = result.Key; - if (sent) - { - Logger.Info("Process " + process.Id + " did not respond to Ctrl+C signal - Killing as fallback"); - } + goto Exited; + } + if (!sent) + { try { - process.Kill(); + sent = process.CloseMainWindow(); } - catch when (process.HasExited) + catch (InvalidOperationException) { + goto Exited; } } - // TODO: Propagate error if process kill fails? Currently we use the legacy behavior + if (sent) + { + if (process.WaitForExit(millisecondsTimeout)) + { + Log.Debug($"Process '{process.Format()}' canceled with code {process.ExitCode}."); + return; + } + } + +#if NETCOREAPP + process.Kill(); +#else + try + { + process.Kill(); + } + catch when (process.HasExited) + { + } +#endif + + Log.Debug($"Process '{process.Format()}' terminated."); + return; + + Exited: + Log.Debug($"Process '{process.Format()}' has already exited."); + } + + private static bool? SendCtrlC(Process process) + { + if (!AttachConsole(process.Id)) + { + int error = Marshal.GetLastWin32Error(); + Log.Debug("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 error == Errors.ERROR_INVALID_PARAMETER ? (bool?)null : false; + } + + _ = SetConsoleCtrlHandler(null, true); + _ = GenerateConsoleCtrlEvent(CtrlEvents.CTRL_C_EVENT, 0); + _ = SetConsoleCtrlHandler(null, false); + bool succeeded = FreeConsole(); + Debug.Assert(succeeded); + + return true; } } } diff --git a/src/WinSW.Core/Util/SignalHelper.cs b/src/WinSW.Core/Util/SignalHelper.cs deleted file mode 100644 index 35d46e2..0000000 --- a/src/WinSW.Core/Util/SignalHelper.cs +++ /dev/null @@ -1,41 +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.Info("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); - } - - _ = ConsoleApis.SetConsoleCtrlHandler(null, true); - _ = ConsoleApis.GenerateConsoleCtrlEvent(ConsoleApis.CtrlEvents.CTRL_C_EVENT, 0); - _ = ConsoleApis.SetConsoleCtrlHandler(null, false); - bool succeeded = ConsoleApis.FreeConsole(); - Debug.Assert(succeeded); - - return new KeyValuePair(true, process.WaitForExit((int)shutdownTimeout.TotalMilliseconds)); - } - } -} diff --git a/src/WinSW/WrapperService.cs b/src/WinSW/WrapperService.cs index c9d0a71..81dd175 100644 --- a/src/WinSW/WrapperService.cs +++ b/src/WinSW/WrapperService.cs @@ -18,7 +18,7 @@ namespace WinSW { internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider(); - private static readonly TimeSpan additionalStopTimeout = new TimeSpan(TimeSpan.TicksPerSecond); + private static readonly int additionalStopTimeout = 1_000; private static readonly ILog Log = LogManager.GetLogger( #if NETCOREAPP @@ -380,14 +380,14 @@ namespace WinSW { Process process = this.process; Log.Debug("ProcessKill " + process.Id); - bool? result = process.Stop(this.config.StopTimeout); + bool? result = process.Stop(this.config.StopTimeoutInMs); this.LogMinimal($"Child process '{process.Format()}' " + result switch { true => $"canceled with code {process.ExitCode}.", false => "terminated.", null => $"finished with code '{process.ExitCode}'." }); - this.process.StopDescendants(this.config.StopTimeout); + this.process.StopDescendants(this.config.StopTimeoutInMs); this.ExtensionManager.FireOnProcessTerminated(process); } else @@ -407,11 +407,11 @@ namespace WinSW this.stoppingProcess = null; this.WaitForProcessToExit(this.process); - this.process.StopDescendants(this.config.StopTimeout); + this.process.StopDescendants(this.config.StopTimeoutInMs); } catch { - this.process.StopTree(this.config.StopTimeout); + this.process.StopTree(this.config.StopTimeoutInMs); throw; } } @@ -505,7 +505,7 @@ namespace WinSW { Log.Warn($"Child process '{process.Format()}' finished with code {process.ExitCode}."); - process.StopDescendants(this.config.StopTimeout); + process.StopDescendants(this.config.StopTimeoutInMs); this.startingProcess?.StopTree(additionalStopTimeout); this.stoppingProcess?.StopTree(additionalStopTimeout);