Skip to content

Commit c8ab0d3

Browse files
ericstjVSadov
andauthored
[release/7.0-staging] Make WindowsServiceLifetime gracefully stop (#85656)
* Make WindowsServiceLifetime gracefully stop (#83892) * 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. * Alternate approach to ensuring we only ever set STATE_STOPPED once. * 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. * Add tests for WindowsServiceLifetime These tests leverage RemoteExecutor to avoid creating a separate service assembly. * 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. * Honor Cancellation in StopAsync * Fix bindingRedirects in RemoteExecutor * Use Async lambdas for service testing * Fix issue on Win7 where duplicate service descriptions are disallowed * Respond to feedback * Fix comment and add timeout * Fix test condition * Enable M.E.H.WindowsServices and S.SP.ServiceController for servicing * Make service wait on its state before stopping (#84447) * Fix WindowsService Tests where RemoteExecutor is unsupported * Enable MS.W.C for servicing * Reference latest Microsoft.Extensions.Logging.Abstractions This package has been serviced and we compile against the serviced version of its assemblies. None of the directly referenced projects have been serviced so our package doesn't restore the serviced versions. Lift up the dependency on Logging.Abstractions to ensure we reference the serviced package. --------- Co-authored-by: Vladimir Sadov <vsadov@microsoft.com>
1 parent 47f52f1 commit c8ab0d3

File tree

12 files changed

+690
-41
lines changed

12 files changed

+690
-41
lines changed

eng/testing/xunit/xunit.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
77
</ItemGroup>
88

9+
<PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
10+
<AutoGenerateBindingRedirects Condition="'$(AutoGenerateBindingRedirects)' == ''">true</AutoGenerateBindingRedirects>
11+
<GenerateBindingRedirectsOutputType Condition="'$(GenerateBindingRedirectsOutputType)' == ''">true</GenerateBindingRedirectsOutputType>
12+
</PropertyGroup>
13+
914
<!-- Run target (F5) support. -->
1015
<PropertyGroup>
1116
<RunWorkingDirectory Condition="'$(RunWorkingDirectory)' == ''">$(OutDir)</RunWorkingDirectory>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Win32.SafeHandles;
5+
using System;
6+
using System.Runtime.InteropServices;
7+
8+
internal static partial class Interop
9+
{
10+
internal static partial class Advapi32
11+
{
12+
[StructLayout(LayoutKind.Sequential)]
13+
internal struct SERVICE_STATUS_PROCESS
14+
{
15+
public int dwServiceType;
16+
public int dwCurrentState;
17+
public int dwControlsAccepted;
18+
public int dwWin32ExitCode;
19+
public int dwServiceSpecificExitCode;
20+
public int dwCheckPoint;
21+
public int dwWaitHint;
22+
public int dwProcessId;
23+
public int dwServiceFlags;
24+
}
25+
26+
private const int SC_STATUS_PROCESS_INFO = 0;
27+
28+
[LibraryImport(Libraries.Advapi32, SetLastError = true)]
29+
[return: MarshalAs(UnmanagedType.Bool)]
30+
private static unsafe partial bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, int InfoLevel, SERVICE_STATUS_PROCESS* pStatus, int cbBufSize, out int pcbBytesNeeded);
31+
32+
internal static unsafe bool QueryServiceStatusEx(SafeServiceHandle serviceHandle, SERVICE_STATUS_PROCESS* pStatus) => QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, pStatus, sizeof(SERVICE_STATUS_PROCESS), out _);
33+
}
34+
}

src/libraries/Common/src/Interop/Windows/Interop.Errors.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ internal static partial class Errors
6464
internal const int ERROR_IO_PENDING = 0x3E5;
6565
internal const int ERROR_NO_TOKEN = 0x3f0;
6666
internal const int ERROR_SERVICE_DOES_NOT_EXIST = 0x424;
67+
internal const int ERROR_EXCEPTION_IN_SERVICE = 0x428;
68+
internal const int ERROR_PROCESS_ABORTED = 0x42B;
6769
internal const int ERROR_NO_UNICODE_TRANSLATION = 0x459;
6870
internal const int ERROR_DLL_INIT_FAILED = 0x45A;
6971
internal const int ERROR_COUNTER_TIMEOUT = 0x461;

src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/Microsoft.Extensions.Hosting.WindowsServices.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
<EnableAOTAnalyzer>true</EnableAOTAnalyzer>
88
<PackageDescription>.NET hosting infrastructure for Windows Services.</PackageDescription>
99
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
10+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
11+
<ServicingVersion>1</ServicingVersion>
1012
</PropertyGroup>
1113

1214
<ItemGroup>
@@ -27,6 +29,7 @@
2729

2830
<ItemGroup>
2931
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Hosting\src\Microsoft.Extensions.Hosting.csproj" />
32+
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Logging.Abstractions\src\Microsoft.Extensions.Logging.Abstractions.csproj" />
3033
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Logging.EventLog\src\Microsoft.Extensions.Logging.EventLog.csproj" />
3134
</ItemGroup>
3235

src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetime.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ namespace Microsoft.Extensions.Hosting.WindowsServices
1515
public class WindowsServiceLifetime : ServiceBase, IHostLifetime
1616
{
1717
private readonly TaskCompletionSource<object?> _delayStart = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
18+
private readonly TaskCompletionSource<object?> _serviceDispatcherStopped = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
1819
private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim();
1920
private readonly HostOptions _hostOptions;
21+
private bool _serviceStopRequested;
2022

2123
public WindowsServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor)
2224
: this(environment, applicationLifetime, loggerFactory, optionsAccessor, Options.Options.Create(new WindowsServiceLifetimeOptions()))
@@ -69,19 +71,30 @@ private void Run()
6971
{
7072
Run(this); // This blocks until the service is stopped.
7173
_delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
74+
_serviceDispatcherStopped.TrySetResult(null);
7275
}
7376
catch (Exception ex)
7477
{
7578
_delayStart.TrySetException(ex);
79+
_serviceDispatcherStopped.TrySetException(ex);
7680
}
7781
}
7882

79-
public Task StopAsync(CancellationToken cancellationToken)
83+
/// <summary>
84+
/// Called from <see cref="IHost.StopAsync"/> to stop the service if not already stopped, and wait for the service dispatcher to exit.
85+
/// Once this method returns the service is stopped and the process can be terminated at any time.
86+
/// </summary>
87+
public async Task StopAsync(CancellationToken cancellationToken)
8088
{
81-
// Avoid deadlock where host waits for StopAsync before firing ApplicationStopped,
82-
// and Stop waits for ApplicationStopped.
83-
Task.Run(Stop, CancellationToken.None);
84-
return Task.CompletedTask;
89+
cancellationToken.ThrowIfCancellationRequested();
90+
91+
if (!_serviceStopRequested)
92+
{
93+
await Task.Run(Stop, cancellationToken).ConfigureAwait(false);
94+
}
95+
96+
// When the underlying service is stopped this will cause the ServiceBase.Run method to complete and return, which completes _serviceDispatcherStopped.
97+
await _serviceDispatcherStopped.Task.ConfigureAwait(false);
8598
}
8699

87100
// Called by base.Run when the service is ready to start.
@@ -91,18 +104,28 @@ protected override void OnStart(string[] args)
91104
base.OnStart(args);
92105
}
93106

94-
// Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
95-
// That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
107+
/// <summary>
108+
/// Executes when a Stop command is sent to the service by the Service Control Manager (SCM).
109+
/// Triggers <see cref="IHostApplicationLifetime.ApplicationStopping"/> and waits for <see cref="IHostApplicationLifetime.ApplicationStopped"/>.
110+
/// Shortly after this method returns, the Service will be marked as stopped in SCM and the process may exit at any point.
111+
/// </summary>
96112
protected override void OnStop()
97113
{
114+
_serviceStopRequested = true;
98115
ApplicationLifetime.StopApplication();
99116
// Wait for the host to shutdown before marking service as stopped.
100117
_delayStop.Wait(_hostOptions.ShutdownTimeout);
101118
base.OnStop();
102119
}
103120

121+
/// <summary>
122+
/// Executes when a Shutdown command is sent to the service by the Service Control Manager (SCM).
123+
/// Triggers <see cref="IHostApplicationLifetime.ApplicationStopping"/> and waits for <see cref="IHostApplicationLifetime.ApplicationStopped"/>.
124+
/// Shortly after this method returns, the Service will be marked as stopped in SCM and the process may exit at any point.
125+
/// </summary>
104126
protected override void OnShutdown()
105127
{
128+
_serviceStopRequested = true;
106129
ApplicationLifetime.StopApplication();
107130
// Wait for the host to shutdown before marking service as stopped.
108131
_delayStop.Wait(_hostOptions.ShutdownTimeout);

src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,45 @@
44
<!-- Use "$(NetCoreAppCurrent)-windows" to avoid PlatformNotSupportedExceptions from ServiceController. -->
55
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetFrameworkMinimum)</TargetFrameworks>
66
<EnableDefaultItems>true</EnableDefaultItems>
7+
<EnableLibraryImportGenerator>true</EnableLibraryImportGenerator>
8+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
9+
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
710
</PropertyGroup>
811

912
<ItemGroup>
1013
<ProjectReference Include="..\src\Microsoft.Extensions.Hosting.WindowsServices.csproj" />
1114
</ItemGroup>
1215

16+
<ItemGroup>
17+
<Compile Include="$(LibrariesProjectRoot)System.ServiceProcess.ServiceController\src\Microsoft\Win32\SafeHandles\SafeServiceHandle.cs"
18+
Link="Microsoft\Win32\SafeHandles\SafeServiceHandle.cs" />
19+
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs"
20+
Link="Common\DisableRuntimeMarshalling.cs"
21+
Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
22+
<Compile Include="$(CommonPath)Interop\Windows\Interop.Errors.cs"
23+
Link="Common\Interop\Windows\Interop.Errors.cs" />
24+
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
25+
Link="Common\Interop\Windows\Interop.Libraries.cs" />
26+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.ServiceProcessOptions.cs"
27+
Link="Common\Interop\Windows\Interop.ServiceProcessOptions.cs" />
28+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CloseServiceHandle.cs"
29+
Link="Common\Interop\Windows\Interop.CloseServiceHandle.cs" />
30+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CreateService.cs"
31+
Link="Common\Interop\Windows\Interop.CreateService.cs" />
32+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.DeleteService.cs"
33+
Link="Common\Interop\Windows\Interop.DeleteService.cs" />
34+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenService.cs"
35+
Link="Common\Interop\Windows\Interop.OpenService.cs" />
36+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenSCManager.cs"
37+
Link="Common\Interop\Windows\Interop.OpenSCManager.cs" />
38+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatus.cs"
39+
Link="Common\Interop\Windows\Interop.QueryServiceStatus.cs" />
40+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatusEx.cs"
41+
Link="Common\Interop\Windows\Interop.QueryServiceStatusEx.cs" />
42+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.SERVICE_STATUS.cs"
43+
Link="Common\Interop\Windows\Interop.SERVICE_STATUS.cs" />
44+
</ItemGroup>
45+
1346
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
1447
<Reference Include="System.ServiceProcess" />
1548
</ItemGroup>

src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.IO;
65
using System.Reflection;
76
using System.ServiceProcess;
7+
using Microsoft.DotNet.RemoteExecutor;
88
using Microsoft.Extensions.DependencyInjection;
99
using Microsoft.Extensions.Hosting.Internal;
1010
using Microsoft.Extensions.Hosting.WindowsServices;
@@ -17,6 +17,8 @@ namespace Microsoft.Extensions.Hosting
1717
{
1818
public class UseWindowsServiceTests
1919
{
20+
private static bool IsRemoteExecutorSupportedAndPrivilegedProcess => RemoteExecutor.IsSupported && AdminHelpers.IsProcessElevated();
21+
2022
private static MethodInfo? _addWindowsServiceLifetimeMethod = null;
2123

2224
[Fact]
@@ -30,6 +32,26 @@ public void DefaultsToOffOutsideOfService()
3032
Assert.IsType<ConsoleLifetime>(lifetime);
3133
}
3234

35+
[ConditionalFact(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
36+
public void CanCreateService()
37+
{
38+
using var serviceTester = WindowsServiceTester.Create(() =>
39+
{
40+
using IHost host = new HostBuilder()
41+
.UseWindowsService()
42+
.Build();
43+
host.Run();
44+
});
45+
46+
serviceTester.Start();
47+
serviceTester.WaitForStatus(ServiceControllerStatus.Running);
48+
serviceTester.Stop();
49+
serviceTester.WaitForStatus(ServiceControllerStatus.Stopped);
50+
51+
var status = serviceTester.QueryServiceStatus();
52+
Assert.Equal(0, status.win32ExitCode);
53+
}
54+
3355
[Fact]
3456
public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService()
3557
{
@@ -66,7 +88,7 @@ public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationN
6688
var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings
6789
{
6890
ApplicationName = appName,
69-
});
91+
});
7092

7193
// Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
7294
AddWindowsServiceLifetime(builder.Services);
@@ -82,7 +104,7 @@ public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationN
82104
[Fact]
83105
public void ServiceCollectionExtensionMethodCanBeCalledOnDefaultConfiguration()
84106
{
85-
var builder = new HostApplicationBuilder();
107+
var builder = new HostApplicationBuilder();
86108

87109
// Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
88110
AddWindowsServiceLifetime(builder.Services);

0 commit comments

Comments
 (0)