Add AddSystemd() and AddWindowsService() IServiceCollection extension methods (#68580)
authorStephen Halter <halter73@gmail.com>
Mon, 20 Jun 2022 21:40:30 +0000 (14:40 -0700)
committerGitHub <noreply@github.com>
Mon, 20 Jun 2022 21:40:30 +0000 (16:40 -0500)
* 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>
src/libraries/Microsoft.Extensions.Hosting.Systemd/ref/Microsoft.Extensions.Hosting.Systemd.cs
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs
src/libraries/Microsoft.Extensions.Hosting.WindowsServices/ref/Microsoft.Extensions.Hosting.WindowsServices.cs
src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetimeHostBuilderExtensions.cs
src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj
src/libraries/Microsoft.Extensions.Hosting.WindowsServices/tests/UseWindowsServiceTests.cs
src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj

index bb70b75..7d1462a 100644 (file)
@@ -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; }
     }
 }
@@ -31,11 +32,11 @@ namespace Microsoft.Extensions.Hosting.Systemd
     {
         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) { }
@@ -43,7 +44,7 @@ namespace Microsoft.Extensions.Hosting.Systemd
         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() { }
index 60eb7db..5be2833 100644 (file)
@@ -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;
@@ -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>
@@ -27,27 +28,67 @@ namespace Microsoft.Extensions.Hosting
         ///     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
+
+        }
     }
 }
index 328a1b8..948f9b4 100644 (file)
@@ -12,16 +12,30 @@ namespace Microsoft.Extensions.Hosting
         [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);
         }
     }
 }
index c22e9cc..6b81268 100644 (file)
@@ -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; }
     }
@@ -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) { }
index 9f5aa6c..90e8035 100644 (file)
@@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting.WindowsServices;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.EventLog;
+using Microsoft.Extensions.Options;
 
 namespace Microsoft.Extensions.Hosting
 {
@@ -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;
+                }
+            }
         }
     }
 }
index ef10859..93be9b8 100644 (file)
@@ -1,7 +1,8 @@
-<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>
 
@@ -9,4 +10,8 @@
     <ProjectReference Include="..\src\Microsoft.Extensions.Hosting.WindowsServices.csproj" />
   </ItemGroup>
 
+  <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
+    <Reference Include="System.ServiceProcess" />
+  </ItemGroup>
+
 </Project>
index 512c2c0..1fb2ade 100644 (file)
 // 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;
 using Microsoft.Extensions.Hosting.Internal;
+using Microsoft.Extensions.Hosting.WindowsServices;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.EventLog;
+using Microsoft.Extensions.Options;
 using Xunit;
 
 namespace Microsoft.Extensions.Hosting
 {
     public class UseWindowsServiceTests
     {
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))]
+        private static MethodInfo? _addWindowsServiceLifetimeMethod = null;
+
+        [Fact]
         public void DefaultsToOffOutsideOfService()
         {
-            var host = new HostBuilder()
+            using IHost host = new HostBuilder()
                 .UseWindowsService()
                 .Build();
 
-            using (host)
+            var lifetime = host.Services.GetRequiredService<IHostLifetime>();
+            Assert.IsType<ConsoleLifetime>(lifetime);
+        }
+
+        [Fact]
+        public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService()
+        {
+            var builder = new HostApplicationBuilder();
+
+            builder.Services.AddWindowsService();
+            // No reason to write event logs in this test. Event log may be unsupported anyway.
+            builder.Logging.ClearProviders();
+
+            using IHost host = builder.Build();
+
+            var lifetime = host.Services.GetRequiredService<IHostLifetime>();
+            Assert.IsType<ConsoleLifetime>(lifetime);
+        }
+
+        [Fact]
+        public void ServiceCollectionExtensionMethodAddsWindowsServiceLifetimeInsideOfService()
+        {
+            var builder = new HostApplicationBuilder();
+
+            // Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
+            AddWindowsServiceLifetime(builder.Services);
+
+            Assert.Single(builder.Services, serviceDescriptor =>
+                serviceDescriptor.ServiceType == typeof(IHostLifetime) &&
+                serviceDescriptor.ImplementationType == typeof(WindowsServiceLifetime));
+        }
+
+        [Fact]
+        public void ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationNameInsideOfService()
+        {
+            string appName = Guid.NewGuid().ToString();
+
+            var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings
             {
-                var lifetime = host.Services.GetRequiredService<IHostLifetime>();
-                Assert.IsType<ConsoleLifetime>(lifetime);
-            }
+                ApplicationName = appName,
+            }); 
+
+            // Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
+            AddWindowsServiceLifetime(builder.Services);
+            // No reason to write event logs in this test.
+            builder.Logging.ClearProviders();
+
+            using IHost host = builder.Build();
+
+            var eventLogSettings = host.Services.GetRequiredService<IOptions<EventLogSettings>>().Value;
+            Assert.Same(appName, eventLogSettings.SourceName);
+        }
+
+        [Fact]
+        public void ServiceCollectionExtensionMethodCanBeCalledOnDefaultConfiguration()
+        {
+            var builder = new HostApplicationBuilder(); 
+
+            // Emulate calling builder.Services.AddWindowsService() from inside a Windows service.
+            AddWindowsServiceLifetime(builder.Services);
+            // No reason to write event logs in this test.
+            builder.Logging.ClearProviders();
+
+            using IHost host = builder.Build();
+
+            var lifetime = host.Services.GetRequiredService<IHostLifetime>();
+            Assert.IsType<WindowsServiceLifetime>(lifetime);
+        }
+
+        private void AddWindowsServiceLifetime(IServiceCollection services, Action<WindowsServiceLifetimeOptions> configure = null)
+        {
+            _addWindowsServiceLifetimeMethod ??= typeof(WindowsServiceLifetimeHostBuilderExtensions).GetMethod("AddWindowsServiceLifetime",
+                BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(IServiceCollection), typeof(Action<WindowsServiceLifetimeOptions>) }, null)
+                ?? throw new MissingMethodException();
+
+            configure ??= _ => { };
+            _addWindowsServiceLifetimeMethod.Invoke(null, new object[] { services, configure });
         }
     }
 }
index 4f68146..9d6aa52 100644 (file)
@@ -197,10 +197,22 @@ namespace Microsoft.Extensions.Hosting
 
         internal static void ApplyDefaultHostConfiguration(IConfigurationBuilder hostConfigBuilder, string[]? args)
         {
-            hostConfigBuilder.AddInMemoryCollection(new[]
+            // If we're running anywhere other than C:\Windows\system32, we default to using the CWD for the ContentRoot.
+            // However, since many things like Windows services and MSIX installers have C:\Windows\system32 as there CWD which is not likely
+            // to really be the home for things like appsettings.json, we skip changing the ContentRoot in that case. The non-"default" initial
+            // value for ContentRoot is AppContext.BaseDirectory (e.g. the executable path) which probably makes more sense than the system32.
+
+            // In my testing, both Environment.CurrentDirectory and Environment.GetFolderPath(Environment.SpecialFolder.System) return the path without
+            // any trailing directory separator characters. I'm not even sure the casing can ever be different from these APIs, but I think it makes sense to
+            // ignore case for Windows path comparisons given the file system is usually (always?) going to be case insensitive for the system path.
+            string cwd = Environment.CurrentDirectory;
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !string.Equals(cwd, Environment.GetFolderPath(Environment.SpecialFolder.System), StringComparison.OrdinalIgnoreCase))
             {
-                new KeyValuePair<string, string?>(HostDefaults.ContentRootKey, Directory.GetCurrentDirectory())
-            });
+                hostConfigBuilder.AddInMemoryCollection(new[]
+                {
+                    new KeyValuePair<string, string?>(HostDefaults.ContentRootKey, cwd),
+                });
+            }
 
             hostConfigBuilder.AddEnvironmentVariables(prefix: "DOTNET_");
             if (args is { Length: > 0 })
index f0e8662..c4cb6ce 100644 (file)
@@ -9,8 +9,10 @@ using System.Diagnostics.Tracing;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.DotNet.RemoteExecutor;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration.UserSecrets;
 using Microsoft.Extensions.DependencyInjection;
@@ -46,6 +48,36 @@ namespace Microsoft.Extensions.Hosting.Tests
             Assert.Equal(expected, env.ContentRootPath);
         }
 
+        public static bool IsWindowsAndRemotExecutorIsSupported => PlatformDetection.IsWindows && RemoteExecutor.IsSupported;
+
+        [ConditionalFact(typeof(HostTests), nameof(IsWindowsAndRemotExecutorIsSupported))]
+        public void CreateDefaultBuilder_DoesNotChangeContentRootIfCurrentDirectoryIsWindowsSystemDirectory()
+        {
+            using var _ = RemoteExecutor.Invoke(() =>
+            {
+                string systemDirectory = Environment.GetFolderPath(Environment.SpecialFolder.System);
+                if (string.IsNullOrEmpty(systemDirectory))
+                {
+                    // Skip the environments (like Nano Server) where Environment.SpecialFolder.System returns empty - https://github.com/dotnet/runtime/issues/21430
+                    return;
+                }
+
+                // Test that the path gets normalized before comparison. Use C:\WINDOWS\SYSTEM32\ instead of C:\Windows\system32.
+                systemDirectory = systemDirectory.ToUpper() + "\\";
+
+                Environment.CurrentDirectory = systemDirectory;
+
+                IHostBuilder builder = Host.CreateDefaultBuilder();
+                using IHost host = builder.Build();
+
+                var config = host.Services.GetRequiredService<IConfiguration>();
+                var env = host.Services.GetRequiredService<IHostEnvironment>();
+
+                Assert.Null(config[HostDefaults.ContentRootKey]);
+                Assert.Equal(AppContext.BaseDirectory, env.ContentRootPath);
+            });
+        }
+
         [Fact]
         public void CreateDefaultBuilder_IncludesCommandLineArguments()
         {