From 98024705ad9acbd8c7f64e87ded9ff2581a4993a Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Fri, 24 Mar 2023 09:58:14 -0700 Subject: [PATCH 01/11] Make WindowsServiceLifetime gracefully stop WindowsServiceLifetime was not waiting for ServiceBase to stop the service. As a result we would sometimes end the process before notifying service control manager that the service had stopped -- resulting in an error in the eventlog and sometimes a service restart. We also were permitting multiple calls to Stop to occur - through SCM callbacks, and through public API. We must not call SetServiceStatus again once the service is marked as stopped. --- .../src/WindowsServiceLifetime.cs | 7 ++++--- .../src/System/ServiceProcess/ServiceBase.cs | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs index d39ddd818099b5..c99b897f8582b1 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs @@ -18,6 +18,7 @@ namespace Microsoft.Extensions.Hosting.WindowsServices public class WindowsServiceLifetime : ServiceBase, IHostLifetime { private readonly TaskCompletionSource _delayStart = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _serviceStopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim(); private readonly HostOptions _hostOptions; @@ -92,14 +93,14 @@ private void Run() { _delayStart.TrySetException(ex); } + _serviceStopped.TrySetResult(null); } public Task StopAsync(CancellationToken cancellationToken) { - // Avoid deadlock where host waits for StopAsync before firing ApplicationStopped, - // and Stop waits for ApplicationStopped. + // Stop will cause the ServiceBase.Run method to complete and return, which completes _serviceStopped. Task.Run(Stop, CancellationToken.None); - return Task.CompletedTask; + return _serviceStopped.Task; } // Called by base.Run when the service is ready to start. diff --git a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs index 870eff65a752f8..468bee26541052 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs @@ -647,6 +647,12 @@ public static void Run(ServiceBase service) public void Stop() { + if (_status.currentState == ServiceControlStatus.STATE_STOPPED || _status.currentState == default) + { + // nothing to do if the service is already stopped or never started + return; + } + DeferredStop(); } From a146ad0b5f8e36f70fb3c4c5427f03333659f2cd Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Mon, 27 Mar 2023 15:01:35 -0700 Subject: [PATCH 02/11] Alternate approach to ensuring we only ever set STATE_STOPPED once. --- .../src/System/ServiceProcess/ServiceBase.cs | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs index 468bee26541052..c745593bc25d65 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceBase.cs @@ -31,6 +31,7 @@ public class ServiceBase : Component private bool _commandPropsFrozen; // set to true once we've use the Can... properties. private bool _disposed; private bool _initialized; + private object _stopLock = new object(); private EventLog? _eventLog; /// @@ -501,27 +502,34 @@ private void DeferredSessionChange(int eventType, int sessionId) // This is a problem when multiple services are hosted in a single process. private unsafe void DeferredStop() { - fixed (SERVICE_STATUS* pStatus = &_status) + lock(_stopLock) { - int previousState = _status.currentState; - - _status.checkPoint = 0; - _status.waitHint = 0; - _status.currentState = ServiceControlStatus.STATE_STOP_PENDING; - SetServiceStatus(_statusHandle, pStatus); - try + // never call SetServiceStatus again after STATE_STOPPED is set. + if (_status.currentState != ServiceControlStatus.STATE_STOPPED) { - OnStop(); - WriteLogEntry(SR.StopSuccessful); - _status.currentState = ServiceControlStatus.STATE_STOPPED; - SetServiceStatus(_statusHandle, pStatus); - } - catch (Exception e) - { - _status.currentState = previousState; - SetServiceStatus(_statusHandle, pStatus); - WriteLogEntry(SR.Format(SR.StopFailed, e), EventLogEntryType.Error); - throw; + fixed (SERVICE_STATUS* pStatus = &_status) + { + int previousState = _status.currentState; + + _status.checkPoint = 0; + _status.waitHint = 0; + _status.currentState = ServiceControlStatus.STATE_STOP_PENDING; + SetServiceStatus(_statusHandle, pStatus); + try + { + OnStop(); + WriteLogEntry(SR.StopSuccessful); + _status.currentState = ServiceControlStatus.STATE_STOPPED; + SetServiceStatus(_statusHandle, pStatus); + } + catch (Exception e) + { + _status.currentState = previousState; + SetServiceStatus(_statusHandle, pStatus); + WriteLogEntry(SR.Format(SR.StopFailed, e), EventLogEntryType.Error); + throw; + } + } } } } @@ -533,14 +541,17 @@ private unsafe void DeferredShutdown() OnShutdown(); WriteLogEntry(SR.ShutdownOK); - if (_status.currentState == ServiceControlStatus.STATE_PAUSED || _status.currentState == ServiceControlStatus.STATE_RUNNING) + lock(_stopLock) { - fixed (SERVICE_STATUS* pStatus = &_status) + if (_status.currentState == ServiceControlStatus.STATE_PAUSED || _status.currentState == ServiceControlStatus.STATE_RUNNING) { - _status.checkPoint = 0; - _status.waitHint = 0; - _status.currentState = ServiceControlStatus.STATE_STOPPED; - SetServiceStatus(_statusHandle, pStatus); + fixed (SERVICE_STATUS* pStatus = &_status) + { + _status.checkPoint = 0; + _status.waitHint = 0; + _status.currentState = ServiceControlStatus.STATE_STOPPED; + SetServiceStatus(_statusHandle, pStatus); + } } } } @@ -647,12 +658,6 @@ public static void Run(ServiceBase service) public void Stop() { - if (_status.currentState == ServiceControlStatus.STATE_STOPPED || _status.currentState == default) - { - // nothing to do if the service is already stopped or never started - return; - } - DeferredStop(); } @@ -660,7 +665,7 @@ private void Initialize(bool multipleServices) { if (!_initialized) { - //Cannot register the service with NT service manatger if the object has been disposed, since finalization has been suppressed. + //Cannot register the service with NT service manager if the object has been disposed, since finalization has been suppressed. if (_disposed) throw new ObjectDisposedException(GetType().Name); @@ -929,8 +934,14 @@ public unsafe void ServiceMainCallback(int argCount, IntPtr argPointer) { string errorMessage = new Win32Exception().Message; WriteLogEntry(SR.Format(SR.StartFailed, errorMessage), EventLogEntryType.Error); - _status.currentState = ServiceControlStatus.STATE_STOPPED; - SetServiceStatus(_statusHandle, pStatus); + lock (_stopLock) + { + if (_status.currentState != ServiceControlStatus.STATE_STOPPED) + { + _status.currentState = ServiceControlStatus.STATE_STOPPED; + SetServiceStatus(_statusHandle, pStatus); + } + } } } } From e6de2e1340cf8b1a94d0e6f5079e20a29967c2e8 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 28 Mar 2023 22:40:56 -0700 Subject: [PATCH 03/11] Avoid calling ServiceBase.Stop on stopped service I fixed double-calling STATE_STOPPED in ServiceBase, but this fix will not be present on .NETFramework. Workaround that by avoiding calling ServiceBase.Stop when the service has already been stopped by SCM. --- .../src/WindowsServiceLifetime.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs index c99b897f8582b1..d0489e6442ecd1 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs @@ -21,6 +21,7 @@ public class WindowsServiceLifetime : ServiceBase, IHostLifetime private readonly TaskCompletionSource _serviceStopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim(); private readonly HostOptions _hostOptions; + private bool _isStopped; /// /// Initializes a new instance. @@ -98,8 +99,12 @@ private void Run() public Task StopAsync(CancellationToken cancellationToken) { - // Stop will cause the ServiceBase.Run method to complete and return, which completes _serviceStopped. - Task.Run(Stop, CancellationToken.None); + if (!_isStopped) + { + Task.Run(Stop, CancellationToken.None); + } + + // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceStopped. return _serviceStopped.Task; } @@ -117,6 +122,7 @@ protected override void OnStart(string[] args) /// This might be called multiple times by service Stop, ApplicationStopping, and StopAsync. That's okay because StopApplication uses a CancellationTokenSource and prevents any recursion. protected override void OnStop() { + _isStopped = true; ApplicationLifetime.StopApplication(); // Wait for the host to shutdown before marking service as stopped. _delayStop.Wait(_hostOptions.ShutdownTimeout); @@ -128,6 +134,7 @@ protected override void OnStop() /// protected override void OnShutdown() { + _isStopped = true; ApplicationLifetime.StopApplication(); // Wait for the host to shutdown before marking service as stopped. _delayStop.Wait(_hostOptions.ShutdownTimeout); From eb39960536257bf8a7fd945efd21b694504b23ed Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 28 Mar 2023 22:46:47 -0700 Subject: [PATCH 04/11] Add tests for WindowsServiceLifetime These tests leverage RemoteExecutor to avoid creating a separate service assembly. --- .../Advapi32/Interop.QueryServiceStatusEx.cs | 34 +++++ ...sions.Hosting.WindowsServices.Tests.csproj | 31 +++++ .../tests/UseWindowsServiceTests.cs | 25 +++- .../tests/WindowsServiceLifetimeTests.cs | 127 ++++++++++++++++++ .../tests/WindowsServiceTester.cs | 107 +++++++++++++++ 5 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs diff --git a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs new file mode 100644 index 00000000000000..db960cf9c9bf2e --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Advapi32 + { + [StructLayout(LayoutKind.Sequential)] + internal struct SERVICE_STATUS_PROCESS + { + public int dwServiceType; + public int dwCurrentState; + public int dwControlsAccepted; + public int dwWin32ExitCode; + public int dwServiceSpecificExitCode; + public int dwCheckPoint; + public int dwWaitHint; + public int dwProcessId; + public int dwServiceFlags; + } + + private const int SC_STATUS_PROCESS_INFO = 0; + + [LibraryImport(Libraries.Advapi32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static unsafe partial bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, int InfoLevel, SERVICE_STATUS_PROCESS* pStatus, int cbBufSize, out int pcbBytesNeeded); + + internal static unsafe bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, SERVICE_STATUS_PROCESS* pStatus) => QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, pStatus, sizeof(SERVICE_STATUS_PROCESS), out int unused); + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj index 93be9b87c967b5..f9447576dfa314 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj @@ -4,12 +4,43 @@ $(NetCoreAppCurrent)-windows;$(NetFrameworkMinimum) true + true + true + true + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs index 1fb2ade8a94079..09aadcf310929b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; using System.Reflection; using System.ServiceProcess; using Microsoft.Extensions.DependencyInjection; @@ -30,6 +29,26 @@ public void DefaultsToOffOutsideOfService() Assert.IsType(lifetime); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void CanCreateService() + { + using var serviceTester = WindowsServiceTester.Create(nameof(CanCreateService), () => + { + using IHost host = new HostBuilder() + .UseWindowsService() + .Build(); + host.Run(); + }); + + serviceTester.Start(); + serviceTester.WaitForStatus(ServiceControllerStatus.Running); + serviceTester.Stop(); + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(0, status.win32ExitCode); + } + [Fact] public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService() { @@ -66,7 +85,7 @@ public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationN var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings { ApplicationName = appName, - }); + }); // Emulate calling builder.Services.AddWindowsService() from inside a Windows service. AddWindowsServiceLifetime(builder.Services); @@ -82,7 +101,7 @@ public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationN [Fact] public void ServiceCollectionExtensionMethodCanBeCalledOnDefaultConfiguration() { - var builder = new HostApplicationBuilder(); + var builder = new HostApplicationBuilder(); // Emulate calling builder.Services.AddWindowsService() from inside a Windows service. AddWindowsServiceLifetime(builder.Services); diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs new file mode 100644 index 00000000000000..c781d3e3a3ae14 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.WindowsServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Hosting +{ + public class WindowsServiceLifetimeTests + { + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void ServiceSequenceIsCorrect() + { + using var serviceTester = WindowsServiceTester.Create(nameof(ServiceSequenceIsCorrect), () => + { + SimpleServiceLogger.InitializeForTestCase(nameof(ServiceSequenceIsCorrect)); + using IHost host = new HostBuilder() + .ConfigureServices(services => + { + services.AddHostedService(); + services.AddSingleton(); + }) + .Build(); + + var applicationLifetime = host.Services.GetRequiredService(); + applicationLifetime.ApplicationStarted.Register(() => SimpleServiceLogger.Log($"lifetime started")); + applicationLifetime.ApplicationStopping.Register(() => SimpleServiceLogger.Log($"lifetime stopping")); + applicationLifetime.ApplicationStopped.Register(() => SimpleServiceLogger.Log($"lifetime stopped")); + + SimpleServiceLogger.Log("host.Run()"); + host.Run(); + SimpleServiceLogger.Log("host.Run() complete"); + }); + + SimpleServiceLogger.DeleteLog(nameof(ServiceSequenceIsCorrect)); + + serviceTester.Start(); + serviceTester.WaitForStatus(ServiceControllerStatus.Running); + + var statusEx = serviceTester.QueryServiceStatusEx(); + var serviceProcess = Process.GetProcessById(statusEx.dwProcessId); + + serviceTester.Stop(); + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + + serviceProcess.WaitForExit(); + + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(0, status.win32ExitCode); + + var logText = SimpleServiceLogger.ReadLog(nameof(ServiceSequenceIsCorrect)); + Assert.Equal(""" + host.Run() + WindowsServiceLifetime.OnStart + BackgroundService.StartAsync + lifetime started + WindowsServiceLifetime.OnStop + lifetime stopping + BackgroundService.StopAsync + lifetime stopped + host.Run() complete + + """, logText); + + } + + public class SimpleWindowsServiceLifetime : WindowsServiceLifetime + { + public SimpleWindowsServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions optionsAccessor) : + base(environment, applicationLifetime, loggerFactory, optionsAccessor) + { } + + protected override void OnStart(string[] args) + { + SimpleServiceLogger.Log("WindowsServiceLifetime.OnStart"); + base.OnStart(args); + } + + protected override void OnStop() + { + SimpleServiceLogger.Log("WindowsServiceLifetime.OnStop"); + base.OnStop(); + } + } + + public class SimpleBackgroundService : BackgroundService + { +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + protected override async Task ExecuteAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.ExecuteAsync"); + public override async Task StartAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.StartAsync"); + public override async Task StopAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.StopAsync"); +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + } + + static class SimpleServiceLogger + { + static string _fileName; + + public static void InitializeForTestCase(string testCaseName) + { + Assert.Null(_fileName); + _fileName = GetLogForTestCase(testCaseName); + } + + private static string GetLogForTestCase(string testCaseName) => Path.Combine(AppContext.BaseDirectory, $"{testCaseName}.log"); + public static void DeleteLog(string testCaseName) => File.Delete(GetLogForTestCase(testCaseName)); + public static string ReadLog(string testCaseName) => File.ReadAllText(GetLogForTestCase(testCaseName)); + public static void Log(string message) + { + Assert.NotNull(_fileName); + lock (_fileName) + { + File.AppendAllText(_fileName, message + Environment.NewLine); + } + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs new file mode 100644 index 00000000000000..3574d93942c4f1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel; +using System.ServiceProcess; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Extensions.Hosting +{ + public class WindowsServiceTester : ServiceController + { + private WindowsServiceTester(SafeServiceHandle handle, string serviceName) : base(serviceName) + { + _handle = handle; + } + + private SafeServiceHandle _handle; + + public static WindowsServiceTester Create(string serviceName, Action serviceMain) + { + // create remote executor commandline arguments + string commandLine; + using (var remoteExecutorHandle = RemoteExecutor.Invoke(serviceMain, new RemoteInvokeOptions() { Start = false })) + { + var startInfo = remoteExecutorHandle.Process.StartInfo; + remoteExecutorHandle.Process.Dispose(); + remoteExecutorHandle.Process = null; + commandLine = startInfo.FileName + " " + startInfo.Arguments; + } + + // install the service + using (var serviceManagerHandle = new SafeServiceHandle(Interop.Advapi32.OpenSCManager(null, null, Interop.Advapi32.ServiceControllerOptions.SC_MANAGER_ALL))) + { + if (serviceManagerHandle.IsInvalid) + { + throw new InvalidOperationException("Cannot open Service Control Manager"); + } + + // delete existing service if it exists + using (var existingServiceHandle = new SafeServiceHandle(Interop.Advapi32.OpenService(serviceManagerHandle, serviceName, Interop.Advapi32.ServiceAccessOptions.ACCESS_TYPE_ALL))) + { + if (!existingServiceHandle.IsInvalid) + { + Interop.Advapi32.DeleteService(existingServiceHandle); + } + } + + var serviceHandle = new SafeServiceHandle( + Interop.Advapi32.CreateService(serviceManagerHandle, + serviceName, + $"{nameof(WindowsServiceTester)} test service", + Interop.Advapi32.ServiceAccessOptions.ACCESS_TYPE_ALL, + Interop.Advapi32.ServiceTypeOptions.SERVICE_WIN32_OWN_PROCESS, + (int)ServiceStartMode.Manual, + Interop.Advapi32.ServiceStartErrorModes.ERROR_CONTROL_NORMAL, + commandLine, + loadOrderGroup: null, + pTagId: IntPtr.Zero, + dependencies: null, + servicesStartName: null, + password: null)); + + if (serviceHandle.IsInvalid) + { + throw new Win32Exception("Could not create service"); + } + + return new WindowsServiceTester(serviceHandle, serviceName); + } + } + + internal unsafe Interop.Advapi32.SERVICE_STATUS QueryServiceStatus() + { + Interop.Advapi32.SERVICE_STATUS status = default; + bool success = Interop.Advapi32.QueryServiceStatus(_handle, &status); + if (!success) + { + throw new Win32Exception(); + } + return status; + } + internal unsafe Interop.Advapi32.SERVICE_STATUS_PROCESS QueryServiceStatusEx() + { + Interop.Advapi32.SERVICE_STATUS_PROCESS status = default; + bool success = Interop.Advapi32.QueryServiceStatusEx(_handle, &status); + if (!success) + { + throw new Win32Exception(); + } + return status; + } + + protected override void Dispose(bool disposing) + { + if (!_handle.IsInvalid) + { + // delete the temporary test service + Interop.Advapi32.DeleteService(_handle); + _handle.Close(); + _handle = null; + } + } + + } +} From 65bf8154183f2e5c88e2d8b75db52dd58a9db80b Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Thu, 30 Mar 2023 16:52:05 -0700 Subject: [PATCH 05/11] Respond to feedback and add more tests. This better integrates with the RemoteExecutor component as well, by hooking up the service process and fetching its handle. This gives us the correct logging and exitcode handling from RemoteExecutor. --- .../Advapi32/Interop.QueryServiceStatusEx.cs | 2 +- .../src/WindowsServiceLifetime.cs | 19 +- .../tests/UseWindowsServiceTests.cs | 2 +- .../tests/WindowsServiceLifetimeTests.cs | 250 ++++++++++++++++-- .../tests/WindowsServiceTester.cs | 65 +++-- 5 files changed, 279 insertions(+), 59 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs index db960cf9c9bf2e..8c38dec4df8eb4 100644 --- a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs +++ b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs @@ -29,6 +29,6 @@ internal struct SERVICE_STATUS_PROCESS [return: MarshalAs(UnmanagedType.Bool)] private static unsafe partial bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, int InfoLevel, SERVICE_STATUS_PROCESS* pStatus, int cbBufSize, out int pcbBytesNeeded); - internal static unsafe bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, SERVICE_STATUS_PROCESS* pStatus) => QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, pStatus, sizeof(SERVICE_STATUS_PROCESS), out int unused); + internal static unsafe bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, SERVICE_STATUS_PROCESS* pStatus) => QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, pStatus, sizeof(SERVICE_STATUS_PROCESS), out _); } } diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs index d0489e6442ecd1..76e5e23cc4bcdd 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs @@ -18,10 +18,10 @@ namespace Microsoft.Extensions.Hosting.WindowsServices public class WindowsServiceLifetime : ServiceBase, IHostLifetime { private readonly TaskCompletionSource _delayStart = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly TaskCompletionSource _serviceStopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _serviceDispatcherStopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim(); private readonly HostOptions _hostOptions; - private bool _isStopped; + private bool _serviceStopped; /// /// Initializes a new instance. @@ -89,23 +89,24 @@ private void Run() { Run(this); // This blocks until the service is stopped. _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); + _serviceDispatcherStopped.TrySetResult(null); } catch (Exception ex) { _delayStart.TrySetException(ex); + _serviceDispatcherStopped.TrySetException(ex); } - _serviceStopped.TrySetResult(null); } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { - if (!_isStopped) + if (!_serviceStopped) { - Task.Run(Stop, CancellationToken.None); + await Task.Run(Stop, CancellationToken.None).ConfigureAwait(false); } // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceStopped. - return _serviceStopped.Task; + await _serviceDispatcherStopped.Task.ConfigureAwait(false); } // Called by base.Run when the service is ready to start. @@ -122,7 +123,7 @@ protected override void OnStart(string[] args) /// This might be called multiple times by service Stop, ApplicationStopping, and StopAsync. That's okay because StopApplication uses a CancellationTokenSource and prevents any recursion. protected override void OnStop() { - _isStopped = true; + _serviceStopped = true; ApplicationLifetime.StopApplication(); // Wait for the host to shutdown before marking service as stopped. _delayStop.Wait(_hostOptions.ShutdownTimeout); @@ -134,7 +135,7 @@ protected override void OnStop() /// protected override void OnShutdown() { - _isStopped = true; + _serviceStopped = true; ApplicationLifetime.StopApplication(); // Wait for the host to shutdown before marking service as stopped. _delayStop.Wait(_hostOptions.ShutdownTimeout); diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs index 09aadcf310929b..c18d5037d665ad 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs @@ -32,7 +32,7 @@ public void DefaultsToOffOutsideOfService() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] public void CanCreateService() { - using var serviceTester = WindowsServiceTester.Create(nameof(CanCreateService), () => + using var serviceTester = WindowsServiceTester.Create(() => { using IHost host = new HostBuilder() .UseWindowsService() diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs index c781d3e3a3ae14..3bf7eea4711c4e 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs @@ -8,8 +8,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; @@ -18,30 +20,30 @@ namespace Microsoft.Extensions.Hosting public class WindowsServiceLifetimeTests { [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] - public void ServiceSequenceIsCorrect() + public void ServiceStops() { - using var serviceTester = WindowsServiceTester.Create(nameof(ServiceSequenceIsCorrect), () => + using var serviceTester = WindowsServiceTester.Create(() => { - SimpleServiceLogger.InitializeForTestCase(nameof(ServiceSequenceIsCorrect)); - using IHost host = new HostBuilder() - .ConfigureServices(services => - { - services.AddHostedService(); - services.AddSingleton(); - }) - .Build(); + var applicationLifetime = new ApplicationLifetime(NullLogger.Instance); + using var lifetime = new WindowsServiceLifetime( + new HostingEnvironment(), + applicationLifetime, + NullLoggerFactory.Instance, + new OptionsWrapper(new HostOptions())); - var applicationLifetime = host.Services.GetRequiredService(); - applicationLifetime.ApplicationStarted.Register(() => SimpleServiceLogger.Log($"lifetime started")); - applicationLifetime.ApplicationStopping.Register(() => SimpleServiceLogger.Log($"lifetime stopping")); - applicationLifetime.ApplicationStopped.Register(() => SimpleServiceLogger.Log($"lifetime stopped")); + lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult(); + + // would normally occur here, but WindowsServiceLifetime does not depend on it. + // applicationLifetime.NotifyStarted(); + + // will be signaled by WindowsServiceLifetime when SCM stops the service. + applicationLifetime.ApplicationStopping.WaitHandle.WaitOne(); - SimpleServiceLogger.Log("host.Run()"); - host.Run(); - SimpleServiceLogger.Log("host.Run() complete"); - }); + // required by WindowsServiceLifetime to identify that app has stopped. + applicationLifetime.NotifyStopped(); - SimpleServiceLogger.DeleteLog(nameof(ServiceSequenceIsCorrect)); + lifetime.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); + }); serviceTester.Start(); serviceTester.WaitForStatus(ServiceControllerStatus.Running); @@ -56,8 +58,156 @@ public void ServiceSequenceIsCorrect() var status = serviceTester.QueryServiceStatus(); Assert.Equal(0, status.win32ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework is missing the fix from https://github.com/dotnet/corefx/commit/3e68d791066ad0fdc6e0b81828afbd9df00dd7f8")] + public void ExceptionOnStartIsPropagated() + { + using var serviceTester = WindowsServiceTester.Create(() => + { + using (var lifetime = ThrowingWindowsServiceLifetime.Create()) + { + lifetime.ThrowOnStart = new Exception("Should be thrown"); + Assert.Equal(lifetime.ThrowOnStart, + Assert.Throws( () => + lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult() )); + } + }); + + serviceTester.Start(); + + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(1064, status.win32ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void ExceptionOnStopIsPropagated() + { + using var serviceTester = WindowsServiceTester.Create(() => + { + using (var lifetime = ThrowingWindowsServiceLifetime.Create()) + { + lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult(); + + lifetime.ThrowOnStop = new Exception("Should be thrown"); + lifetime.ApplicationLifetime.NotifyStopped(); + Assert.Equal(lifetime.ThrowOnStop, + Assert.Throws( () => + lifetime.StopAsync(CancellationToken.None).GetAwaiter().GetResult() )); + } + }); + + serviceTester.Start(); + + // service will proceed to stopped without any error + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(1067, status.win32ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void ServiceCanStopItself() + { + using (var serviceTester = WindowsServiceTester.Create(() => + { + FileLogger.InitializeForTestCase(nameof(ServiceCanStopItself)); + using IHost host = new HostBuilder() + .ConfigureServices(services => + { + services.AddHostedService(); + services.AddSingleton(); + }) + .Build(); + + var applicationLifetime = host.Services.GetRequiredService(); + applicationLifetime.ApplicationStarted.Register(() => FileLogger.Log($"lifetime started")); + applicationLifetime.ApplicationStopping.Register(() => FileLogger.Log($"lifetime stopping")); + applicationLifetime.ApplicationStopped.Register(() => FileLogger.Log($"lifetime stopped")); + + FileLogger.Log("host.Start()"); + host.Start(); - var logText = SimpleServiceLogger.ReadLog(nameof(ServiceSequenceIsCorrect)); + FileLogger.Log("host.Stop()"); + host.StopAsync().GetAwaiter().GetResult(); + FileLogger.Log("host.Stop() complete"); + })) + { + FileLogger.DeleteLog(nameof(ServiceCanStopItself)); + + // service should start cleanly + serviceTester.Start(); + + // service will proceed to stopped without any error + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(0, status.win32ExitCode); + + } + + var logText = FileLogger.ReadLog(nameof(ServiceCanStopItself)); + Assert.Equal(""" + host.Start() + WindowsServiceLifetime.OnStart + BackgroundService.StartAsync + lifetime started + host.Stop() + lifetime stopping + BackgroundService.StopAsync + lifetime stopped + WindowsServiceLifetime.OnStop + host.Stop() complete + + """, logText); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void ServiceSequenceIsCorrect() + { + using (var serviceTester = WindowsServiceTester.Create(() => + { + FileLogger.InitializeForTestCase(nameof(ServiceSequenceIsCorrect)); + using IHost host = new HostBuilder() + .ConfigureServices(services => + { + services.AddHostedService(); + services.AddSingleton(); + }) + .Build(); + + var applicationLifetime = host.Services.GetRequiredService(); + applicationLifetime.ApplicationStarted.Register(() => FileLogger.Log($"lifetime started")); + applicationLifetime.ApplicationStopping.Register(() => FileLogger.Log($"lifetime stopping")); + applicationLifetime.ApplicationStopped.Register(() => FileLogger.Log($"lifetime stopped")); + + FileLogger.Log("host.Run()"); + host.Run(); + FileLogger.Log("host.Run() complete"); + })) + { + + FileLogger.DeleteLog(nameof(ServiceSequenceIsCorrect)); + + serviceTester.Start(); + serviceTester.WaitForStatus(ServiceControllerStatus.Running); + + var statusEx = serviceTester.QueryServiceStatusEx(); + var serviceProcess = Process.GetProcessById(statusEx.dwProcessId); + + // Give a chance for all asynchronous "started" events to be raised, these happen after the service status changes to started + Thread.Sleep(1000); + + serviceTester.Stop(); + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(0, status.win32ExitCode); + + } + + var logText = FileLogger.ReadLog(nameof(ServiceSequenceIsCorrect)); Assert.Equal(""" host.Run() WindowsServiceLifetime.OnStart @@ -73,35 +223,77 @@ lifetime stopped } - public class SimpleWindowsServiceLifetime : WindowsServiceLifetime + public class LoggingWindowsServiceLifetime : WindowsServiceLifetime { - public SimpleWindowsServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions optionsAccessor) : + public LoggingWindowsServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions optionsAccessor) : base(environment, applicationLifetime, loggerFactory, optionsAccessor) { } protected override void OnStart(string[] args) { - SimpleServiceLogger.Log("WindowsServiceLifetime.OnStart"); + FileLogger.Log("WindowsServiceLifetime.OnStart"); base.OnStart(args); } protected override void OnStop() { - SimpleServiceLogger.Log("WindowsServiceLifetime.OnStop"); + FileLogger.Log("WindowsServiceLifetime.OnStop"); + base.OnStop(); + } + } + + public class ThrowingWindowsServiceLifetime : WindowsServiceLifetime + { + public static ThrowingWindowsServiceLifetime Create(Exception throwOnStart = null, Exception throwOnStop = null) => + new ThrowingWindowsServiceLifetime( + new HostingEnvironment(), + new ApplicationLifetime(NullLogger.Instance), + NullLoggerFactory.Instance, + new OptionsWrapper(new HostOptions())) + { + ThrowOnStart = throwOnStart, + ThrowOnStop = throwOnStop + }; + + public ThrowingWindowsServiceLifetime(IHostEnvironment environment, ApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions optionsAccessor) : + base(environment, applicationLifetime, loggerFactory, optionsAccessor) + { + ApplicationLifetime = applicationLifetime; + } + + public ApplicationLifetime ApplicationLifetime { get; } + + public Exception ThrowOnStart { get; set; } + protected override void OnStart(string[] args) + { + if (ThrowOnStart != null) + { + throw ThrowOnStart; + } + base.OnStart(args); + } + + public Exception ThrowOnStop { get; set; } + protected override void OnStop() + { + if (ThrowOnStop != null) + { + throw ThrowOnStop; + } base.OnStop(); } } - public class SimpleBackgroundService : BackgroundService + public class LoggingBackgroundService : BackgroundService { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.ExecuteAsync"); - public override async Task StartAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.StartAsync"); - public override async Task StopAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.StopAsync"); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) => FileLogger.Log("BackgroundService.ExecuteAsync"); + public override async Task StartAsync(CancellationToken stoppingToken) => FileLogger.Log("BackgroundService.StartAsync"); + public override async Task StopAsync(CancellationToken stoppingToken) => FileLogger.Log("BackgroundService.StopAsync"); #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously } - static class SimpleServiceLogger + static class FileLogger { static string _fileName; diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs index 3574d93942c4f1..bbb32456970eb8 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs @@ -3,6 +3,8 @@ using System; using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; using System.ServiceProcess; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32.SafeHandles; @@ -11,24 +13,45 @@ namespace Microsoft.Extensions.Hosting { public class WindowsServiceTester : ServiceController { - private WindowsServiceTester(SafeServiceHandle handle, string serviceName) : base(serviceName) + private WindowsServiceTester(SafeServiceHandle serviceHandle, RemoteInvokeHandle remoteInvokeHandle, string serviceName) : base(serviceName) { - _handle = handle; + _serviceHandle = serviceHandle; + _remoteInvokeHandle = remoteInvokeHandle; } - private SafeServiceHandle _handle; + private SafeServiceHandle _serviceHandle; + private RemoteInvokeHandle _remoteInvokeHandle; - public static WindowsServiceTester Create(string serviceName, Action serviceMain) + public new void Start() { - // create remote executor commandline arguments - string commandLine; - using (var remoteExecutorHandle = RemoteExecutor.Invoke(serviceMain, new RemoteInvokeOptions() { Start = false })) + Start(Array.Empty()); + } + + public new void Start(string[] args) + { + base.Start(args); + + // get the process + _remoteInvokeHandle.Process.Dispose(); + _remoteInvokeHandle.Process = null; + + var statusEx = QueryServiceStatusEx(); + try { - var startInfo = remoteExecutorHandle.Process.StartInfo; - remoteExecutorHandle.Process.Dispose(); - remoteExecutorHandle.Process = null; - commandLine = startInfo.FileName + " " + startInfo.Arguments; + _remoteInvokeHandle.Process = Process.GetProcessById(statusEx.dwProcessId); + // fetch the process handle so that we can get the exit code later. + var _ = _remoteInvokeHandle.Process.SafeHandle; } + catch (ArgumentException) + { } + } + + public static WindowsServiceTester Create(Action serviceMain, [CallerMemberName] string serviceName = null) + { + // create remote executor commandline arguments + var remoteInvokeHandle = RemoteExecutor.Invoke(serviceMain, new RemoteInvokeOptions() { Start = false }); + var startInfo = remoteInvokeHandle.Process.StartInfo; + string commandLine = startInfo.FileName + " " + startInfo.Arguments; // install the service using (var serviceManagerHandle = new SafeServiceHandle(Interop.Advapi32.OpenSCManager(null, null, Interop.Advapi32.ServiceControllerOptions.SC_MANAGER_ALL))) @@ -67,24 +90,25 @@ public static WindowsServiceTester Create(string serviceName, Action serviceMain throw new Win32Exception("Could not create service"); } - return new WindowsServiceTester(serviceHandle, serviceName); + return new WindowsServiceTester(serviceHandle, remoteInvokeHandle, serviceName); } } internal unsafe Interop.Advapi32.SERVICE_STATUS QueryServiceStatus() { Interop.Advapi32.SERVICE_STATUS status = default; - bool success = Interop.Advapi32.QueryServiceStatus(_handle, &status); + bool success = Interop.Advapi32.QueryServiceStatus(_serviceHandle, &status); if (!success) { throw new Win32Exception(); } return status; } + internal unsafe Interop.Advapi32.SERVICE_STATUS_PROCESS QueryServiceStatusEx() { Interop.Advapi32.SERVICE_STATUS_PROCESS status = default; - bool success = Interop.Advapi32.QueryServiceStatusEx(_handle, &status); + bool success = Interop.Advapi32.QueryServiceStatusEx(_serviceHandle, &status); if (!success) { throw new Win32Exception(); @@ -94,14 +118,17 @@ internal unsafe Interop.Advapi32.SERVICE_STATUS_PROCESS QueryServiceStatusEx() protected override void Dispose(bool disposing) { - if (!_handle.IsInvalid) + if (_remoteInvokeHandle != null) + { + _remoteInvokeHandle.Dispose(); + } + + if (!_serviceHandle.IsInvalid) { // delete the temporary test service - Interop.Advapi32.DeleteService(_handle); - _handle.Close(); - _handle = null; + Interop.Advapi32.DeleteService(_serviceHandle); + _serviceHandle.Close(); } } - } } From 2432d101132930a3fbd48ea1ddde6ee851a330bb Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Thu, 30 Mar 2023 19:27:37 -0700 Subject: [PATCH 06/11] Honor Cancellation in StopAsync --- .../src/WindowsServiceLifetime.cs | 4 ++- .../tests/WindowsServiceLifetimeTests.cs | 30 ++++++++++++++++++- .../tests/WindowsServiceTester.cs | 4 +-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs index 76e5e23cc4bcdd..7cb9af48ca2d0f 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs @@ -100,9 +100,11 @@ private void Run() public async Task StopAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + if (!_serviceStopped) { - await Task.Run(Stop, CancellationToken.None).ConfigureAwait(false); + await Task.Run(Stop, cancellationToken).ConfigureAwait(false); } // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceStopped. diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs index 3bf7eea4711c4e..cacfc0cb50c8eb 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs @@ -101,12 +101,40 @@ public void ExceptionOnStopIsPropagated() serviceTester.Start(); - // service will proceed to stopped without any error serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); var status = serviceTester.QueryServiceStatus(); Assert.Equal(1067, status.win32ExitCode); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void CancelStopAsync() + { + using var serviceTester = WindowsServiceTester.Create(() => + { + var applicationLifetime = new ApplicationLifetime(NullLogger.Instance); + using var lifetime = new WindowsServiceLifetime( + new HostingEnvironment(), + applicationLifetime, + NullLoggerFactory.Instance, + new OptionsWrapper(new HostOptions())); + { + lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult(); + + applicationLifetime.NotifyStopped(); + + Assert.ThrowsAsync(async () => await lifetime.StopAsync(new CancellationToken(true))); + lifetime.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + }); + + serviceTester.Start(); + + // service will proceed to stopped without any error + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + var status = serviceTester.QueryServiceStatus(); + Assert.Equal(0, status.win32ExitCode); + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] public void ServiceCanStopItself() { diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs index bbb32456970eb8..a2d70a2997e72d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs @@ -58,7 +58,7 @@ public static WindowsServiceTester Create(Action serviceMain, [CallerMemberName] { if (serviceManagerHandle.IsInvalid) { - throw new InvalidOperationException("Cannot open Service Control Manager"); + throw new InvalidOperationException(); } // delete existing service if it exists @@ -87,7 +87,7 @@ public static WindowsServiceTester Create(Action serviceMain, [CallerMemberName] if (serviceHandle.IsInvalid) { - throw new Win32Exception("Could not create service"); + throw new Win32Exception(); } return new WindowsServiceTester(serviceHandle, remoteInvokeHandle, serviceName); From 4b6bfc6818a3610fa6edb1c5b119114f9d4489bb Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Fri, 31 Mar 2023 12:46:20 -0700 Subject: [PATCH 07/11] Fix bindingRedirects in RemoteExecutor --- eng/testing/xunit/xunit.targets | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/eng/testing/xunit/xunit.targets b/eng/testing/xunit/xunit.targets index 6b048e6f6a9a4e..e72ebd444ad835 100644 --- a/eng/testing/xunit/xunit.targets +++ b/eng/testing/xunit/xunit.targets @@ -6,6 +6,11 @@ Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" /> + + true + true + + $(OutDir) From 27f55acb4377da8dcc6e55aeb3f03c3be3adede2 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Fri, 31 Mar 2023 12:47:44 -0700 Subject: [PATCH 08/11] Use Async lambdas for service testing --- .../tests/WindowsServiceLifetimeTests.cs | 49 ++++++++----------- .../tests/WindowsServiceTester.cs | 15 +++++- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs index cacfc0cb50c8eb..8d761590ac464b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs @@ -22,7 +22,7 @@ public class WindowsServiceLifetimeTests [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] public void ServiceStops() { - using var serviceTester = WindowsServiceTester.Create(() => + using var serviceTester = WindowsServiceTester.Create(async () => { var applicationLifetime = new ApplicationLifetime(NullLogger.Instance); using var lifetime = new WindowsServiceLifetime( @@ -31,7 +31,7 @@ public void ServiceStops() NullLoggerFactory.Instance, new OptionsWrapper(new HostOptions())); - lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult(); + await lifetime.WaitForStartAsync(CancellationToken.None); // would normally occur here, but WindowsServiceLifetime does not depend on it. // applicationLifetime.NotifyStarted(); @@ -42,7 +42,7 @@ public void ServiceStops() // required by WindowsServiceLifetime to identify that app has stopped. applicationLifetime.NotifyStopped(); - lifetime.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); + await lifetime.StopAsync(CancellationToken.None); }); serviceTester.Start(); @@ -64,14 +64,13 @@ public void ServiceStops() [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework is missing the fix from https://github.com/dotnet/corefx/commit/3e68d791066ad0fdc6e0b81828afbd9df00dd7f8")] public void ExceptionOnStartIsPropagated() { - using var serviceTester = WindowsServiceTester.Create(() => + using var serviceTester = WindowsServiceTester.Create(async () => { - using (var lifetime = ThrowingWindowsServiceLifetime.Create()) + using (var lifetime = ThrowingWindowsServiceLifetime.Create(throwOnStart: new Exception("Should be thrown"))) { - lifetime.ThrowOnStart = new Exception("Should be thrown"); Assert.Equal(lifetime.ThrowOnStart, - Assert.Throws( () => - lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult() )); + await Assert.ThrowsAsync(async () => + await lifetime.WaitForStartAsync(CancellationToken.None))); } }); @@ -85,17 +84,15 @@ public void ExceptionOnStartIsPropagated() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] public void ExceptionOnStopIsPropagated() { - using var serviceTester = WindowsServiceTester.Create(() => + using var serviceTester = WindowsServiceTester.Create(async () => { - using (var lifetime = ThrowingWindowsServiceLifetime.Create()) + using (var lifetime = ThrowingWindowsServiceLifetime.Create(throwOnStop: new Exception("Should be thrown"))) { - lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult(); - - lifetime.ThrowOnStop = new Exception("Should be thrown"); + await lifetime.WaitForStartAsync(CancellationToken.None); lifetime.ApplicationLifetime.NotifyStopped(); Assert.Equal(lifetime.ThrowOnStop, - Assert.Throws( () => - lifetime.StopAsync(CancellationToken.None).GetAwaiter().GetResult() )); + await Assert.ThrowsAsync( async () => + await lifetime.StopAsync(CancellationToken.None))); } }); @@ -109,7 +106,7 @@ public void ExceptionOnStopIsPropagated() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] public void CancelStopAsync() { - using var serviceTester = WindowsServiceTester.Create(() => + using var serviceTester = WindowsServiceTester.Create(async () => { var applicationLifetime = new ApplicationLifetime(NullLogger.Instance); using var lifetime = new WindowsServiceLifetime( @@ -117,28 +114,22 @@ public void CancelStopAsync() applicationLifetime, NullLoggerFactory.Instance, new OptionsWrapper(new HostOptions())); - { - lifetime.WaitForStartAsync(CancellationToken.None).GetAwaiter().GetResult(); - - applicationLifetime.NotifyStopped(); - - Assert.ThrowsAsync(async () => await lifetime.StopAsync(new CancellationToken(true))); - lifetime.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); - } + await lifetime.WaitForStartAsync(CancellationToken.None); + + await Assert.ThrowsAsync(async () => await lifetime.StopAsync(new CancellationToken(true))); }); serviceTester.Start(); - // service will proceed to stopped without any error - serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); + serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); var status = serviceTester.QueryServiceStatus(); - Assert.Equal(0, status.win32ExitCode); + Assert.Equal(1067, status.win32ExitCode); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] public void ServiceCanStopItself() { - using (var serviceTester = WindowsServiceTester.Create(() => + using (var serviceTester = WindowsServiceTester.Create(async () => { FileLogger.InitializeForTestCase(nameof(ServiceCanStopItself)); using IHost host = new HostBuilder() @@ -158,7 +149,7 @@ public void ServiceCanStopItself() host.Start(); FileLogger.Log("host.Stop()"); - host.StopAsync().GetAwaiter().GetResult(); + await host.StopAsync(); FileLogger.Log("host.Stop() complete"); })) { diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs index a2d70a2997e72d..ad023282ca47e0 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.ServiceProcess; +using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32.SafeHandles; @@ -46,10 +47,20 @@ private WindowsServiceTester(SafeServiceHandle serviceHandle, RemoteInvokeHandle { } } - public static WindowsServiceTester Create(Action serviceMain, [CallerMemberName] string serviceName = null) + // the following overloads are necessary to ensure the compiler will produce the correct signature from a lambda. + public static WindowsServiceTester Create(Func serviceMain, [CallerMemberName] string serviceName = null) => Create(RemoteExecutor.Invoke(serviceMain, remoteInvokeOptions), serviceName); + + public static WindowsServiceTester Create(Func> serviceMain, [CallerMemberName] string serviceName = null) => Create(RemoteExecutor.Invoke(serviceMain, remoteInvokeOptions), serviceName); + + public static WindowsServiceTester Create(Func serviceMain, [CallerMemberName] string serviceName = null) => Create(RemoteExecutor.Invoke(serviceMain, remoteInvokeOptions), serviceName); + + public static WindowsServiceTester Create(Action serviceMain, [CallerMemberName] string serviceName = null) => Create(RemoteExecutor.Invoke(serviceMain, remoteInvokeOptions), serviceName); + + private static RemoteInvokeOptions remoteInvokeOptions = new RemoteInvokeOptions() { Start = false }; + + private static WindowsServiceTester Create(RemoteInvokeHandle remoteInvokeHandle, string serviceName) { // create remote executor commandline arguments - var remoteInvokeHandle = RemoteExecutor.Invoke(serviceMain, new RemoteInvokeOptions() { Start = false }); var startInfo = remoteInvokeHandle.Process.StartInfo; string commandLine = startInfo.FileName + " " + startInfo.Arguments; From 80cfc7e5423e5ca713454ede1c27a6996c423eb4 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Fri, 31 Mar 2023 12:48:11 -0700 Subject: [PATCH 09/11] Fix issue on Win7 where duplicate service descriptions are disallowed --- .../tests/WindowsServiceTester.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs index ad023282ca47e0..e0f72d450de15c 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs @@ -84,7 +84,7 @@ private static WindowsServiceTester Create(RemoteInvokeHandle remoteInvokeHandle var serviceHandle = new SafeServiceHandle( Interop.Advapi32.CreateService(serviceManagerHandle, serviceName, - $"{nameof(WindowsServiceTester)} test service", + $"{nameof(WindowsServiceTester)} {serviceName} test service", Interop.Advapi32.ServiceAccessOptions.ACCESS_TYPE_ALL, Interop.Advapi32.ServiceTypeOptions.SERVICE_WIN32_OWN_PROCESS, (int)ServiceStartMode.Manual, From eae37333fadf6cf0c6eb8ac5a6d7fed628178d2d Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Tue, 4 Apr 2023 12:21:51 -0700 Subject: [PATCH 10/11] Respond to feedback --- .../src/Interop/Windows/Interop.Errors.cs | 2 ++ .../src/WindowsServiceLifetime.cs | 23 ++++++++++++------- ...sions.Hosting.WindowsServices.Tests.csproj | 2 ++ .../tests/WindowsServiceLifetimeTests.cs | 6 ++--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs index cde3ae0ac197e8..c810603e6300a9 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs @@ -64,6 +64,8 @@ internal static partial class Errors internal const int ERROR_IO_PENDING = 0x3E5; internal const int ERROR_NO_TOKEN = 0x3f0; internal const int ERROR_SERVICE_DOES_NOT_EXIST = 0x424; + internal const int ERROR_EXCEPTION_IN_SERVICE = 0x428; + internal const int ERROR_PROCESS_ABORTED = 0x42B; internal const int ERROR_NO_UNICODE_TRANSLATION = 0x459; internal const int ERROR_DLL_INIT_FAILED = 0x45A; internal const int ERROR_COUNTER_TIMEOUT = 0x461; diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs index 7cb9af48ca2d0f..f10f7ba6b8f4e8 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs @@ -21,7 +21,7 @@ public class WindowsServiceLifetime : ServiceBase, IHostLifetime private readonly TaskCompletionSource _serviceDispatcherStopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim(); private readonly HostOptions _hostOptions; - private bool _serviceStopped; + private bool _serviceStopRequested; /// /// Initializes a new instance. @@ -98,16 +98,20 @@ private void Run() } } + /// + /// Called from to stop the service if not already stopped, and wait for the service dispatcher to exit. + /// Once this method returns the service is stopped and the process can be terminated at any time. + /// public async Task StopAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (!_serviceStopped) + if (!_serviceStopRequested) { await Task.Run(Stop, cancellationToken).ConfigureAwait(false); } - // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceStopped. + // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceStopRequested. await _serviceDispatcherStopped.Task.ConfigureAwait(false); } @@ -120,12 +124,13 @@ protected override void OnStart(string[] args) } /// - /// Raises the Stop event to stop the . + /// Executes when a Stop command is sent to the service by the Service Control Manager (SCM). + /// Triggers and waits for . + /// Shortly after this method returns, the Service will be marked as stopped in SCM and the process may exit at any point. /// - /// This might be called multiple times by service Stop, ApplicationStopping, and StopAsync. That's okay because StopApplication uses a CancellationTokenSource and prevents any recursion. protected override void OnStop() { - _serviceStopped = true; + _serviceStopRequested = true; ApplicationLifetime.StopApplication(); // Wait for the host to shutdown before marking service as stopped. _delayStop.Wait(_hostOptions.ShutdownTimeout); @@ -133,11 +138,13 @@ protected override void OnStop() } /// - /// Raises the Shutdown event. + /// Executes when a Shutdown command is sent to the service by the Service Control Manager (SCM). + /// Triggers and waits for . + /// Shortly after this method returns, the Service will be marked as stopped in SCM and the process may exit at any point. /// protected override void OnShutdown() { - _serviceStopped = true; + _serviceStopRequested = true; ApplicationLifetime.StopApplication(); // Wait for the host to shutdown before marking service as stopped. _delayStop.Wait(_hostOptions.ShutdownTimeout); diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj index f9447576dfa314..ee433d9207d1d2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj @@ -19,6 +19,8 @@ + (async () => serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); var status = serviceTester.QueryServiceStatus(); - Assert.Equal(1064, status.win32ExitCode); + Assert.Equal(Interop.Errors.ERROR_EXCEPTION_IN_SERVICE, status.win32ExitCode); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] @@ -100,7 +100,7 @@ await Assert.ThrowsAsync( async () => serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); var status = serviceTester.QueryServiceStatus(); - Assert.Equal(1067, status.win32ExitCode); + Assert.Equal(Interop.Errors.ERROR_PROCESS_ABORTED, status.win32ExitCode); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] @@ -123,7 +123,7 @@ public void CancelStopAsync() serviceTester.WaitForStatus(ServiceControllerStatus.Stopped); var status = serviceTester.QueryServiceStatus(); - Assert.Equal(1067, status.win32ExitCode); + Assert.Equal(Interop.Errors.ERROR_PROCESS_ABORTED, status.win32ExitCode); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] From efbb10188460a20edd1d24f3ed51c8c8ebaf4efd Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Wed, 5 Apr 2023 13:01:20 -0700 Subject: [PATCH 11/11] Fix comment and add timeout --- .../src/WindowsServiceLifetime.cs | 2 +- .../tests/WindowsServiceTester.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs index f10f7ba6b8f4e8..7edd3ea3d35c32 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs @@ -111,7 +111,7 @@ public async Task StopAsync(CancellationToken cancellationToken) await Task.Run(Stop, cancellationToken).ConfigureAwait(false); } - // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceStopRequested. + // When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceDispatcherStopped. await _serviceDispatcherStopped.Task.ConfigureAwait(false); } diff --git a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs index e0f72d450de15c..895b4a87108ebc 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceTester.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32.SafeHandles; +using Xunit; namespace Microsoft.Extensions.Hosting { @@ -47,6 +48,18 @@ private WindowsServiceTester(SafeServiceHandle serviceHandle, RemoteInvokeHandle { } } + public TimeSpan WaitForStatusTimeout { get; set; } = TimeSpan.FromSeconds(30); + + public new void WaitForStatus(ServiceControllerStatus desiredStatus) => + WaitForStatus(desiredStatus, WaitForStatusTimeout); + + public new void WaitForStatus(ServiceControllerStatus desiredStatus, TimeSpan timeout) + { + base.WaitForStatus(desiredStatus, timeout); + + Assert.Equal(Status, desiredStatus); + } + // the following overloads are necessary to ensure the compiler will produce the correct signature from a lambda. public static WindowsServiceTester Create(Func serviceMain, [CallerMemberName] string serviceName = null) => Create(RemoteExecutor.Invoke(serviceMain, remoteInvokeOptions), serviceName);