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);