Skip to content

Commit

Permalink
Add AddSystemd() and AddWindowsService() IServiceCollection extension…
Browse files Browse the repository at this point in the history
… methods (#68580)

* Add AddSystemd() IServiceCollection extension method

* Add AddWindowsService() IServiceCollection extension method

* Don't default to CWD if in C:\Windows\system32
- instead, when CWD is C:\Windows\system32 Hosting will use AppContext.BaseDirectory. This way Windows apps and services that are launched will work by default. HostApplicationBuilder.ContentRootPath can't be changed after construction, so setting it to a workable default for Windows apps.

Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
Co-authored-by: Martin Costello <martin@martincostello.com>

* Use RemoteExecutor

* Update src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs

* Skip test on Windows nano server

* Respond to PR feedback

Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
Co-authored-by: Martin Costello <martin@martincostello.com>
  • Loading branch information
3 people authored Jun 20, 2022
1 parent 1070187 commit e2cddab
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.Extensions.Hosting
{
public static partial class SystemdHostBuilderExtensions
{
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddSystemd(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseSystemd(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder) { throw null; }
}
}
Expand All @@ -31,19 +32,19 @@ public static partial class SystemdHelpers
{
public static bool IsSystemdService() { throw null; }
}
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
public partial class SystemdLifetime : Microsoft.Extensions.Hosting.IHostLifetime, System.IDisposable
{
public SystemdLifetime(Microsoft.Extensions.Hosting.IHostEnvironment environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Hosting.Systemd.ISystemdNotifier systemdNotifier, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public void Dispose() { }
public System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task WaitForStartAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
}
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public partial class SystemdNotifier : Microsoft.Extensions.Hosting.Systemd.ISystemdNotifier
{
public SystemdNotifier() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Systemd;
using Microsoft.Extensions.Logging.Console;
Expand All @@ -13,7 +14,7 @@ namespace Microsoft.Extensions.Hosting
public static class SystemdHostBuilderExtensions
{
/// <summary>
/// Sets the host lifetime to <see cref="SystemdLifetime" />,
/// Configures the <see cref="IHost"/> lifetime to <see cref="SystemdLifetime"/>,
/// provides notification messages for application started and stopping,
/// and configures console logging to the systemd format.
/// </summary>
Expand All @@ -27,27 +28,67 @@ public static class SystemdHostBuilderExtensions
/// notifications. See https://www.freedesktop.org/software/systemd/man/systemd.service.html.
/// </para>
/// </remarks>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to use.</param>
/// <returns></returns>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to configure.</param>
/// <returns>The <paramref name="hostBuilder"/> instance for chaining.</returns>
public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder)
{
ThrowHelper.ThrowIfNull(hostBuilder);

if (SystemdHelpers.IsSystemdService())
{
hostBuilder.ConfigureServices((hostContext, services) =>
{
services.Configure<ConsoleLoggerOptions>(options =>
{
options.FormatterName = ConsoleFormatterNames.Systemd;
});

// IsSystemdService() will never return true for android/browser/iOS/tvOS
#pragma warning disable CA1416 // Validate platform compatibility
services.AddSingleton<ISystemdNotifier, SystemdNotifier>();
services.AddSingleton<IHostLifetime, SystemdLifetime>();
#pragma warning restore CA1416 // Validate platform compatibility
AddSystemdLifetime(services);
});
}
return hostBuilder;
}

/// <summary>
/// Configures the lifetime of the <see cref="IHost"/> built from <paramref name="services"/> to
/// <see cref="SystemdLifetime"/>, provides notification messages for application started
/// and stopping, and configures console logging to the systemd format.
/// </summary>
/// <remarks>
/// <para>
/// This is context aware and will only activate if it detects the process is running
/// as a systemd Service.
/// </para>
/// <para>
/// The systemd service file must be configured with <c>Type=notify</c> to enable
/// notifications. See <see href="https://www.freedesktop.org/software/systemd/man/systemd.service.html"/>.
/// </para>
/// </remarks>
/// <param name="services">
/// The <see cref="IServiceCollection"/> used to build the <see cref="IHost"/>.
/// For example, <see cref="HostApplicationBuilder.Services"/> or the <see cref="IServiceCollection"/> passed to the
/// <see cref="IHostBuilder.ConfigureServices(System.Action{HostBuilderContext, IServiceCollection})"/> callback.
/// </param>
/// <returns>The <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddSystemd(this IServiceCollection services)
{
ThrowHelper.ThrowIfNull(services);

if (SystemdHelpers.IsSystemdService())
{
AddSystemdLifetime(services);
}
return services;
}

private static void AddSystemdLifetime(IServiceCollection services)
{
services.Configure<ConsoleLoggerOptions>(options =>
{
options.FormatterName = ConsoleFormatterNames.Systemd;
});

// IsSystemdService() will never return true for android/browser/iOS/tvOS
#pragma warning disable CA1416 // Validate platform compatibility
services.AddSingleton<ISystemdNotifier, SystemdNotifier>();
services.AddSingleton<IHostLifetime, SystemdLifetime>();
#pragma warning restore CA1416 // Validate platform compatibility

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,30 @@ public class UseSystemdTests
[Fact]
public void DefaultsToOffOutsideOfService()
{
var host = new HostBuilder()
using IHost host = new HostBuilder()
.UseSystemd()
.Build();

using (host)
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
Assert.NotNull(lifetime);
Assert.IsNotType<SystemdLifetime>(lifetime);
}

[Fact]
public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService()
{
var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings
{
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
Assert.NotNull(lifetime);
Assert.IsNotType<SystemdLifetime>(lifetime);
}
// Disable defaults that may not be supported on the testing platform like EventLogLoggerProvider.
DisableDefaults = true,
});

builder.Services.AddSystemd();
using IHost host = builder.Build();

var lifetime = host.Services.GetRequiredService<IHostLifetime>();
Assert.NotNull(lifetime);
Assert.IsNotType<SystemdLifetime>(lifetime);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Microsoft.Extensions.Hosting
{
public static partial class WindowsServiceLifetimeHostBuilderExtensions
{
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddWindowsService(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddWindowsService(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Extensions.Hosting.WindowsServiceLifetimeOptions> configure) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseWindowsService(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseWindowsService(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.WindowsServiceLifetimeOptions> configure) { throw null; }
}
Expand All @@ -21,10 +23,10 @@ namespace Microsoft.Extensions.Hosting.WindowsServices
{
public static partial class WindowsServiceHelpers
{
[System.Runtime.Versioning.SupportedOSPlatformGuard("windows")]
[System.Runtime.Versioning.SupportedOSPlatformGuardAttribute("windows")]
public static bool IsWindowsService() { throw null; }
}
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public partial class WindowsServiceLifetime : System.ServiceProcess.ServiceBase, Microsoft.Extensions.Hosting.IHostLifetime
{
public WindowsServiceLifetime(Microsoft.Extensions.Hosting.IHostEnvironment environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Hosting.HostOptions> optionsAccessor) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Hosting
{
Expand All @@ -17,62 +18,124 @@ namespace Microsoft.Extensions.Hosting
public static class WindowsServiceLifetimeHostBuilderExtensions
{
/// <summary>
/// Sets the host lifetime to WindowsServiceLifetime, sets the Content Root,
/// and enables logging to the event log with the application name as the default source name.
/// Sets the host lifetime to <see cref="WindowsServiceLifetime"/> and enables logging to the event log with
/// the application name as the default source name.
/// </summary>
/// <remarks>
/// This is context aware and will only activate if it detects the process is running
/// as a Windows Service.
/// This is context aware and will only activate if it detects the process is running as a Windows Service.
/// </remarks>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to operate on.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
/// <returns>The <paramref name="hostBuilder"/> instance for chaining.</returns>
public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder)
{
return UseWindowsService(hostBuilder, _ => { });
}

/// <summary>
/// Sets the host lifetime to WindowsServiceLifetime, sets the Content Root,
/// and enables logging to the event log with the application name as the default source name.
/// Sets the host lifetime to <see cref="WindowsServiceLifetime"/> and enables logging to the event log with the application
/// name as the default source name.
/// </summary>
/// <remarks>
/// This is context aware and will only activate if it detects the process is running
/// as a Windows Service.
/// </remarks>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to operate on.</param>
/// <param name="configure"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
/// <param name="configure">An <see cref="Action{WindowsServiceLifetimeOptions}"/> to configure the provided <see cref="WindowsServiceLifetimeOptions"/>.</param>
/// <returns>The <paramref name="hostBuilder"/> instance for chaining.</returns>
public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder, Action<WindowsServiceLifetimeOptions> configure)
{
ThrowHelper.ThrowIfNull(hostBuilder);

if (WindowsServiceHelpers.IsWindowsService())
{
// Host.CreateDefaultBuilder uses CurrentDirectory for VS scenarios, but CurrentDirectory for services is c:\Windows\System32.
hostBuilder.UseContentRoot(AppContext.BaseDirectory);
hostBuilder.ConfigureLogging((hostingContext, logging) =>
hostBuilder.ConfigureServices(services =>
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
AddWindowsServiceLifetime(services, configure);
});
}

logging.AddEventLog();
})
.ConfigureServices((hostContext, services) =>
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
return hostBuilder;
}

services.AddSingleton<IHostLifetime, WindowsServiceLifetime>();
services.Configure<EventLogSettings>(settings =>
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
/// <summary>
/// Configures the lifetime of the <see cref="IHost"/> built from <paramref name="services"/> to
/// <see cref="WindowsServiceLifetime"/> and enables logging to the event log with the application
/// name as the default source name.
/// </summary>
/// <remarks>
/// This is context aware and will only activate if it detects the process is running
/// as a Windows Service.
/// </remarks>
/// <param name="services">
/// The <see cref="IServiceCollection"/> used to build the <see cref="IHost"/>.
/// For example, <see cref="HostApplicationBuilder.Services"/> or the <see cref="IServiceCollection"/> passed to the
/// <see cref="IHostBuilder.ConfigureServices(Action{HostBuilderContext, IServiceCollection})"/> callback.
/// </param>
/// <returns>The <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddWindowsService(this IServiceCollection services)
{
return AddWindowsService(services, _ => { });
}

if (string.IsNullOrEmpty(settings.SourceName))
{
settings.SourceName = hostContext.HostingEnvironment.ApplicationName;
}
});
services.Configure(configure);
});
/// <summary>
/// Configures the lifetime of the <see cref="IHost"/> built from <paramref name="services"/> to
/// <see cref="WindowsServiceLifetime"/> and enables logging to the event log with the application name as the default source name.
/// </summary>
/// <remarks>
/// This is context aware and will only activate if it detects the process is running
/// as a Windows Service.
/// </remarks>
/// <param name="services">
/// The <see cref="IServiceCollection"/> used to build the <see cref="IHost"/>.
/// For example, <see cref="HostApplicationBuilder.Services"/> or the <see cref="IServiceCollection"/> passed to the
/// <see cref="IHostBuilder.ConfigureServices(Action{HostBuilderContext, IServiceCollection})"/> callback.
/// </param>
/// <param name="configure">An <see cref="Action{WindowsServiceLifetimeOptions}"/> to configure the provided <see cref="WindowsServiceLifetimeOptions"/>.</param>
/// <returns>The <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddWindowsService(this IServiceCollection services, Action<WindowsServiceLifetimeOptions> configure)
{
ThrowHelper.ThrowIfNull(services);

if (WindowsServiceHelpers.IsWindowsService())
{
AddWindowsServiceLifetime(services, configure);
}

return hostBuilder;
return services;
}

private static void AddWindowsServiceLifetime(IServiceCollection services, Action<WindowsServiceLifetimeOptions> configure)
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));

services.AddLogging(logging =>
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
logging.AddEventLog();
});
services.AddSingleton<IHostLifetime, WindowsServiceLifetime>();
services.AddSingleton<IConfigureOptions<EventLogSettings>, EventLogSettingsSetup>();
services.Configure(configure);
}

private sealed class EventLogSettingsSetup : IConfigureOptions<EventLogSettings>
{
private readonly string? _applicationName;

public EventLogSettingsSetup(IHostEnvironment environment)
{
_applicationName = environment.ApplicationName;
}

public void Configure(EventLogSettings settings)
{
Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));

if (string.IsNullOrEmpty(settings.SourceName))
{
settings.SourceName = _applicationName;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkMinimum)</TargetFrameworks>
<!-- Use "$(NetCoreAppCurrent)-windows" to avoid PlatformNotSupportedExceptions from ServiceController. -->
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetFrameworkMinimum)</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
</PropertyGroup>

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

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<Reference Include="System.ServiceProcess" />
</ItemGroup>

</Project>
Loading

0 comments on commit e2cddab

Please sign in to comment.