Skip to content

Commit d628263

Browse files
ericstjVSadov
andauthored
[release/6.0-staging] Make WindowsServiceLifetime gracefully stop (#85661)
* 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 # Conflicts: # src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs * 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 # Conflicts: # src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs * Port changes to 6.0 codebase * Version Microsoft.Windows.Compatibility --------- Co-authored-by: Vladimir Sadov <[email protected]>
1 parent 7d7cce6 commit d628263

File tree

12 files changed

+696
-44
lines changed

12 files changed

+696
-44
lines changed

eng/testing/xunit/xunit.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
<PackageReference Include="NETStandard.Library" Version="$(NetStandardLibraryVersion)" />
55
</ItemGroup>
66

7+
<PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
8+
<AutoGenerateBindingRedirects Condition="'$(AutoGenerateBindingRedirects)' == ''">true</AutoGenerateBindingRedirects>
9+
<GenerateBindingRedirectsOutputType Condition="'$(GenerateBindingRedirectsOutputType)' == ''">true</GenerateBindingRedirectsOutputType>
10+
</PropertyGroup>
11+
712
<!-- Run target (F5) support. -->
813
<PropertyGroup>
914
<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+
[DllImport(Libraries.Advapi32, SetLastError = true)]
29+
[return: MarshalAs(UnmanagedType.Bool)]
30+
private static unsafe extern 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
@@ -63,6 +63,8 @@ internal static partial class Errors
6363
internal const int ERROR_IO_PENDING = 0x3E5;
6464
internal const int ERROR_NO_TOKEN = 0x3f0;
6565
internal const int ERROR_SERVICE_DOES_NOT_EXIST = 0x424;
66+
internal const int ERROR_EXCEPTION_IN_SERVICE = 0x428;
67+
internal const int ERROR_PROCESS_ABORTED = 0x42B;
6668
internal const int ERROR_NO_UNICODE_TRANSLATION = 0x459;
6769
internal const int ERROR_DLL_INIT_FAILED = 0x45A;
6870
internal const int ERROR_COUNTER_TIMEOUT = 0x461;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
<TargetFrameworks>net461;netstandard2.0;netstandard2.1</TargetFrameworks>
55
<EnableDefaultItems>true</EnableDefaultItems>
66
<PackageDescription>.NET hosting infrastructure for Windows Services.</PackageDescription>
7-
<ServicingVersion>1</ServicingVersion>
7+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
8+
<ServicingVersion>2</ServicingVersion>
89
</PropertyGroup>
910

1011
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ namespace Microsoft.Extensions.Hosting.WindowsServices
1313
public class WindowsServiceLifetime : ServiceBase, IHostLifetime
1414
{
1515
private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
16+
private readonly TaskCompletionSource<object> _serviceDispatcherStopped = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
1617
private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim();
1718
private readonly HostOptions _hostOptions;
19+
private bool _serviceStopRequested;
1820

1921
public WindowsServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor)
2022
: this(environment, applicationLifetime, loggerFactory, optionsAccessor, Options.Options.Create(new WindowsServiceLifetimeOptions()))
@@ -73,19 +75,30 @@ private void Run()
7375
{
7476
Run(this); // This blocks until the service is stopped.
7577
_delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
78+
_serviceDispatcherStopped.TrySetResult(null);
7679
}
7780
catch (Exception ex)
7881
{
7982
_delayStart.TrySetException(ex);
83+
_serviceDispatcherStopped.TrySetException(ex);
8084
}
8185
}
8286

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

91104
// Called by base.Run when the service is ready to start.
@@ -95,18 +108,28 @@ protected override void OnStart(string[] args)
95108
base.OnStart(args);
96109
}
97110

98-
// Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
99-
// That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
111+
/// <summary>
112+
/// Executes when a Stop command is sent to the service by the Service Control Manager (SCM).
113+
/// Triggers <see cref="IHostApplicationLifetime.ApplicationStopping"/> and waits for <see cref="IHostApplicationLifetime.ApplicationStopped"/>.
114+
/// Shortly after this method returns, the Service will be marked as stopped in SCM and the process may exit at any point.
115+
/// </summary>
100116
protected override void OnStop()
101117
{
118+
_serviceStopRequested = true;
102119
ApplicationLifetime.StopApplication();
103120
// Wait for the host to shutdown before marking service as stopped.
104121
_delayStop.Wait(_hostOptions.ShutdownTimeout);
105122
base.OnStop();
106123
}
107124

125+
/// <summary>
126+
/// Executes when a Shutdown command is sent to the service by the Service Control Manager (SCM).
127+
/// Triggers <see cref="IHostApplicationLifetime.ApplicationStopping"/> and waits for <see cref="IHostApplicationLifetime.ApplicationStopped"/>.
128+
/// Shortly after this method returns, the Service will be marked as stopped in SCM and the process may exit at any point.
129+
/// </summary>
108130
protected override void OnShutdown()
109131
{
132+
_serviceStopRequested = true;
110133
ApplicationLifetime.StopApplication();
111134
// Wait for the host to shutdown before marking service as stopped.
112135
_delayStop.Wait(_hostOptions.ShutdownTimeout);
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,46 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>$(NetCoreAppCurrent);net461</TargetFrameworks>
4+
<!-- Use "$(NetCoreAppCurrent)-windows" to avoid PlatformNotSupportedExceptions from ServiceController. -->
5+
<TargetFrameworks>$(NetCoreAppCurrent)-windows;net461</TargetFrameworks>
56
<EnableDefaultItems>true</EnableDefaultItems>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
69
</PropertyGroup>
710

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

15+
<ItemGroup>
16+
<Compile Include="$(LibrariesProjectRoot)System.ServiceProcess.ServiceController\src\Microsoft\Win32\SafeHandles\SafeServiceHandle.cs"
17+
Link="Microsoft\Win32\SafeHandles\SafeServiceHandle.cs" />
18+
<Compile Include="$(CommonPath)Interop\Windows\Interop.Errors.cs"
19+
Link="Common\Interop\Windows\Interop.Errors.cs" />
20+
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
21+
Link="Common\Interop\Windows\Interop.Libraries.cs" />
22+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.ServiceProcessOptions.cs"
23+
Link="Common\Interop\Windows\Interop.ServiceProcessOptions.cs" />
24+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CloseServiceHandle.cs"
25+
Link="Common\Interop\Windows\Interop.CloseServiceHandle.cs" />
26+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CreateService.cs"
27+
Link="Common\Interop\Windows\Interop.CreateService.cs" />
28+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.DeleteService.cs"
29+
Link="Common\Interop\Windows\Interop.DeleteService.cs" />
30+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenService.cs"
31+
Link="Common\Interop\Windows\Interop.OpenService.cs" />
32+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenSCManager.cs"
33+
Link="Common\Interop\Windows\Interop.OpenSCManager.cs" />
34+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatus.cs"
35+
Link="Common\Interop\Windows\Interop.QueryServiceStatus.cs" />
36+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatusEx.cs"
37+
Link="Common\Interop\Windows\Interop.QueryServiceStatusEx.cs" />
38+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.SERVICE_STATUS.cs"
39+
Link="Common\Interop\Windows\Interop.SERVICE_STATUS.cs" />
40+
</ItemGroup>
41+
42+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
43+
<Reference Include="System.ServiceProcess" />
44+
</ItemGroup>
45+
1246
</Project>
Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
5+
using System.Reflection;
6+
using System.ServiceProcess;
7+
using Microsoft.DotNet.RemoteExecutor;
48
using Microsoft.Extensions.DependencyInjection;
59
using Microsoft.Extensions.Hosting.Internal;
10+
using Microsoft.Extensions.Hosting.WindowsServices;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Logging.EventLog;
13+
using Microsoft.Extensions.Options;
614
using Xunit;
715

816
namespace Microsoft.Extensions.Hosting
917
{
1018
public class UseWindowsServiceTests
1119
{
20+
private static bool IsRemoteExecutorSupportedAndPrivilegedProcess => RemoteExecutor.IsSupported && AdminHelpers.IsProcessElevated();
21+
1222
[Fact]
1323
public void DefaultsToOffOutsideOfService()
1424
{
@@ -18,9 +28,29 @@ public void DefaultsToOffOutsideOfService()
1828

1929
using (host)
2030
{
21-
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
22-
Assert.IsType<ConsoleLifetime>(lifetime);
23-
}
31+
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
32+
Assert.IsType<ConsoleLifetime>(lifetime);
33+
}
34+
}
35+
36+
[ConditionalFact(nameof(IsRemoteExecutorSupportedAndPrivilegedProcess))]
37+
public void CanCreateService()
38+
{
39+
using var serviceTester = WindowsServiceTester.Create(() =>
40+
{
41+
using IHost host = new HostBuilder()
42+
.UseWindowsService()
43+
.Build();
44+
host.Run();
45+
});
46+
47+
serviceTester.Start();
48+
serviceTester.WaitForStatus(ServiceControllerStatus.Running);
49+
serviceTester.Stop();
50+
serviceTester.WaitForStatus(ServiceControllerStatus.Stopped);
51+
52+
var status = serviceTester.QueryServiceStatus();
53+
Assert.Equal(0, status.win32ExitCode);
2454
}
2555
}
2656
}

0 commit comments

Comments
 (0)