From 74e3641f9ad3a5054c79ceca2b058a3004899007 Mon Sep 17 00:00:00 2001 From: CybCorv Date: Fri, 13 Mar 2026 15:15:31 +0100 Subject: [PATCH 01/15] Fix UseSystemd() silently failing with ProtectProc=invisible GetIsSystemdService() used to read /proc/{ppid}/comm to verify the parent process is named "systemd". This file is hidden when the service runs with ProtectProc=invisible (recommended by systemd.exec(5)), causing the check to silently return false. As a result, no IHostLifetime is registered and READY=1 is never sent, leading to a systemd start timeout. Fix this by using the SYSTEMD_EXEC_PID environment variable (systemd v248+), which is set to the PID of the main service process. Comparing it to the current PID is reliable and unaffected by ProtectProc=invisible. The /proc fallback is kept for compatibility with older systemd versions (e.g. Ubuntu 20.04 / systemd 245, Debian 10 / systemd 241). Partially addresses #88660 See #125368 --- .../src/SystemdHelpers.cs | 26 +++++- ...ft.Extensions.Hosting.Systemd.Tests.csproj | 3 +- .../tests/SystemdHelpersTests.cs | 92 +++++++++++++++++++ 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 0094144b740400..b2e45d0f3e212b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -34,15 +34,32 @@ private static bool GetIsSystemdService() return false; } - // To support containerized systemd services, check if we're the main process (PID 1) - // and if there are systemd environment variables defined for notifying the service - // manager, or passing listen handles. #if NET int processId = Environment.ProcessId; #else int processId = Process.GetCurrentProcess().Id; #endif + // Preferred detection method: SYSTEMD_EXEC_PID is set by systemd (v248+) to the PID + // of the main service process. Comparing it to the current PID lets us reliably + // determine whether we are the direct service process, without reading /proc. + // This also correctly handles ProtectProc=invisible, which hides /proc entries of + // other users' processes and would cause the fallback below to silently fail. + string? systemdExecPid = Environment.GetEnvironmentVariable("SYSTEMD_EXEC_PID"); + if (!string.IsNullOrEmpty(systemdExecPid)) + { + if (int.TryParse(systemdExecPid, NumberStyles.None, CultureInfo.InvariantCulture, out int execPid)) + { + return execPid == processId; + } + // Malformed value: don't trust it, fall through to legacy detection. + } + + // Legacy detection for systemd < 248 (e.g. Ubuntu 20.04, Debian 9/10). + + // To support containerized systemd services, check if we're the main process (PID 1) + // and if there are systemd environment variables defined for notifying the service + // manager, or passing listen handles. if (processId == 1) { return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")) || @@ -52,6 +69,9 @@ private static bool GetIsSystemdService() try { // Check whether our direct parent is 'systemd'. + // Note: this silently fails (returns false) when ProtectProc=invisible is set, + // as /proc/{ppid}/comm becomes inaccessible. This is the bug fixed above for + // systemd >= 248 via SYSTEMD_EXEC_PID. int parentPid = Interop.libc.GetParentPid(); string ppidString = parentPid.ToString(NumberFormatInfo.InvariantInfo); byte[] comm = File.ReadAllBytes("/proc/" + ppidString + "/comm"); diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj index 8ed239e3381135..8e342d4840c8e7 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj @@ -3,10 +3,11 @@ $(NetCoreAppCurrent) true + true - + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs new file mode 100644 index 00000000000000..37d74c410f33ad --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs @@ -0,0 +1,92 @@ +// 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.Globalization; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.Hosting.Systemd; +using Xunit; + +namespace Microsoft.Extensions.Hosting +{ + public class SystemdHelpersTests + { + public static bool IsRemoteExecutorSupportedOnLinux => PlatformDetection.IsLinux && RemoteExecutor.IsSupported; + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceReturnsTrueWhenSystemdExecPidMatchesCurrentProcessId() + { + using var _ = RemoteExecutor.Invoke(static () => + { + string processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", processId); + + Assert.True(SystemdHelpers.IsSystemdService()); + }); + } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceReturnsFalseWhenSystemdExecPidDoesNotMatchCurrentProcessId() + { + using var _ = RemoteExecutor.Invoke(static () => + { + string processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); + int nonMatchingPid = int.MaxValue; // No real process will ever have this PID. + + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", nonMatchingPid.ToString(CultureInfo.InvariantCulture)); + + Assert.False(SystemdHelpers.IsSystemdService()); + }); + } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceReturnsFalseWhenSystemdExecPidIsAbsent() + { + using var _ = RemoteExecutor.Invoke(static () => + { + // When SYSTEMD_EXEC_PID is absent the code skips the v248+ path entirely + // and falls through to the legacy detection. Outside a real systemd session + // (not PID 1, parent is not systemd), the result must be false. + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + Environment.SetEnvironmentVariable("LISTEN_PID", null); + + Assert.False(SystemdHelpers.IsSystemdService()); + }); + } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceReturnsFalseWhenSystemdExecPidIsMalformed() + { + using var _ = RemoteExecutor.Invoke(static () => + { + // A malformed SYSTEMD_EXEC_PID must not be trusted: the logic falls through + // to the legacy /proc-based detection, which won't match outside a real systemd session. + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", "not-a-pid"); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + Environment.SetEnvironmentVariable("LISTEN_PID", null); + + Assert.False(SystemdHelpers.IsSystemdService()); + }); + } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceCachesFirstEvaluation() + { + using var _ = RemoteExecutor.Invoke(static () => + { + string processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); + int nonMatchingPid = int.MaxValue; // No real process will ever have this PID. + + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", processId); + bool firstEvaluation = SystemdHelpers.IsSystemdService(); + + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", nonMatchingPid.ToString(CultureInfo.InvariantCulture)); + bool secondEvaluation = SystemdHelpers.IsSystemdService(); + + Assert.True(firstEvaluation); + Assert.True(secondEvaluation); + }); + } + } +} From db71c719a34efe432abcd58b4a08b4bff16bc9aa Mon Sep 17 00:00:00 2001 From: CybCorv <118264404+CybCorv@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:45:25 +0100 Subject: [PATCH 02/15] Remove unused variable in Test Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../tests/SystemdHelpersTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs index 37d74c410f33ad..2f0d8c691b2c2d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs @@ -30,7 +30,6 @@ public void IsSystemdServiceReturnsFalseWhenSystemdExecPidDoesNotMatchCurrentPro { using var _ = RemoteExecutor.Invoke(static () => { - string processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); int nonMatchingPid = int.MaxValue; // No real process will ever have this PID. Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", nonMatchingPid.ToString(CultureInfo.InvariantCulture)); From 8d3278ff0921e60e7aaceea8daf417ee594ae96b Mon Sep 17 00:00:00 2001 From: CybCorv Date: Tue, 17 Mar 2026 15:37:43 +0100 Subject: [PATCH 03/15] Add SYSTEMD_EXEC_PID constant and refactor comments in SystemdHelpers --- .../src/SystemdHelpers.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index b2e45d0f3e212b..7e02d5c3419db0 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -18,6 +18,12 @@ public static class SystemdHelpers { private static readonly bool _isSystemdService = GetIsSystemdService(); + // Environment variable set by systemd (v248+) to the PID of the main service process. + // This is the most reliable way to detect if we're running as a systemd service, as it doesn't + // require reading /proc and works even when ProtectProc=invisible is set. + // See https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#%24SYSTEMD_EXEC_PID for details. + private const string SYSTEMD_EXEC_PID = "SYSTEMD_EXEC_PID"; + /// /// Checks if the current process is hosted as a systemd Service. /// @@ -40,12 +46,9 @@ private static bool GetIsSystemdService() int processId = Process.GetCurrentProcess().Id; #endif - // Preferred detection method: SYSTEMD_EXEC_PID is set by systemd (v248+) to the PID - // of the main service process. Comparing it to the current PID lets us reliably - // determine whether we are the direct service process, without reading /proc. - // This also correctly handles ProtectProc=invisible, which hides /proc entries of - // other users' processes and would cause the fallback below to silently fail. - string? systemdExecPid = Environment.GetEnvironmentVariable("SYSTEMD_EXEC_PID"); + // Preferred detection method: compare SYSTEMD_EXEC_PID to the current PID. + // Works even when ProtectProc=invisible hides /proc entries. + string? systemdExecPid = Environment.GetEnvironmentVariable(SYSTEMD_EXEC_PID); if (!string.IsNullOrEmpty(systemdExecPid)) { if (int.TryParse(systemdExecPid, NumberStyles.None, CultureInfo.InvariantCulture, out int execPid)) @@ -55,23 +58,20 @@ private static bool GetIsSystemdService() // Malformed value: don't trust it, fall through to legacy detection. } - // Legacy detection for systemd < 248 (e.g. Ubuntu 20.04, Debian 9/10). - - // To support containerized systemd services, check if we're the main process (PID 1) - // and if there are systemd environment variables defined for notifying the service - // manager, or passing listen handles. + // To support containerized systemd services (e.g. Podman with --sdnotify=container), + // check if we're the main process (PID 1) and if there are systemd environment + // variables defined for notifying the service manager, or passing listen handles. if (processId == 1) { return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LISTEN_PID")); } + // Legacy detection for systemd < 248 (e.g. Ubuntu 20.04, Debian 11). + // Note: silently returns false when ProtectProc=invisible is set. try { // Check whether our direct parent is 'systemd'. - // Note: this silently fails (returns false) when ProtectProc=invisible is set, - // as /proc/{ppid}/comm becomes inaccessible. This is the bug fixed above for - // systemd >= 248 via SYSTEMD_EXEC_PID. int parentPid = Interop.libc.GetParentPid(); string ppidString = parentPid.ToString(NumberFormatInfo.InvariantInfo); byte[] comm = File.ReadAllBytes("/proc/" + ppidString + "/comm"); From 9e3f6c79f0d4987efbdb083dba6b14124ac09d45 Mon Sep 17 00:00:00 2001 From: CybCorv Date: Tue, 24 Mar 2026 15:47:23 +0100 Subject: [PATCH 04/15] Fall through to legacy detection on SYSTEMD_EXEC_PID mismatch Update test accordingly. --- .../src/SystemdHelpers.cs | 6 +++++- .../tests/SystemdHelpersTests.cs | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 7e02d5c3419db0..c1c6af745c1f67 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -53,7 +53,11 @@ private static bool GetIsSystemdService() { if (int.TryParse(systemdExecPid, NumberStyles.None, CultureInfo.InvariantCulture, out int execPid)) { - return execPid == processId; + if (execPid == processId) + { + return true; + } + // Mismatch: fall through to PID 1 / legacy checks } // Malformed value: don't trust it, fall through to legacy detection. } diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs index 2f0d8c691b2c2d..2338e2534d4f56 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs @@ -26,13 +26,18 @@ public void IsSystemdServiceReturnsTrueWhenSystemdExecPidMatchesCurrentProcessId } [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void IsSystemdServiceReturnsFalseWhenSystemdExecPidDoesNotMatchCurrentProcessId() + public void IsSystemdServiceFallsBackToLegacyDetectionWhenSystemdExecPidDoesNotMatch() { using var _ = RemoteExecutor.Invoke(static () => { + // When SYSTEMD_EXEC_PID is set but doesn't match the current PID, the code + // falls through to legacy detection instead of returning false immediately. + // Outside a real systemd session (not PID 1, parent is not named "systemd"), + // the legacy path returns false — but the important guarantee is that it runs. int nonMatchingPid = int.MaxValue; // No real process will ever have this PID. - Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", nonMatchingPid.ToString(CultureInfo.InvariantCulture)); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + Environment.SetEnvironmentVariable("LISTEN_PID", null); Assert.False(SystemdHelpers.IsSystemdService()); }); From a0c7769a547e69c75285f49709ce4af6905b2143 Mon Sep 17 00:00:00 2001 From: CybCorv Date: Wed, 25 Mar 2026 12:04:35 +0100 Subject: [PATCH 05/15] Decouple sd_notify and systemd log formatter detection UseSystemd() and AddSystemd() now register the notifier based on NOTIFY_SOCKET alone, and the log formatter based on IsSystemdService(). Unset NOTIFY_SOCKET after capture to prevent child process inheritance. Centralize environment variable names in SystemdConstants. --- .../src/SystemdConstants.cs | 26 +++ .../src/SystemdHelpers.cs | 33 ++- .../src/SystemdHostBuilderExtensions.cs | 21 +- .../src/SystemdNotifier.cs | 6 +- .../tests/UseSystemdTests.cs | 200 ++++++++++++++++-- 5 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs new file mode 100644 index 00000000000000..cfd498f0797cd5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static class SystemdConstants +{ + /// + /// Environment variable set by systemd (v229+) to the path of the notify socket for the service. If this variable is set, the service should send status notifications to systemd using this socket. + /// + /// + internal const string NotifySocket = "NOTIFY_SOCKET"; + + /// + /// Environment variable set by systemd (v248+) to the PID of the main service process. + /// This is the most reliable way to detect if we're running as a systemd service, as it doesn't + /// require reading /proc and works even when ProtectProc=invisible is set. + /// + /// + internal const string SystemdExecPid = "SYSTEMD_EXEC_PID"; + + /// + /// Environment variable set by systemd for socket activation, indicating the PID + /// that should receive the listen file descriptors. + /// + /// + internal const string ListenPid = "LISTEN_PID"; +} diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index c1c6af745c1f67..c3a3dd7f526aaf 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -18,12 +18,6 @@ public static class SystemdHelpers { private static readonly bool _isSystemdService = GetIsSystemdService(); - // Environment variable set by systemd (v248+) to the PID of the main service process. - // This is the most reliable way to detect if we're running as a systemd service, as it doesn't - // require reading /proc and works even when ProtectProc=invisible is set. - // See https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#%24SYSTEMD_EXEC_PID for details. - private const string SYSTEMD_EXEC_PID = "SYSTEMD_EXEC_PID"; - /// /// Checks if the current process is hosted as a systemd Service. /// @@ -48,7 +42,7 @@ private static bool GetIsSystemdService() // Preferred detection method: compare SYSTEMD_EXEC_PID to the current PID. // Works even when ProtectProc=invisible hides /proc entries. - string? systemdExecPid = Environment.GetEnvironmentVariable(SYSTEMD_EXEC_PID); + string? systemdExecPid = Environment.GetEnvironmentVariable(SystemdConstants.SystemdExecPid); if (!string.IsNullOrEmpty(systemdExecPid)) { if (int.TryParse(systemdExecPid, NumberStyles.None, CultureInfo.InvariantCulture, out int execPid)) @@ -67,8 +61,8 @@ private static bool GetIsSystemdService() // variables defined for notifying the service manager, or passing listen handles. if (processId == 1) { - return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")) || - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LISTEN_PID")); + return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(SystemdConstants.NotifySocket)) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(SystemdConstants.ListenPid)); } // Legacy detection for systemd < 248 (e.g. Ubuntu 20.04, Debian 11). @@ -87,5 +81,26 @@ private static bool GetIsSystemdService() return false; } + + private static readonly bool _isSystemdNotify = GetIsSystemdNotify(); + /// + /// Checks if the current process has systemd notify enabled. + /// + /// + /// if the current process has systemd notify enabled; otherwise, . + /// + internal static bool IsSystemdNotify() => _isSystemdNotify; + + private static bool GetIsSystemdNotify() + { + // No point in testing anything unless it's Unix + if (Environment.OSVersion.Platform != PlatformID.Unix) + { + return false; + } + // Checks whether NOTIFY_SOCKET is set, indicating the service manager expects sd_notify notifications. + string? socketPath = Environment.GetEnvironmentVariable(SystemdConstants.NotifySocket); + return !string.IsNullOrEmpty(socketPath); + } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs index 814160886b3c63..cbe315378b3a69 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs @@ -35,6 +35,14 @@ public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder) ArgumentNullException.ThrowIfNull(hostBuilder); if (SystemdHelpers.IsSystemdService()) + { + hostBuilder.ConfigureServices((hostContext, services) => + { + AddSystemdLogger(services); + }); + } + + if (SystemdHelpers.IsSystemdNotify()) { hostBuilder.ConfigureServices((hostContext, services) => { @@ -70,20 +78,29 @@ public static IServiceCollection AddSystemd(this IServiceCollection services) ArgumentNullException.ThrowIfNull(services); if (SystemdHelpers.IsSystemdService()) + { + AddSystemdLogger(services); + } + + if (SystemdHelpers.IsSystemdNotify()) { AddSystemdLifetime(services); } + return services; } - private static void AddSystemdLifetime(IServiceCollection services) + private static void AddSystemdLogger(IServiceCollection services) { services.Configure(options => { options.FormatterName = ConsoleFormatterNames.Systemd; }); + } - // IsSystemdService() will never return true for android/browser/iOS/tvOS + private static void AddSystemdLifetime(IServiceCollection services) + { + // IsSystemdNotify() will never return true for android/browser/iOS/tvOS #pragma warning disable CA1416 // Validate platform compatibility services.AddSingleton(); services.AddSingleton(); diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs index 791eda2a8413af..0b3de4489fe214 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs @@ -13,8 +13,6 @@ namespace Microsoft.Extensions.Hosting.Systemd [UnsupportedOSPlatform("browser")] public class SystemdNotifier : ISystemdNotifier { - private const string NOTIFY_SOCKET = "NOTIFY_SOCKET"; - private readonly string? _socketPath; /// @@ -60,12 +58,14 @@ public void Notify(ServiceState state) private static string? GetNotifySocketPath() { - string? socketPath = Environment.GetEnvironmentVariable(NOTIFY_SOCKET); + string? socketPath = Environment.GetEnvironmentVariable(SystemdConstants.NotifySocket); if (string.IsNullOrEmpty(socketPath)) { return null; } + // Don't let child processes inherit the socket path, as that would cause them to interfere with the service manager notifications. + Environment.SetEnvironmentVariable(SystemdConstants.NotifySocket, null); // Support abstract socket paths. if (socketPath[0] == '@') diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs index 948f9b4b527f16..045f6d7eb42a94 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs @@ -1,41 +1,217 @@ // 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.Globalization; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting.Systemd; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.Extensions.Hosting { public class UseSystemdTests { - [Fact] - public void DefaultsToOffOutsideOfService() + public static bool IsRemoteExecutorSupportedOnLinux => PlatformDetection.IsLinux && RemoteExecutor.IsSupported; + + private static IHost BuildHostWithAddSystemd() + { + var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings + { + // Disable defaults that may not be supported on the testing platform like EventLogLoggerProvider. + DisableDefaults = true, + }); + builder.Services.AddSystemd(); + return builder.Build(); + } + + private static IHost BuildHostWithUseSystemd() { - using IHost host = new HostBuilder() + return new HostBuilder() .UseSystemd() .Build(); + } + [Fact] + public void UseSystemd_DefaultsToOffOutsideOfService() + { + using IHost host = BuildHostWithUseSystemd(); var lifetime = host.Services.GetRequiredService(); Assert.NotNull(lifetime); Assert.IsNotType(lifetime); } [Fact] - public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService() + public void AddSystemd_DefaultsToOffOutsideOfService() { - var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings + using IHost host = BuildHostWithAddSystemd(); + var lifetime = host.Services.GetRequiredService(); + Assert.NotNull(lifetime); + Assert.IsNotType(lifetime); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void AddSystemd_SystemdLifetimeIsRegisteredWhenNotifySocketIsSet() + { + using var _ = RemoteExecutor.Invoke(static () => { - // Disable defaults that may not be supported on the testing platform like EventLogLoggerProvider. - DisableDefaults = true, + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + + using IHost host = BuildHostWithAddSystemd(); + + Assert.Null(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")); // Verify that the socket path is cleared from the environment after reading, to avoid inheritance by child processes. + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); }); + } - builder.Services.AddSystemd(); - using IHost host = builder.Build(); + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void UseSystemd_SystemdLifetimeIsRegisteredWhenNotifySocketIsSet() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); - var lifetime = host.Services.GetRequiredService(); - Assert.NotNull(lifetime); - Assert.IsNotType(lifetime); + using IHost host = BuildHostWithUseSystemd(); + + Assert.Null(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void AddSystemd_SystemdLifetimeIsNotRegisteredWhenNotifySocketIsAbsent() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + + using IHost host = BuildHostWithAddSystemd(); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsNotType(lifetime); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void UseSystemd_SystemdLifetimeIsNotRegisteredWhenNotifySocketIsAbsent() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + + using IHost host = BuildHostWithUseSystemd(); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsNotType(lifetime); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void AddSystemd_SystemdLoggerIsConfiguredWhenIsSystemdServiceIsTrue() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", + Environment.ProcessId.ToString(CultureInfo.InvariantCulture)); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + + using IHost host = BuildHostWithAddSystemd(); + var options = host.Services.GetRequiredService>().Value; + Assert.Equal(ConsoleFormatterNames.Systemd, options.FormatterName); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void UseSystemd_SystemdLoggerIsConfiguredWhenIsSystemdServiceIsTrue() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", + Environment.ProcessId.ToString(CultureInfo.InvariantCulture)); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + + using IHost host = BuildHostWithUseSystemd(); + var options = host.Services.GetRequiredService>().Value; + Assert.Equal(ConsoleFormatterNames.Systemd, options.FormatterName); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void AddSystemd_SystemdLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + + using IHost host = BuildHostWithAddSystemd(); + + var options = host.Services.GetRequiredService>().Value; + Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void UseSystemd_SystemdLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + + using IHost host = BuildHostWithUseSystemd(); + + var options = host.Services.GetRequiredService>().Value; + Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void AddSystemd_SystemdLoggerAndLifetimeAreBothRegisteredWhenBothConditionsAreMet() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", + Environment.ProcessId.ToString(CultureInfo.InvariantCulture)); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + + using IHost host = BuildHostWithAddSystemd(); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); + + var options = host.Services.GetRequiredService>().Value; + Assert.Equal(ConsoleFormatterNames.Systemd, options.FormatterName); + }); + } + + [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void UseSystemd_SystemdLoggerAndLifetimeAreBothRegisteredWhenBothConditionsAreMet() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", + Environment.ProcessId.ToString(CultureInfo.InvariantCulture)); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + + using IHost host = BuildHostWithUseSystemd(); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); + + var options = host.Services.GetRequiredService>().Value; + Assert.Equal(ConsoleFormatterNames.Systemd, options.FormatterName); + }); } } } From 452bdc3b19809244021fe2adf08671c72a56239f Mon Sep 17 00:00:00 2001 From: CybCorv Date: Wed, 25 Mar 2026 12:28:13 +0100 Subject: [PATCH 06/15] Address Copilot review feedback Rename test to accurately describe what is verified. Add namespace to SystemdConstants. --- .../src/SystemdConstants.cs | 41 ++++++++++--------- .../tests/SystemdHelpersTests.cs | 9 ++-- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs index cfd498f0797cd5..b85916996430e4 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs @@ -1,26 +1,29 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -internal static class SystemdConstants +namespace Microsoft.Extensions.Hosting.Systemd { - /// - /// Environment variable set by systemd (v229+) to the path of the notify socket for the service. If this variable is set, the service should send status notifications to systemd using this socket. - /// - /// - internal const string NotifySocket = "NOTIFY_SOCKET"; + internal static class SystemdConstants + { + /// + /// Environment variable set by systemd (v229+) to the path of the notify socket for the service. If this variable is set, the service should send status notifications to systemd using this socket. + /// + /// + internal const string NotifySocket = "NOTIFY_SOCKET"; - /// - /// Environment variable set by systemd (v248+) to the PID of the main service process. - /// This is the most reliable way to detect if we're running as a systemd service, as it doesn't - /// require reading /proc and works even when ProtectProc=invisible is set. - /// - /// - internal const string SystemdExecPid = "SYSTEMD_EXEC_PID"; + /// + /// Environment variable set by systemd (v248+) to the PID of the main service process. + /// This is the most reliable way to detect if we're running as a systemd service, as it doesn't + /// require reading /proc and works even when ProtectProc=invisible is set. + /// + /// + internal const string SystemdExecPid = "SYSTEMD_EXEC_PID"; - /// - /// Environment variable set by systemd for socket activation, indicating the PID - /// that should receive the listen file descriptors. - /// - /// - internal const string ListenPid = "LISTEN_PID"; + /// + /// Environment variable set by systemd for socket activation, indicating the PID + /// that should receive the listen file descriptors. + /// + /// + internal const string ListenPid = "LISTEN_PID"; + } } diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs index 2338e2534d4f56..b10954e01d389d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs @@ -26,14 +26,15 @@ public void IsSystemdServiceReturnsTrueWhenSystemdExecPidMatchesCurrentProcessId } [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void IsSystemdServiceFallsBackToLegacyDetectionWhenSystemdExecPidDoesNotMatch() + public void IsSystemdServiceReturnsFalseWhenSystemdExecPidDoesNotMatchOutsideSystemdSession() { using var _ = RemoteExecutor.Invoke(static () => { // When SYSTEMD_EXEC_PID is set but doesn't match the current PID, the code - // falls through to legacy detection instead of returning false immediately. - // Outside a real systemd session (not PID 1, parent is not named "systemd"), - // the legacy path returns false — but the important guarantee is that it runs. + // falls through to legacy detection. Outside a real systemd session + // (not PID 1, parent is not named "systemd"), the legacy path returns false. + // Note: the fall-through itself cannot be directly observed in a unit test + // without being PID 1 or having a parent named "systemd". int nonMatchingPid = int.MaxValue; // No real process will ever have this PID. Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", nonMatchingPid.ToString(CultureInfo.InvariantCulture)); Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); From d6d72692059a953ec1c36a4a6f674e583101e0d4 Mon Sep 17 00:00:00 2001 From: CybCorv Date: Thu, 2 Apr 2026 14:39:48 +0200 Subject: [PATCH 07/15] Refactor tests: consolidate assertions and align with new detection logic Update UseSystemdTests to reflect the decoupled logger/lifetime behavior: assert both lifetime and logger in each scenario, add systemd service type context comments, and remove redundant test cases. Add SystemdHelpersTests for cache negative evaluation, child process NOTIFY_SOCKET inheritance, and empty NOTIFY_SOCKET edge case. --- .../tests/SystemdHelpersTests.cs | 51 ++++++++++ .../tests/UseSystemdTests.cs | 96 ++++++++++--------- 2 files changed, 100 insertions(+), 47 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs index b10954e01d389d..9d679df8f81469 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs @@ -93,5 +93,56 @@ public void IsSystemdServiceCachesFirstEvaluation() Assert.True(secondEvaluation); }); } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceCachesFirstNegativeEvaluation() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); + Environment.SetEnvironmentVariable("LISTEN_PID", null); + + var firstEvaluation = SystemdHelpers.IsSystemdService(); + + string processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", processId); + + var secondEvaluation = SystemdHelpers.IsSystemdService(); + Assert.False(firstEvaluation); + Assert.False(secondEvaluation); + }); + } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceReturnsFalseWhenSystemdExecPidDoesNotMatchAndNotifySocketIsSet() + { + // Child process scenario: SYSTEMD_EXEC_PID is set but doesn't match the current PID, + // while NOTIFY_SOCKET is inherited from the parent service. + // The mismatch falls through to legacy detection (not PID 1, parent not named "systemd"), + // which returns false. NOTIFY_SOCKET is only considered in the PID 1 container path. + using var _ = RemoteExecutor.Invoke(static () => + { + int nonMatchingPid = int.MaxValue; + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", nonMatchingPid.ToString(CultureInfo.InvariantCulture)); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); + Environment.SetEnvironmentVariable("LISTEN_PID", null); + + Assert.False(SystemdHelpers.IsSystemdService()); + }); + } + + [ConditionalFact(typeof(SystemdHelpersTests), nameof(IsRemoteExecutorSupportedOnLinux))] + public void IsSystemdServiceReturnsFalseWhenNotifySocketIsEmpty() + { + using var _ = RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", ""); + Environment.SetEnvironmentVariable("LISTEN_PID", null); + + Assert.False(SystemdHelpers.IsSystemdService()); + }); + } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs index 045f6d7eb42a94..853325cde2c28a 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs @@ -35,66 +35,43 @@ private static IHost BuildHostWithUseSystemd() } [Fact] - public void UseSystemd_DefaultsToOffOutsideOfService() + public void AddSystemd_DefaultsToOffOutsideOfService() { - using IHost host = BuildHostWithUseSystemd(); + // Simulate running outside of a systemd service + // SystemdLifetime and SystemdLogger should not be registered, even if UseSystemd is called. + using IHost host = BuildHostWithAddSystemd(); var lifetime = host.Services.GetRequiredService(); Assert.NotNull(lifetime); Assert.IsNotType(lifetime); + var options = host.Services.GetRequiredService>().Value; + Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); } [Fact] - public void AddSystemd_DefaultsToOffOutsideOfService() + public void UseSystemd_DefaultsToOffOutsideOfService() { - using IHost host = BuildHostWithAddSystemd(); + // Simulate running outside of a systemd service + // SystemdLifetime and SystemdLogger should not be registered, even if UseSystemd is called. + using IHost host = BuildHostWithUseSystemd(); var lifetime = host.Services.GetRequiredService(); Assert.NotNull(lifetime); Assert.IsNotType(lifetime); + var options = host.Services.GetRequiredService>().Value; + Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); } [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void AddSystemd_SystemdLifetimeIsRegisteredWhenNotifySocketIsSet() + public void AddSystemd_SystemdLoggerIsNotConfiguredAndSystemdLifetimeIsNotRegisteredWhenIsSystemdServiceIsFalse() { + // Simulate running outside of a systemd service using var _ = RemoteExecutor.Invoke(static () => { - Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); - - using IHost host = BuildHostWithAddSystemd(); - - Assert.Null(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")); // Verify that the socket path is cleared from the environment after reading, to avoid inheritance by child processes. - - var lifetime = host.Services.GetRequiredService(); - Assert.IsType(lifetime); - }); - } - - [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void UseSystemd_SystemdLifetimeIsRegisteredWhenNotifySocketIsSet() - { - using var _ = RemoteExecutor.Invoke(static () => - { - Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); - Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); - - using IHost host = BuildHostWithUseSystemd(); - - Assert.Null(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")); - - var lifetime = host.Services.GetRequiredService(); - Assert.IsType(lifetime); - }); - } - - [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void AddSystemd_SystemdLifetimeIsNotRegisteredWhenNotifySocketIsAbsent() - { - using var _ = RemoteExecutor.Invoke(static () => - { Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); - Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); using IHost host = BuildHostWithAddSystemd(); + var options = host.Services.GetRequiredService>().Value; + Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); var lifetime = host.Services.GetRequiredService(); Assert.IsNotType(lifetime); @@ -102,14 +79,17 @@ public void AddSystemd_SystemdLifetimeIsNotRegisteredWhenNotifySocketIsAbsent() } [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void UseSystemd_SystemdLifetimeIsNotRegisteredWhenNotifySocketIsAbsent() + public void UseSystemd_SystemdLoggerIsNotConfiguredAndSystemdLifetimeIsNotRegisteredWhenIsSystemdServiceIsFalse() { + // Simulate running outside of a systemd service using var _ = RemoteExecutor.Invoke(static () => { - Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null); using IHost host = BuildHostWithUseSystemd(); + var options = host.Services.GetRequiredService>().Value; + Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); var lifetime = host.Services.GetRequiredService(); Assert.IsNotType(lifetime); @@ -117,8 +97,9 @@ public void UseSystemd_SystemdLifetimeIsNotRegisteredWhenNotifySocketIsAbsent() } [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void AddSystemd_SystemdLoggerIsConfiguredWhenIsSystemdServiceIsTrue() + public void AddSystemd_SystemdLoggerIsConfiguredAndSystemdLifetimeIsRegisteredWhenIsSystemdServiceIsTrue() { + // Simulates: Type=simple with SYSTEMD_EXEC_PID (e.g. systemd >= v248) nominal case. using var _ = RemoteExecutor.Invoke(static () => { Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", @@ -128,12 +109,16 @@ public void AddSystemd_SystemdLoggerIsConfiguredWhenIsSystemdServiceIsTrue() using IHost host = BuildHostWithAddSystemd(); var options = host.Services.GetRequiredService>().Value; Assert.Equal(ConsoleFormatterNames.Systemd, options.FormatterName); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); }); } [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void UseSystemd_SystemdLoggerIsConfiguredWhenIsSystemdServiceIsTrue() + public void UseSystemd_SystemdLoggerIsConfiguredAndSystemdLifetimeIsRegisteredWhenIsSystemdServiceIsTrue() { + // Simulates: Type=simple with SYSTEMD_EXEC_PID (e.g. systemd >= v248) nominal case. using var _ = RemoteExecutor.Invoke(static () => { Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", @@ -143,34 +128,49 @@ public void UseSystemd_SystemdLoggerIsConfiguredWhenIsSystemdServiceIsTrue() using IHost host = BuildHostWithUseSystemd(); var options = host.Services.GetRequiredService>().Value; Assert.Equal(ConsoleFormatterNames.Systemd, options.FormatterName); + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); }); } [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void AddSystemd_SystemdLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() + public void AddSystemd_SystemdLifetimeIsRegisteredAndLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() { + // Simulates: Type=notify without SYSTEMD_EXEC_PID (e.g. containerized, Podman --sdnotify=container, systemd < v248). using var _ = RemoteExecutor.Invoke(static () => { - Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); using IHost host = BuildHostWithAddSystemd(); + Assert.Null(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")); // Cleared to prevent child process inheritance. + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); + var options = host.Services.GetRequiredService>().Value; Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); }); } [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] - public void UseSystemd_SystemdLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() + public void UseSystemd_SystemdLifetimeIsRegisteredAndLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() { + // Simulates: Type=notify without SYSTEMD_EXEC_PID (e.g. containerized, Podman --sdnotify=container, systemd < v248). using var _ = RemoteExecutor.Invoke(static () => { - Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null); + Environment.SetEnvironmentVariable("NOTIFY_SOCKET", "/run/systemd/notify"); using IHost host = BuildHostWithUseSystemd(); + Assert.Null(Environment.GetEnvironmentVariable("NOTIFY_SOCKET")); // Cleared to prevent child process inheritance. + + var lifetime = host.Services.GetRequiredService(); + Assert.IsType(lifetime); + var options = host.Services.GetRequiredService>().Value; Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); }); @@ -179,6 +179,7 @@ public void UseSystemd_SystemdLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet() [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] public void AddSystemd_SystemdLoggerAndLifetimeAreBothRegisteredWhenBothConditionsAreMet() { + // Simulates: Type=notify with SYSTEMD_EXEC_PID (systemd >= v248). using var _ = RemoteExecutor.Invoke(static () => { Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", @@ -198,6 +199,7 @@ public void AddSystemd_SystemdLoggerAndLifetimeAreBothRegisteredWhenBothConditio [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] public void UseSystemd_SystemdLoggerAndLifetimeAreBothRegisteredWhenBothConditionsAreMet() { + // Simulates: Type=notify with SYSTEMD_EXEC_PID (systemd >= v248). using var _ = RemoteExecutor.Invoke(static () => { Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", From 381df75abfd0b168837b8083472dc6ccc962e9b6 Mon Sep 17 00:00:00 2001 From: CybCorv Date: Thu, 2 Apr 2026 14:44:19 +0200 Subject: [PATCH 08/15] Address tmds review feedback Introduce internal IsSystemdLogger() and IsSystemdLifetime() helper methods to clarify intent and prepare for future JOURNAL_STREAM detection. Make IsSystemdNotify() private. Use these methods in UseSystemd and AddSystemd. Update GetNotifySocketPath comment to explain NOTIFY_SOCKET unset timing. --- .../src/SystemdHelpers.cs | 18 ++++++++++++++---- .../src/SystemdHostBuilderExtensions.cs | 8 ++++---- .../src/SystemdNotifier.cs | 5 ++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 97b1f849ca2a83..448723952bc886 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -79,13 +79,11 @@ private static bool GetIsSystemdService() } private static readonly bool _isSystemdNotify = GetIsSystemdNotify(); + /// /// Checks if the current process has systemd notify enabled. /// - /// - /// if the current process has systemd notify enabled; otherwise, . - /// - internal static bool IsSystemdNotify() => _isSystemdNotify; + private static bool IsSystemdNotify() => _isSystemdNotify; private static bool GetIsSystemdNotify() { @@ -98,5 +96,17 @@ private static bool GetIsSystemdNotify() string? socketPath = Environment.GetEnvironmentVariable(SystemdConstants.NotifySocket); return !string.IsNullOrEmpty(socketPath); } + + /// + /// Checks if the systemd journal log formatter should be enabled. + /// + /// Will use $JOURNAL_STREAM + fstat in a follow-up. + internal static bool IsSystemdLogger() => IsSystemdService(); + + /// + /// Checks if and should be registered. + /// + /// is a noop when NOTIFY_SOCKET is absent. + internal static bool IsSystemdLifetime() => IsSystemdService() || IsSystemdNotify(); } } diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs index cbe315378b3a69..100c11e3fcc457 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs @@ -34,7 +34,7 @@ public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder) { ArgumentNullException.ThrowIfNull(hostBuilder); - if (SystemdHelpers.IsSystemdService()) + if (SystemdHelpers.IsSystemdLogger()) { hostBuilder.ConfigureServices((hostContext, services) => { @@ -42,7 +42,7 @@ public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder) }); } - if (SystemdHelpers.IsSystemdNotify()) + if (SystemdHelpers.IsSystemdLifetime()) { hostBuilder.ConfigureServices((hostContext, services) => { @@ -77,12 +77,12 @@ public static IServiceCollection AddSystemd(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); - if (SystemdHelpers.IsSystemdService()) + if (SystemdHelpers.IsSystemdLogger()) { AddSystemdLogger(services); } - if (SystemdHelpers.IsSystemdNotify()) + if (SystemdHelpers.IsSystemdLifetime()) { AddSystemdLifetime(services); } diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs index 0b3de4489fe214..8a183fd04cd0b5 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs @@ -64,7 +64,10 @@ public void Notify(ServiceState state) { return null; } - // Don't let child processes inherit the socket path, as that would cause them to interfere with the service manager notifications. + + // Because this method is called on Notifier construction, the envvar is cleared when the Host is built + // (IHostLifetime depends on ISystemdNotifier). This prevents child processes from inheriting the socket + // and interfering with service manager notifications. Environment.SetEnvironmentVariable(SystemdConstants.NotifySocket, null); // Support abstract socket paths. From 09da18f84eef50de20c52526bb5f582270c2e9ef Mon Sep 17 00:00:00 2001 From: CybCorv Date: Thu, 2 Apr 2026 15:38:25 +0200 Subject: [PATCH 09/15] Address Copilot review feedback Update XML docs to reflect decoupled logger/lifetime detection behavior. Simplify duplicate comment in GetIsSystemdService(). Fix misleading platform compatibility comment in AddSystemdLifetime(). --- .../src/SystemdHelpers.cs | 4 +--- .../src/SystemdHostBuilderExtensions.cs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 448723952bc886..913cff921a2622 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -31,9 +31,7 @@ private static bool GetIsSystemdService() return false; } - // To support containerized systemd services, check if we're the main process (PID 1) - // and if there are systemd environment variables defined for notifying the service - // manager, or passing listen handles. + // Containerized systemd services: PID 1 with systemd notify/listen environment variables. int processId = Environment.ProcessId; // Preferred detection method: compare SYSTEMD_EXEC_PID to the current PID. diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs index 100c11e3fcc457..8eb0bd5092534d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs @@ -21,7 +21,12 @@ public static class SystemdHostBuilderExtensions /// /// /// This is context aware and will only activate if it detects the process is running - /// as a systemd Service. + /// as a systemd Service or if NOTIFY_SOCKET is set. + /// + /// + /// The console log formatter is enabled when the process is detected as a systemd service. + /// The and are registered when + /// NOTIFY_SOCKET is set or the process is detected as a systemd service. /// /// /// The systemd service file must be configured with Type=notify to enable @@ -60,7 +65,12 @@ public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder) /// /// /// This is context aware and will only activate if it detects the process is running - /// as a systemd Service. + /// as a systemd Service or if NOTIFY_SOCKET is set. + /// + /// + /// The console log formatter is enabled when the process is detected as a systemd service. + /// The and are registered when + /// NOTIFY_SOCKET is set or the process is detected as a systemd service. /// /// /// The systemd service file must be configured with Type=notify to enable @@ -100,7 +110,8 @@ private static void AddSystemdLogger(IServiceCollection services) private static void AddSystemdLifetime(IServiceCollection services) { - // IsSystemdNotify() will never return true for android/browser/iOS/tvOS + // SystemdNotifier and SystemdLifetime are Unix-only; IsSystemdLifetime() ensures + // we only reach this code on platforms where systemd can run. #pragma warning disable CA1416 // Validate platform compatibility services.AddSingleton(); services.AddSingleton(); From 7ab282eaa222d2dc0c5e9eacc33496e54f7de632 Mon Sep 17 00:00:00 2001 From: CybCorv <118264404+CybCorv@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:28:44 +0200 Subject: [PATCH 10/15] Update src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/SystemdHostBuilderExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs index 8eb0bd5092534d..a5a3ba60071e4d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs @@ -111,7 +111,9 @@ private static void AddSystemdLogger(IServiceCollection services) private static void AddSystemdLifetime(IServiceCollection services) { // SystemdNotifier and SystemdLifetime are Unix-only; IsSystemdLifetime() ensures - // we only reach this code on platforms where systemd can run. + // we only reach this code when running on Unix and when the environment indicates + // systemd-style integration (for example, when NOTIFY_SOCKET is set or the process + // is detected as a systemd service). #pragma warning disable CA1416 // Validate platform compatibility services.AddSingleton(); services.AddSingleton(); From b9581d42b6a09035ea09f6e40ddf1181d6e28921 Mon Sep 17 00:00:00 2001 From: CybCorv <118264404+CybCorv@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:29:19 +0200 Subject: [PATCH 11/15] Update src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tests/UseSystemdTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs index 853325cde2c28a..a1751891a620ec 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs @@ -38,7 +38,7 @@ private static IHost BuildHostWithUseSystemd() public void AddSystemd_DefaultsToOffOutsideOfService() { // Simulate running outside of a systemd service - // SystemdLifetime and SystemdLogger should not be registered, even if UseSystemd is called. + // SystemdLifetime and SystemdLogger should not be registered, even if AddSystemd() is used. using IHost host = BuildHostWithAddSystemd(); var lifetime = host.Services.GetRequiredService(); Assert.NotNull(lifetime); From ee82b81a1e687dacb2969af83e0542cac6d64a96 Mon Sep 17 00:00:00 2001 From: CybCorv Date: Thu, 2 Apr 2026 17:03:59 +0200 Subject: [PATCH 12/15] Address Copilot review feedbacks Refactor SystemdHelpers and SystemdHostBuilderExtensions documentation; remove environment-dependent tests from UseSystemdTests --- .../src/SystemdHelpers.cs | 5 +--- .../src/SystemdHostBuilderExtensions.cs | 6 ++--- .../tests/UseSystemdTests.cs | 26 ------------------- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 913cff921a2622..1e9d94d8db08db 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -31,7 +31,6 @@ private static bool GetIsSystemdService() return false; } - // Containerized systemd services: PID 1 with systemd notify/listen environment variables. int processId = Environment.ProcessId; // Preferred detection method: compare SYSTEMD_EXEC_PID to the current PID. @@ -50,9 +49,7 @@ private static bool GetIsSystemdService() // Malformed value: don't trust it, fall through to legacy detection. } - // To support containerized systemd services (e.g. Podman with --sdnotify=container), - // check if we're the main process (PID 1) and if there are systemd environment - // variables defined for notifying the service manager, or passing listen handles. + // To support containerized systemd services (e.g. Podman with --sdnotify=container) if (processId == 1) { return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(SystemdConstants.NotifySocket)) || diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs index a5a3ba60071e4d..6e4c43416b32d6 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs @@ -16,7 +16,7 @@ public static class SystemdHostBuilderExtensions /// /// Configures the lifetime to , /// provides notification messages for application started and stopping, - /// and configures console logging to the systemd format. + /// and configures console logging to the systemd format when running as a systemd service. /// /// /// @@ -30,7 +30,7 @@ public static class SystemdHostBuilderExtensions /// /// /// The systemd service file must be configured with Type=notify to enable - /// notifications. See https://www.freedesktop.org/software/systemd/man/systemd.service.html. + /// notifications. See . /// /// /// The to configure. @@ -60,7 +60,7 @@ public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder) /// /// Configures the lifetime of the built from to /// , provides notification messages for application started - /// and stopping, and configures console logging to the systemd format. + /// and stopping, and configures console logging to the systemd format when running as a systemd service. /// /// /// diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs index a1751891a620ec..0cafddd15eecf9 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs @@ -34,32 +34,6 @@ private static IHost BuildHostWithUseSystemd() .Build(); } - [Fact] - public void AddSystemd_DefaultsToOffOutsideOfService() - { - // Simulate running outside of a systemd service - // SystemdLifetime and SystemdLogger should not be registered, even if AddSystemd() is used. - using IHost host = BuildHostWithAddSystemd(); - var lifetime = host.Services.GetRequiredService(); - Assert.NotNull(lifetime); - Assert.IsNotType(lifetime); - var options = host.Services.GetRequiredService>().Value; - Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); - } - - [Fact] - public void UseSystemd_DefaultsToOffOutsideOfService() - { - // Simulate running outside of a systemd service - // SystemdLifetime and SystemdLogger should not be registered, even if UseSystemd is called. - using IHost host = BuildHostWithUseSystemd(); - var lifetime = host.Services.GetRequiredService(); - Assert.NotNull(lifetime); - Assert.IsNotType(lifetime); - var options = host.Services.GetRequiredService>().Value; - Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName); - } - [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))] public void AddSystemd_SystemdLoggerIsNotConfiguredAndSystemdLifetimeIsNotRegisteredWhenIsSystemdServiceIsFalse() { From 5441de8c03bc0c1c43871901770cb434d545208f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Tue, 21 Apr 2026 14:23:44 +0200 Subject: [PATCH 13/15] Update src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs --- .../Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 1e9d94d8db08db..af771e6e31c4c7 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -95,7 +95,7 @@ private static bool GetIsSystemdNotify() /// /// Checks if the systemd journal log formatter should be enabled. /// - /// Will use $JOURNAL_STREAM + fstat in a follow-up. + //TODO: #127218 internal static bool IsSystemdLogger() => IsSystemdService(); /// From 56b6551d262f0b084fa7ca05dee837aa1e19e49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Tue, 21 Apr 2026 15:04:01 +0200 Subject: [PATCH 14/15] Update src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index af771e6e31c4c7..61bfffa0da96c2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -95,7 +95,7 @@ private static bool GetIsSystemdNotify() /// /// Checks if the systemd journal log formatter should be enabled. /// - //TODO: #127218 + // TODO: #127218 internal static bool IsSystemdLogger() => IsSystemdService(); /// From 801e28cedb79671457c890d83334a640e69721ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Tue, 21 Apr 2026 15:04:27 +0200 Subject: [PATCH 15/15] Update src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs index 61bfffa0da96c2..b88670315ad7a2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs @@ -57,7 +57,7 @@ private static bool GetIsSystemdService() } // Legacy detection for systemd < 248 (e.g. Ubuntu 20.04, Debian 11). - // Note: silently returns false when ProtectProc=invisible is set. + // Note: silently returns false if /proc cannot be read, such as when ProtectProc=invisible is set. try { // Check whether our direct parent is 'systemd'.