Skip to content
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

Add AddSystemd() and AddWindowsService() IServiceCollection extension methods #68580

Merged
merged 21 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace Microsoft.Extensions.Hosting
{
public static partial class SystemdHostBuilderExtensions
{
public static Microsoft.Extensions.Hosting.IHostBuilder UseSystemd(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection UseSystemd(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder AddSystemd(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder) { throw null; }
}
}
namespace Microsoft.Extensions.Hosting.Systemd
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
Expand Up @@ -13,7 +13,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 +27,63 @@ 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)
{
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)
{
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,28 @@ public class UseSystemdTests
[Fact]
public void DefaultsToOffOutsideOfService()
{
var host = new HostBuilder()
.UseSystemd()
using IHost host = new HostBuilder()
.AddSystemd()
.Build();

using (host)
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
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.UseSystemd();
using IHost host = builder.Build();

var lifetime = host.Services.GetRequiredService<IHostLifetime>();
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 UseWindowsService(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,137 @@ 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"/>, sets the <see cref="IHostEnvironment.ContentRootPath"/>
/// to <see cref="AppContext.BaseDirectory"/>, 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>
/// <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"/>, sets the <see cref="IHostEnvironment.ContentRootPath"/>
/// to <see cref="AppContext.BaseDirectory"/>, 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)
{
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"/>, verifies the <see cref="IHostEnvironment.ContentRootPath"/>
/// is equal to <see cref="AppContext.BaseDirectory"/>, 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"/>, verifies the <see cref="IHostEnvironment.ContentRootPath"/>
/// is equal to <see cref="AppContext.BaseDirectory"/>, 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)
{
if (WindowsServiceHelpers.IsWindowsService())
{
AddWindowsServiceUnchecked(services, configure);
}

return hostBuilder;
return services;
}

// This is a separate method for testing.
private static void AddWindowsServiceUnchecked(IServiceCollection services, Action<WindowsServiceLifetimeOptions> configure)
{
services.Configure<WindowsServiceLifetimeOptions>(options =>
{
// Host.CreateDefaultBuilder uses CurrentDirectory for VS scenarios, but CurrentDirectory for services is c:\Windows\System32.
options.ValidateContentRoot = true;
});
AddWindowsServiceLifetime(services, configure);
}

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
Expand Up @@ -11,5 +11,11 @@ public class WindowsServiceLifetimeOptions
/// The name used to identify the service to the system.
/// </summary>
public string ServiceName { get; set; } = string.Empty;

// Used by the IServiceCollection overload of UseWindowsService to indicate that WindowsServiceLifetime
// should verify IHostEnvironment.ContentRootPath = AppContext.BaseDirectory (usually the default).
// This should also be the content root for the IHostBuilder overload unless it's been overridden, but
// we don't want to break people who might have successfully overridden IHostBuilder's ContentRoot.
internal bool ValidateContentRoot { get; set; }
}
}
Loading