Skip to content
Merged
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ namespace Microsoft.Extensions.Hosting.WindowsServices
public class WindowsServiceLifetime : ServiceBase, IHostLifetime
{
private readonly TaskCompletionSource<object?> _delayStart = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource<object?> _serviceStopped = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly ManualResetEventSlim _delayStop = new ManualResetEventSlim();
private readonly HostOptions _hostOptions;
private bool _isStopped;

/// <summary>
/// Initializes a new <see cref="WindowsServiceLifetime"/> instance.
Expand Down Expand Up @@ -92,14 +94,18 @@ 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.
Task.Run(Stop, CancellationToken.None);
return Task.CompletedTask;
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;
}

// Called by base.Run when the service is ready to start.
Expand All @@ -116,6 +122,7 @@ protected override void OnStart(string[] args)
/// <remarks>This might be called multiple times by service Stop, ApplicationStopping, and StopAsync. That's okay because StopApplication uses a CancellationTokenSource and prevents any recursion.</remarks>
protected override void OnStop()
{
_isStopped = true;
ApplicationLifetime.StopApplication();
// Wait for the host to shutdown before marking service as stopped.
_delayStop.Wait(_hostOptions.ShutdownTimeout);
Expand All @@ -127,6 +134,7 @@ protected override void OnStop()
/// </summary>
protected override void OnShutdown()
{
_isStopped = true;
ApplicationLifetime.StopApplication();
// Wait for the host to shutdown before marking service as stopped.
_delayStop.Wait(_hostOptions.ShutdownTimeout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,43 @@
<!-- Use "$(NetCoreAppCurrent)-windows" to avoid PlatformNotSupportedExceptions from ServiceController. -->
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetFrameworkMinimum)</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<EnableLibraryImportGenerator>true</EnableLibraryImportGenerator>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
</PropertyGroup>

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

<ItemGroup>
<Compile Include="$(LibrariesProjectRoot)System.ServiceProcess.ServiceController\src\Microsoft\Win32\SafeHandles\SafeServiceHandle.cs"
Link="Microsoft\Win32\SafeHandles\SafeServiceHandle.cs" />
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs"
Link="Common\DisableRuntimeMarshalling.cs"
Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
Link="Common\Interop\Windows\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.ServiceProcessOptions.cs"
Link="Common\Interop\Windows\Interop.ServiceProcessOptions.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CloseServiceHandle.cs"
Link="Common\Interop\Windows\Interop.CloseServiceHandle.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CreateService.cs"
Link="Common\Interop\Windows\Interop.CreateService.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.DeleteService.cs"
Link="Common\Interop\Windows\Interop.DeleteService.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenService.cs"
Link="Common\Interop\Windows\Interop.OpenService.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenSCManager.cs"
Link="Common\Interop\Windows\Interop.OpenSCManager.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatus.cs"
Link="Common\Interop\Windows\Interop.QueryServiceStatus.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatusEx.cs"
Link="Common\Interop\Windows\Interop.QueryServiceStatusEx.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.SERVICE_STATUS.cs"
Link="Common\Interop\Windows\Interop.SERVICE_STATUS.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<Reference Include="System.ServiceProcess" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -30,6 +29,26 @@ public void DefaultsToOffOutsideOfService()
Assert.IsType<ConsoleLifetime>(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()
{
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
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);
}
}
}
}
}
Loading