Skip to content

Commit eb39960

Browse files
committed
Add tests for WindowsServiceLifetime
These tests leverage RemoteExecutor to avoid creating a separate service assembly.
1 parent e6de2e1 commit eb39960

File tree

5 files changed

+321
-3
lines changed

5 files changed

+321
-3
lines changed
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 int unused);
33+
}
34+
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,43 @@
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.Libraries.cs"
23+
Link="Common\Interop\Windows\Interop.Libraries.cs" />
24+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.ServiceProcessOptions.cs"
25+
Link="Common\Interop\Windows\Interop.ServiceProcessOptions.cs" />
26+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CloseServiceHandle.cs"
27+
Link="Common\Interop\Windows\Interop.CloseServiceHandle.cs" />
28+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.CreateService.cs"
29+
Link="Common\Interop\Windows\Interop.CreateService.cs" />
30+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.DeleteService.cs"
31+
Link="Common\Interop\Windows\Interop.DeleteService.cs" />
32+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenService.cs"
33+
Link="Common\Interop\Windows\Interop.OpenService.cs" />
34+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.OpenSCManager.cs"
35+
Link="Common\Interop\Windows\Interop.OpenSCManager.cs" />
36+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatus.cs"
37+
Link="Common\Interop\Windows\Interop.QueryServiceStatus.cs" />
38+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.QueryServiceStatusEx.cs"
39+
Link="Common\Interop\Windows\Interop.QueryServiceStatusEx.cs" />
40+
<Compile Include="$(CommonPath)Interop\Windows\Advapi32\Interop.SERVICE_STATUS.cs"
41+
Link="Common\Interop\Windows\Interop.SERVICE_STATUS.cs" />
42+
</ItemGroup>
43+
1344
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
1445
<Reference Include="System.ServiceProcess" />
1546
</ItemGroup>

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
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;
87
using Microsoft.Extensions.DependencyInjection;
@@ -30,6 +29,26 @@ public void DefaultsToOffOutsideOfService()
3029
Assert.IsType<ConsoleLifetime>(lifetime);
3130
}
3231

32+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
33+
public void CanCreateService()
34+
{
35+
using var serviceTester = WindowsServiceTester.Create(nameof(CanCreateService), () =>
36+
{
37+
using IHost host = new HostBuilder()
38+
.UseWindowsService()
39+
.Build();
40+
host.Run();
41+
});
42+
43+
serviceTester.Start();
44+
serviceTester.WaitForStatus(ServiceControllerStatus.Running);
45+
serviceTester.Stop();
46+
serviceTester.WaitForStatus(ServiceControllerStatus.Stopped);
47+
48+
var status = serviceTester.QueryServiceStatus();
49+
Assert.Equal(0, status.win32ExitCode);
50+
}
51+
3352
[Fact]
3453
public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService()
3554
{
@@ -66,7 +85,7 @@ public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationN
6685
var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings
6786
{
6887
ApplicationName = appName,
69-
});
88+
});
7089

7190
// Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
7291
AddWindowsServiceLifetime(builder.Services);
@@ -82,7 +101,7 @@ public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationN
82101
[Fact]
83102
public void ServiceCollectionExtensionMethodCanBeCalledOnDefaultConfiguration()
84103
{
85-
var builder = new HostApplicationBuilder();
104+
var builder = new HostApplicationBuilder();
86105

87106
// Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
88107
AddWindowsServiceLifetime(builder.Services);
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using System.ServiceProcess;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Hosting.WindowsServices;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Options;
14+
using Xunit;
15+
16+
namespace Microsoft.Extensions.Hosting
17+
{
18+
public class WindowsServiceLifetimeTests
19+
{
20+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
21+
public void ServiceSequenceIsCorrect()
22+
{
23+
using var serviceTester = WindowsServiceTester.Create(nameof(ServiceSequenceIsCorrect), () =>
24+
{
25+
SimpleServiceLogger.InitializeForTestCase(nameof(ServiceSequenceIsCorrect));
26+
using IHost host = new HostBuilder()
27+
.ConfigureServices(services =>
28+
{
29+
services.AddHostedService<SimpleBackgroundService>();
30+
services.AddSingleton<IHostLifetime, SimpleWindowsServiceLifetime>();
31+
})
32+
.Build();
33+
34+
var applicationLifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
35+
applicationLifetime.ApplicationStarted.Register(() => SimpleServiceLogger.Log($"lifetime started"));
36+
applicationLifetime.ApplicationStopping.Register(() => SimpleServiceLogger.Log($"lifetime stopping"));
37+
applicationLifetime.ApplicationStopped.Register(() => SimpleServiceLogger.Log($"lifetime stopped"));
38+
39+
SimpleServiceLogger.Log("host.Run()");
40+
host.Run();
41+
SimpleServiceLogger.Log("host.Run() complete");
42+
});
43+
44+
SimpleServiceLogger.DeleteLog(nameof(ServiceSequenceIsCorrect));
45+
46+
serviceTester.Start();
47+
serviceTester.WaitForStatus(ServiceControllerStatus.Running);
48+
49+
var statusEx = serviceTester.QueryServiceStatusEx();
50+
var serviceProcess = Process.GetProcessById(statusEx.dwProcessId);
51+
52+
serviceTester.Stop();
53+
serviceTester.WaitForStatus(ServiceControllerStatus.Stopped);
54+
55+
serviceProcess.WaitForExit();
56+
57+
var status = serviceTester.QueryServiceStatus();
58+
Assert.Equal(0, status.win32ExitCode);
59+
60+
var logText = SimpleServiceLogger.ReadLog(nameof(ServiceSequenceIsCorrect));
61+
Assert.Equal("""
62+
host.Run()
63+
WindowsServiceLifetime.OnStart
64+
BackgroundService.StartAsync
65+
lifetime started
66+
WindowsServiceLifetime.OnStop
67+
lifetime stopping
68+
BackgroundService.StopAsync
69+
lifetime stopped
70+
host.Run() complete
71+
72+
""", logText);
73+
74+
}
75+
76+
public class SimpleWindowsServiceLifetime : WindowsServiceLifetime
77+
{
78+
public SimpleWindowsServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor) :
79+
base(environment, applicationLifetime, loggerFactory, optionsAccessor)
80+
{ }
81+
82+
protected override void OnStart(string[] args)
83+
{
84+
SimpleServiceLogger.Log("WindowsServiceLifetime.OnStart");
85+
base.OnStart(args);
86+
}
87+
88+
protected override void OnStop()
89+
{
90+
SimpleServiceLogger.Log("WindowsServiceLifetime.OnStop");
91+
base.OnStop();
92+
}
93+
}
94+
95+
public class SimpleBackgroundService : BackgroundService
96+
{
97+
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
98+
protected override async Task ExecuteAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.ExecuteAsync");
99+
public override async Task StartAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.StartAsync");
100+
public override async Task StopAsync(CancellationToken stoppingToken) => SimpleServiceLogger.Log("BackgroundService.StopAsync");
101+
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
102+
}
103+
104+
static class SimpleServiceLogger
105+
{
106+
static string _fileName;
107+
108+
public static void InitializeForTestCase(string testCaseName)
109+
{
110+
Assert.Null(_fileName);
111+
_fileName = GetLogForTestCase(testCaseName);
112+
}
113+
114+
private static string GetLogForTestCase(string testCaseName) => Path.Combine(AppContext.BaseDirectory, $"{testCaseName}.log");
115+
public static void DeleteLog(string testCaseName) => File.Delete(GetLogForTestCase(testCaseName));
116+
public static string ReadLog(string testCaseName) => File.ReadAllText(GetLogForTestCase(testCaseName));
117+
public static void Log(string message)
118+
{
119+
Assert.NotNull(_fileName);
120+
lock (_fileName)
121+
{
122+
File.AppendAllText(_fileName, message + Environment.NewLine);
123+
}
124+
}
125+
}
126+
}
127+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 System;
5+
using System.ComponentModel;
6+
using System.ServiceProcess;
7+
using Microsoft.DotNet.RemoteExecutor;
8+
using Microsoft.Win32.SafeHandles;
9+
10+
namespace Microsoft.Extensions.Hosting
11+
{
12+
public class WindowsServiceTester : ServiceController
13+
{
14+
private WindowsServiceTester(SafeServiceHandle handle, string serviceName) : base(serviceName)
15+
{
16+
_handle = handle;
17+
}
18+
19+
private SafeServiceHandle _handle;
20+
21+
public static WindowsServiceTester Create(string serviceName, Action serviceMain)
22+
{
23+
// create remote executor commandline arguments
24+
string commandLine;
25+
using (var remoteExecutorHandle = RemoteExecutor.Invoke(serviceMain, new RemoteInvokeOptions() { Start = false }))
26+
{
27+
var startInfo = remoteExecutorHandle.Process.StartInfo;
28+
remoteExecutorHandle.Process.Dispose();
29+
remoteExecutorHandle.Process = null;
30+
commandLine = startInfo.FileName + " " + startInfo.Arguments;
31+
}
32+
33+
// install the service
34+
using (var serviceManagerHandle = new SafeServiceHandle(Interop.Advapi32.OpenSCManager(null, null, Interop.Advapi32.ServiceControllerOptions.SC_MANAGER_ALL)))
35+
{
36+
if (serviceManagerHandle.IsInvalid)
37+
{
38+
throw new InvalidOperationException("Cannot open Service Control Manager");
39+
}
40+
41+
// delete existing service if it exists
42+
using (var existingServiceHandle = new SafeServiceHandle(Interop.Advapi32.OpenService(serviceManagerHandle, serviceName, Interop.Advapi32.ServiceAccessOptions.ACCESS_TYPE_ALL)))
43+
{
44+
if (!existingServiceHandle.IsInvalid)
45+
{
46+
Interop.Advapi32.DeleteService(existingServiceHandle);
47+
}
48+
}
49+
50+
var serviceHandle = new SafeServiceHandle(
51+
Interop.Advapi32.CreateService(serviceManagerHandle,
52+
serviceName,
53+
$"{nameof(WindowsServiceTester)} test service",
54+
Interop.Advapi32.ServiceAccessOptions.ACCESS_TYPE_ALL,
55+
Interop.Advapi32.ServiceTypeOptions.SERVICE_WIN32_OWN_PROCESS,
56+
(int)ServiceStartMode.Manual,
57+
Interop.Advapi32.ServiceStartErrorModes.ERROR_CONTROL_NORMAL,
58+
commandLine,
59+
loadOrderGroup: null,
60+
pTagId: IntPtr.Zero,
61+
dependencies: null,
62+
servicesStartName: null,
63+
password: null));
64+
65+
if (serviceHandle.IsInvalid)
66+
{
67+
throw new Win32Exception("Could not create service");
68+
}
69+
70+
return new WindowsServiceTester(serviceHandle, serviceName);
71+
}
72+
}
73+
74+
internal unsafe Interop.Advapi32.SERVICE_STATUS QueryServiceStatus()
75+
{
76+
Interop.Advapi32.SERVICE_STATUS status = default;
77+
bool success = Interop.Advapi32.QueryServiceStatus(_handle, &status);
78+
if (!success)
79+
{
80+
throw new Win32Exception();
81+
}
82+
return status;
83+
}
84+
internal unsafe Interop.Advapi32.SERVICE_STATUS_PROCESS QueryServiceStatusEx()
85+
{
86+
Interop.Advapi32.SERVICE_STATUS_PROCESS status = default;
87+
bool success = Interop.Advapi32.QueryServiceStatusEx(_handle, &status);
88+
if (!success)
89+
{
90+
throw new Win32Exception();
91+
}
92+
return status;
93+
}
94+
95+
protected override void Dispose(bool disposing)
96+
{
97+
if (!_handle.IsInvalid)
98+
{
99+
// delete the temporary test service
100+
Interop.Advapi32.DeleteService(_handle);
101+
_handle.Close();
102+
_handle = null;
103+
}
104+
}
105+
106+
}
107+
}

0 commit comments

Comments
 (0)