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..b85916996430e4
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Hosting.Systemd
+{
+ 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 3b8259bd799234..b88670315ad7a2 100644
--- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs
@@ -31,17 +31,33 @@ 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.
int processId = Environment.ProcessId;
+ // Preferred detection method: compare SYSTEMD_EXEC_PID to the current PID.
+ // Works even when ProtectProc=invisible hides /proc entries.
+ string? systemdExecPid = Environment.GetEnvironmentVariable(SystemdConstants.SystemdExecPid);
+ if (!string.IsNullOrEmpty(systemdExecPid))
+ {
+ if (int.TryParse(systemdExecPid, NumberStyles.None, CultureInfo.InvariantCulture, out int execPid))
+ {
+ if (execPid == processId)
+ {
+ return true;
+ }
+ // Mismatch: fall through to PID 1 / legacy checks
+ }
+ // Malformed value: don't trust it, fall through to legacy detection.
+ }
+
+ // To support containerized systemd services (e.g. Podman with --sdnotify=container)
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).
+ // Note: silently returns false if /proc cannot be read, such as when ProtectProc=invisible is set.
try
{
// Check whether our direct parent is 'systemd'.
@@ -56,5 +72,36 @@ private static bool GetIsSystemdService()
return false;
}
+
+ private static readonly bool _isSystemdNotify = GetIsSystemdNotify();
+
+ ///
+ /// Checks if the current process has systemd notify enabled.
+ ///
+ private 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);
+ }
+
+ ///
+ /// Checks if the systemd journal log formatter should be enabled.
+ ///
+ // TODO: #127218
+ 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 814160886b3c63..6e4c43416b32d6 100644
--- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs
@@ -16,16 +16,21 @@ 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.
///
///
///
/// 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
- /// notifications. See https://www.freedesktop.org/software/systemd/man/systemd.service.html.
+ /// notifications. See .
///
///
/// The to configure.
@@ -34,7 +39,15 @@ public static IHostBuilder UseSystemd(this IHostBuilder hostBuilder)
{
ArgumentNullException.ThrowIfNull(hostBuilder);
- if (SystemdHelpers.IsSystemdService())
+ if (SystemdHelpers.IsSystemdLogger())
+ {
+ hostBuilder.ConfigureServices((hostContext, services) =>
+ {
+ AddSystemdLogger(services);
+ });
+ }
+
+ if (SystemdHelpers.IsSystemdLifetime())
{
hostBuilder.ConfigureServices((hostContext, services) =>
{
@@ -47,12 +60,17 @@ 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.
///
///
///
/// 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
@@ -69,21 +87,33 @@ public static IServiceCollection AddSystemd(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
- if (SystemdHelpers.IsSystemdService())
+ if (SystemdHelpers.IsSystemdLogger())
+ {
+ AddSystemdLogger(services);
+ }
+
+ if (SystemdHelpers.IsSystemdLifetime())
{
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)
+ {
+ // SystemdNotifier and SystemdLifetime are Unix-only; IsSystemdLifetime() ensures
+ // 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();
diff --git a/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs b/src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs
index 791eda2a8413af..8a183fd04cd0b5 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,13 +58,18 @@ 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;
}
+ // 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.
if (socketPath[0] == '@')
{
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..9d679df8f81469
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs
@@ -0,0 +1,148 @@
+// 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 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. 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);
+ Environment.SetEnvironmentVariable("LISTEN_PID", null);
+
+ 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);
+ });
+ }
+
+ [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 948f9b4b527f16..0cafddd15eecf9 100644
--- a/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs
@@ -1,41 +1,193 @@
// 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()
{
- using IHost host = new HostBuilder()
+ 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()
+ {
+ return new HostBuilder()
.UseSystemd()
.Build();
+ }
+
+ [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))]
+ public void AddSystemd_SystemdLoggerIsNotConfiguredAndSystemdLifetimeIsNotRegisteredWhenIsSystemdServiceIsFalse()
+ {
+ // Simulate running outside of a systemd service
+ using var _ = RemoteExecutor.Invoke(static () =>
+ {
+ Environment.SetEnvironmentVariable("SYSTEMD_EXEC_PID", null);
+ Environment.SetEnvironmentVariable("NOTIFY_SOCKET", null);
+
+ using IHost host = BuildHostWithAddSystemd();
+ var options = host.Services.GetRequiredService>().Value;
+ Assert.NotEqual(ConsoleFormatterNames.Systemd, options.FormatterName);
- var lifetime = host.Services.GetRequiredService();
- Assert.NotNull(lifetime);
- Assert.IsNotType(lifetime);
+ var lifetime = host.Services.GetRequiredService();
+ Assert.IsNotType(lifetime);
+ });
}
- [Fact]
- public void ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService()
+ [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))]
+ public void UseSystemd_SystemdLoggerIsNotConfiguredAndSystemdLifetimeIsNotRegisteredWhenIsSystemdServiceIsFalse()
{
- var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings
+ // Simulate running outside of a systemd service
+ using var _ = RemoteExecutor.Invoke(static () =>
{
- // Disable defaults that may not be supported on the testing platform like EventLogLoggerProvider.
- DisableDefaults = true,
+ 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);
});
+ }
- builder.Services.AddSystemd();
- using IHost host = builder.Build();
+ [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))]
+ 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",
+ 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);
+
+ var lifetime = host.Services.GetRequiredService();
+ Assert.IsType(lifetime);
+ });
+ }
+
+ [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))]
+ 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",
+ 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);
+
+ var lifetime = host.Services.GetRequiredService();
+ Assert.IsType(lifetime);
+ });
+ }
+
+ [ConditionalFact(typeof(UseSystemdTests), nameof(IsRemoteExecutorSupportedOnLinux))]
+ 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("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_SystemdLifetimeIsRegisteredAndLoggerIsNotConfiguredWhenOnlyNotifySocketIsSet()
+ {
+ // Simulates: Type=notify without SYSTEMD_EXEC_PID (e.g. containerized, Podman --sdnotify=container, systemd < v248).
+ using var _ = RemoteExecutor.Invoke(static () =>
+ {
+ 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 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_SystemdLoggerAndLifetimeAreBothRegisteredWhenBothConditionsAreMet()
+ {
+ // Simulates: Type=notify with SYSTEMD_EXEC_PID (systemd >= v248).
+ 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()
+ {
+ // Simulates: Type=notify with SYSTEMD_EXEC_PID (systemd >= v248).
+ 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);
+ });
}
}
}