From c3771e391c91532c44aa64bc051cd410d2e4b87b Mon Sep 17 00:00:00 2001 From: NextTurn <45985406+NextTurn@users.noreply.github.com> Date: Wed, 29 Jul 2020 00:00:00 +0800 Subject: [PATCH] Preshutdown support --- .../Configuration/DefaultSettings.cs | 2 + src/WinSW.Core/Native/Service.cs | 12 +++ src/WinSW.Core/NullableAttributes.cs | 5 +- src/WinSW.Core/ServiceDescriptor.cs | 11 +++ src/WinSW.Core/Util/ProcessHelper.cs | 53 ++++++-------- src/WinSW/Logging/IServiceEventLog.cs | 9 +++ .../Logging/IServiceEventLogProvider.cs | 8 +- .../Logging/ServiceEventLogAppender.cs | 13 ++-- .../Logging/WrapperServiceEventLogProvider.cs | 8 +- src/WinSW/NullableAttributes.cs | 9 ++- src/WinSW/Program.cs | 13 +++- src/WinSW/WrapperService.cs | 73 +++++++++++++++---- 12 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 src/WinSW/Logging/IServiceEventLog.cs rename src/{WinSW.Core => WinSW}/Logging/IServiceEventLogProvider.cs (68%) rename src/{WinSW.Core => WinSW}/Logging/ServiceEventLogAppender.cs (72%) diff --git a/src/WinSW.Core/Configuration/DefaultSettings.cs b/src/WinSW.Core/Configuration/DefaultSettings.cs index 470e2cb..cf10a28 100644 --- a/src/WinSW.Core/Configuration/DefaultSettings.cs +++ b/src/WinSW.Core/Configuration/DefaultSettings.cs @@ -55,6 +55,8 @@ namespace WinSW.Configuration public bool DelayedAutoStart => false; + public bool Preshutdown => false; + public string[] ServiceDependencies => new string[0]; public bool Interactive => false; diff --git a/src/WinSW.Core/Native/Service.cs b/src/WinSW.Core/Native/Service.cs index 6c4d895..47915cf 100644 --- a/src/WinSW.Core/Native/Service.cs +++ b/src/WinSW.Core/Native/Service.cs @@ -272,6 +272,18 @@ namespace WinSW.Native } } + /// + internal void SetPreshutdownTimeout(TimeSpan timeout) + { + if (!ChangeServiceConfig2( + this.handle, + ServiceConfigInfoLevels.PRESHUTDOWN_INFO, + new SERVICE_PRESHUTDOWN_INFO { PreshutdownTimeout = (int)timeout.TotalMilliseconds })) + { + Throw.Command.Win32Exception("Failed to configure the preshutdown timeout."); + } + } + /// internal void SetSecurityDescriptor(RawSecurityDescriptor securityDescriptor) { diff --git a/src/WinSW.Core/NullableAttributes.cs b/src/WinSW.Core/NullableAttributes.cs index fa9b3ca..f4c0ad8 100644 --- a/src/WinSW.Core/NullableAttributes.cs +++ b/src/WinSW.Core/NullableAttributes.cs @@ -1,4 +1,7 @@ -#if !NETCOREAPP +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NETCOREAPP namespace System.Diagnostics.CodeAnalysis { /// Specifies that null is allowed as an input even if the corresponding type disallows it. diff --git a/src/WinSW.Core/ServiceDescriptor.cs b/src/WinSW.Core/ServiceDescriptor.cs index 24a6173..8935cf8 100644 --- a/src/WinSW.Core/ServiceDescriptor.cs +++ b/src/WinSW.Core/ServiceDescriptor.cs @@ -574,6 +574,17 @@ namespace WinSW /// public bool DelayedAutoStart => this.SingleBoolElement("delayedAutoStart", Defaults.DelayedAutoStart); + public bool Preshutdown => this.SingleBoolElement("preshutdown", Defaults.Preshutdown); + + public TimeSpan? PreshutdownTimeout + { + get + { + string? value = this.SingleElement("preshutdownTimeout", true); + return value is null ? default : this.ParseTimeSpan(value); + } + } + /// /// True if the service should beep when finished on shutdown. /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx diff --git a/src/WinSW.Core/Util/ProcessHelper.cs b/src/WinSW.Core/Util/ProcessHelper.cs index 585a672..e4ca9db 100644 --- a/src/WinSW.Core/Util/ProcessHelper.cs +++ b/src/WinSW.Core/Util/ProcessHelper.cs @@ -26,40 +26,35 @@ namespace WinSW.Util foreach (Process process in Process.GetProcesses()) { - if (process.StartTime <= startTime) - { - process.Dispose(); - continue; - } - - IntPtr handle; try { - handle = process.Handle; - } - catch (Win32Exception) - { - process.Dispose(); - continue; - } + if (process.StartTime <= startTime) + { + goto Next; + } - if (NtQueryInformationProcess( - handle, - PROCESSINFOCLASS.ProcessBasicInformation, - out PROCESS_BASIC_INFORMATION information, - sizeof(PROCESS_BASIC_INFORMATION)) != 0) - { - Logger.Warn("Failed to locate children of the process with PID=" + processId + ". Child processes won't be terminated"); - process.Dispose(); - continue; - } + IntPtr handle = process.Handle; - if ((int)information.InheritedFromUniqueProcessId == processId) - { - Logger.Info($"Found child process '{process.Format()}'."); - children.Add(process); + if (NtQueryInformationProcess( + handle, + PROCESSINFOCLASS.ProcessBasicInformation, + out PROCESS_BASIC_INFORMATION information, + sizeof(PROCESS_BASIC_INFORMATION)) != 0) + { + goto Next; + } + + if ((int)information.InheritedFromUniqueProcessId == processId) + { + Logger.Info($"Found child process '{process.Format()}'."); + children.Add(process); + continue; + } + + Next: + process.Dispose(); } - else + catch (Exception e) when (e is InvalidOperationException || e is Win32Exception) { process.Dispose(); } diff --git a/src/WinSW/Logging/IServiceEventLog.cs b/src/WinSW/Logging/IServiceEventLog.cs new file mode 100644 index 0000000..43927e3 --- /dev/null +++ b/src/WinSW/Logging/IServiceEventLog.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; + +namespace WinSW.Logging +{ + internal interface IServiceEventLog + { + void WriteEntry(string message, EventLogEntryType type); + } +} diff --git a/src/WinSW.Core/Logging/IServiceEventLogProvider.cs b/src/WinSW/Logging/IServiceEventLogProvider.cs similarity index 68% rename from src/WinSW.Core/Logging/IServiceEventLogProvider.cs rename to src/WinSW/Logging/IServiceEventLogProvider.cs index 0c79fd0..9c96902 100644 --- a/src/WinSW.Core/Logging/IServiceEventLogProvider.cs +++ b/src/WinSW/Logging/IServiceEventLogProvider.cs @@ -1,16 +1,14 @@ -using System.Diagnostics; - -namespace WinSW.Logging +namespace WinSW.Logging { /// /// Indicates that the class may reference the event log /// - public interface IServiceEventLogProvider + internal interface IServiceEventLogProvider { /// /// Locates Event Log for the service. /// /// Event Log or null if it is not avilable - EventLog? Locate(); + IServiceEventLog? Locate(); } } diff --git a/src/WinSW.Core/Logging/ServiceEventLogAppender.cs b/src/WinSW/Logging/ServiceEventLogAppender.cs similarity index 72% rename from src/WinSW.Core/Logging/ServiceEventLogAppender.cs rename to src/WinSW/Logging/ServiceEventLogAppender.cs index 34b99ab..bf5b7d5 100644 --- a/src/WinSW.Core/Logging/ServiceEventLogAppender.cs +++ b/src/WinSW/Logging/ServiceEventLogAppender.cs @@ -8,15 +8,18 @@ namespace WinSW.Logging /// Implementes service Event log appender for log4j. /// The implementation presumes that service gets initialized after the logging. /// - public class ServiceEventLogAppender : AppenderSkeleton + internal sealed class ServiceEventLogAppender : AppenderSkeleton { -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public IServiceEventLogProvider Provider { get; set; } -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + private readonly IServiceEventLogProvider provider; + + internal ServiceEventLogAppender(IServiceEventLogProvider provider) + { + this.provider = provider; + } protected override void Append(LoggingEvent loggingEvent) { - EventLog? eventLog = this.Provider.Locate(); + IServiceEventLog? eventLog = this.provider.Locate(); // We write the event iff the provider is ready eventLog?.WriteEntry(loggingEvent.RenderedMessage, ToEventLogEntryType(loggingEvent.Level)); diff --git a/src/WinSW/Logging/WrapperServiceEventLogProvider.cs b/src/WinSW/Logging/WrapperServiceEventLogProvider.cs index 5f78b57..df454ee 100644 --- a/src/WinSW/Logging/WrapperServiceEventLogProvider.cs +++ b/src/WinSW/Logging/WrapperServiceEventLogProvider.cs @@ -1,14 +1,12 @@ -using System.Diagnostics; - -namespace WinSW.Logging +namespace WinSW.Logging { /// /// Implements caching of the WindowsService reference in WinSW. /// - public class WrapperServiceEventLogProvider : IServiceEventLogProvider + internal sealed class WrapperServiceEventLogProvider : IServiceEventLogProvider { public WrapperService? Service { get; set; } - public EventLog? Locate() => this.Service?.EventLog; + public IServiceEventLog? Locate() => this.Service; } } diff --git a/src/WinSW/NullableAttributes.cs b/src/WinSW/NullableAttributes.cs index 45b5ff2..7b33c84 100644 --- a/src/WinSW/NullableAttributes.cs +++ b/src/WinSW/NullableAttributes.cs @@ -1,8 +1,13 @@ -#if !NETCOREAPP +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NETCOREAPP namespace System.Diagnostics.CodeAnalysis { /// Applied to a method that will never return under any circumstance. [AttributeUsage(AttributeTargets.Method, Inherited = false)] - internal sealed class DoesNotReturnAttribute : Attribute { } + internal sealed class DoesNotReturnAttribute : Attribute + { + } } #endif diff --git a/src/WinSW/Program.cs b/src/WinSW/Program.cs index 06f5497..f7bb5e2 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -396,6 +396,11 @@ namespace WinSW sc.SetDelayedAutoStart(true); } + if (descriptor.PreshutdownTimeout is TimeSpan preshutdownTimeout) + { + sc.SetPreshutdownTimeout(preshutdownTimeout); + } + string? securityDescriptor = descriptor.SecurityDescriptor; if (securityDescriptor != null) { @@ -740,6 +745,11 @@ namespace WinSW sc.SetDelayedAutoStart(true); } + if (descriptor.PreshutdownTimeout is TimeSpan preshutdownTimeout) + { + sc.SetPreshutdownTimeout(preshutdownTimeout); + } + string? securityDescriptor = descriptor.SecurityDescriptor; if (securityDescriptor != null) { @@ -846,11 +856,10 @@ namespace WinSW } // event log - var systemEventLogger = new ServiceEventLogAppender + var systemEventLogger = new ServiceEventLogAppender(WrapperService.eventLogProvider) { Name = "Wrapper event log", Threshold = eventLogLevel, - Provider = WrapperService.eventLogProvider, }; systemEventLogger.ActivateOptions(); appenders.Add(systemEventLogger); diff --git a/src/WinSW/WrapperService.cs b/src/WinSW/WrapperService.cs index 549401c..fa507c8 100644 --- a/src/WinSW/WrapperService.cs +++ b/src/WinSW/WrapperService.cs @@ -14,7 +14,7 @@ using WinSW.Util; namespace WinSW { - public class WrapperService : ServiceBase, IEventLogger + public sealed class WrapperService : ServiceBase, IEventLogger, IServiceEventLog { private readonly Process process = new Process(); private readonly ServiceDescriptor descriptor; @@ -58,11 +58,11 @@ namespace WinSW // Register the event log provider eventLogProvider.Service = this; - } - public WrapperService() - : this(new ServiceDescriptor()) - { + if (descriptor.Preshutdown) + { + this.AcceptPreshutdown(); + } } /// @@ -135,34 +135,51 @@ namespace WinSW public void LogEvent(string message) { + if (this.shuttingdown) + { + // The Event Log service exits earlier. + return; + } + try { this.EventLog.WriteEntry(message); } catch (Exception e) { - if (!this.shuttingdown) - { - Log.Error("Failed to log event in Windows Event Log: " + message + "; Reason: ", e); - } + Log.Error("Failed to log event in Windows Event Log: " + message + "; Reason: ", e); } } public void LogEvent(string message, EventLogEntryType type) { + if (this.shuttingdown) + { + // The Event Log service exits earlier. + return; + } + try { this.EventLog.WriteEntry(message, type); } catch (Exception e) { - if (!this.shuttingdown) - { - Log.Error("Failed to log event in Windows Event Log. Reason: ", e); - } + Log.Error("Failed to log event in Windows Event Log. Reason: ", e); } } + void IServiceEventLog.WriteEntry(string message, EventLogEntryType type) + { + if (this.shuttingdown) + { + // The Event Log service exits earlier. + return; + } + + this.EventLog.WriteEntry(message, type); + } + private void LogInfo(string message) { this.LogEvent(message); @@ -213,6 +230,15 @@ namespace WinSW } } + protected override void OnCustomCommand(int command) + { + if (command == 0x0000000F) + { + // SERVICE_CONTROL_PRESHUTDOWN + this.Stop(); + } + } + private void DoStart() { this.envs = this.descriptor.EnvironmentVariables; @@ -385,6 +411,27 @@ namespace WinSW } } + /// + private void AcceptPreshutdown() + { + const string acceptedCommandsFieldName = +#if NETCOREAPP + "_acceptedCommands"; +#else + "acceptedCommands"; +#endif + + FieldInfo? acceptedCommandsField = typeof(ServiceBase).GetField(acceptedCommandsFieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (acceptedCommandsField is null) + { + throw new MissingFieldException(nameof(ServiceBase), acceptedCommandsFieldName); + } + + int acceptedCommands = (int)acceptedCommandsField.GetValue(this)!; + acceptedCommands |= 0x00000100; // SERVICE_ACCEPT_PRESHUTDOWN + acceptedCommandsField.SetValue(this, acceptedCommands); + } + private void SignalPending() { this.RequestAdditionalTime(15_000);