-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Make WindowsServiceLifetime gracefully stop #83892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9802470
Make WindowsServiceLifetime gracefully stop
ericstj a146ad0
Alternate approach to ensuring we only ever set STATE_STOPPED once.
ericstj e6de2e1
Avoid calling ServiceBase.Stop on stopped service
ericstj eb39960
Add tests for WindowsServiceLifetime
ericstj 65bf815
Respond to feedback and add more tests.
ericstj 2432d10
Honor Cancellation in StopAsync
ericstj 4b6bfc6
Fix bindingRedirects in RemoteExecutor
ericstj 27f55ac
Use Async lambdas for service testing
ericstj 80cfc7e
Fix issue on Win7 where duplicate service descriptions are disallowed
ericstj eae3733
Respond to feedback
ericstj efbb101
Fix comment and add timeout
ericstj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
34 changes: 34 additions & 0 deletions
34
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.QueryServiceStatusEx.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
...braries/Microsoft.Extensions.Hosting.WindowsServices/tests/WindowsServiceLifetimeTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<SimpleBackgroundService>(); | ||
| services.AddSingleton<IHostLifetime, SimpleWindowsServiceLifetime>(); | ||
| }) | ||
| .Build(); | ||
|
|
||
| var applicationLifetime = host.Services.GetRequiredService<IHostApplicationLifetime>(); | ||
| 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<HostOptions> 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.