This commit is contained in:
TypNull 2026-04-20 16:06:52 +00:00 committed by GitHub
commit af76a2084b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 492 additions and 177 deletions

View file

@ -21,7 +21,7 @@ function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
const dispatch = useDispatch();
const { authentication, isDocker } = useSelector(
const { authentication, isContainerized } = useSelector(
(state: AppState) => state.system.status.item
);
@ -48,24 +48,22 @@ function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
{translate('KeyboardShortcuts')}
</MenuItem>
{isDocker ? null : (
<>
<MenuItemSeparator />
<MenuItemSeparator />
<MenuItem onPress={handleRestartPress}>
<Icon className={styles.itemIcon} name={icons.RESTART} />
{translate('Restart')}
</MenuItem>
<MenuItem onPress={handleRestartPress}>
<Icon className={styles.itemIcon} name={icons.RESTART} />
{translate('Restart')}
</MenuItem>
<MenuItem onPress={handleShutdownPress}>
<Icon
className={styles.itemIcon}
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
{translate('Shutdown')}
</MenuItem>
</>
{isContainerized ? null : (
<MenuItem onPress={handleShutdownPress}>
<Icon
className={styles.itemIcon}
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
{translate('Shutdown')}
</MenuItem>
)}
{formsAuth ? (

View file

@ -123,12 +123,10 @@ class PluginsConnector extends Component {
console.log('Plugin match result:', pluginMatch);
}
pluginDetailsUrl = url;
} else {
if (command && command.message) {
const pluginMatch = command.message.match(/Plugin \[([^/]+)\/([^\]]+)\] v([0-9.]+) uninstalled/);
if (pluginMatch) {
pluginVersion = pluginMatch[3];
}
} else if (command && command.message) {
const pluginMatch = command.message.match(/Plugin \[([^/]+)\/([^\]]+)\] v([0-9.]+) uninstalled/);
if (pluginMatch) {
pluginVersion = pluginMatch[3];
}
}
}

View file

@ -20,7 +20,7 @@ class About extends Component {
packageVersion,
packageAuthor,
isNetCore,
isDocker,
isContainerized,
runtimeVersion,
databaseVersion,
databaseType,
@ -58,7 +58,7 @@ class About extends Component {
}
{
isDocker &&
isContainerized &&
<DescriptionListItem
title={translate('Docker')}
data={'Yes'}
@ -113,7 +113,7 @@ About.propTypes = {
packageAuthor: PropTypes.string,
isNetCore: PropTypes.bool.isRequired,
runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired,
isContainerized: PropTypes.bool.isRequired,
databaseType: PropTypes.string.isRequired,
databaseVersion: PropTypes.string.isRequired,
migrationVersion: PropTypes.number.isRequired,

View file

@ -7,7 +7,7 @@ interface SystemStatus {
instanceName: string;
isAdmin: boolean;
isDebug: boolean;
isDocker: boolean;
isContainerized: boolean;
isLinux: boolean;
isNetCore: boolean;
isOsx: boolean;

View file

@ -76,6 +76,7 @@ public SystemResource GetStatus()
IsOsx = OsInfo.IsOsx,
IsWindows = OsInfo.IsWindows,
IsDocker = _osInfo.IsDocker,
IsContainerized = _osInfo.IsContainerized,
Mode = _runtimeInfo.Mode,
Branch = _configFileProvider.Branch,
Authentication = _configFileProvider.AuthenticationMethod,

View file

@ -25,6 +25,7 @@ public class SystemResource
public bool IsOsx { get; set; }
public bool IsWindows { get; set; }
public bool IsDocker { get; set; }
public bool IsContainerized { get; set; }
public RuntimeMode Mode { get; set; }
public string Branch { get; set; }
public DatabaseType DatabaseType { get; set; }

View file

@ -9,6 +9,8 @@ public interface IRuntimeInfo
bool IsAdmin { get; }
bool IsWindowsService { get; }
bool IsWindowsTray { get; }
bool IsSystemdService { get; }
bool IsContainerized { get; }
bool IsStarting { get; set; }
bool IsExiting { get; set; }
bool IsTray { get; }

View file

@ -17,6 +17,8 @@ public class OsInfo : IOsInfo
// this needs to not be static so we can mock it
public bool IsDocker { get; }
public bool IsPodman { get; }
public bool IsContainerized { get; }
public string Version { get; }
public string Name { get; }
@ -79,11 +81,14 @@ public OsInfo(IEnumerable<IOsVersionAdapter> versionAdapters, Logger logger)
FullName = Name;
}
if (IsLinux &&
(File.Exists("/.dockerenv") ||
(File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/"))))
if (IsLinux)
{
IsDocker = true;
IsDocker = File.Exists("/.dockerenv") ||
(File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/"));
IsPodman = File.Exists("/run/.containerenv") ||
Environment.GetEnvironmentVariable("container") != null;
IsContainerized = IsDocker || IsPodman || Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is "true" or "1";
}
}
}
@ -94,6 +99,8 @@ public interface IOsInfo
string Name { get; }
string FullName { get; }
bool IsDocker { get; }
bool IsPodman { get; }
bool IsContainerized { get; }
}
public enum Os

View file

@ -12,11 +12,13 @@ namespace NzbDrone.Common.EnvironmentInfo
public class RuntimeInfo : IRuntimeInfo
{
private readonly Logger _logger;
private readonly IOsInfo _osInfo;
private readonly DateTime _startTime = DateTime.UtcNow;
public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
public RuntimeInfo(Logger logger, IOsInfo osInfo, IHostLifetime hostLifetime = null)
{
_logger = logger;
_osInfo = osInfo;
IsWindowsService = hostLifetime is WindowsServiceLifetime;
IsStarting = true;
@ -83,6 +85,30 @@ public bool IsAdmin
public bool IsWindowsService { get; private set; }
public bool IsContainerized => _osInfo.IsContainerized;
public bool IsSystemdService
{
get
{
if (!OsInfo.IsLinux)
{
return false;
}
try
{
var invocationId = Environment.GetEnvironmentVariable("INVOCATION_ID");
return !string.IsNullOrEmpty(invocationId);
}
catch (Exception ex)
{
_logger.Warn(ex, "Error checking if system is running under systemd");
return false;
}
}
}
public bool IsStarting { get; set; }
public bool IsExiting { get; set; }

View file

@ -223,6 +223,13 @@ public static void ConfigureConsoleLayout(ColoredConsoleTarget target, ConsoleLo
_ => NzbDroneLogger.CleansingConsoleLayout
};
}
public static void ResetAllTargets(IStartupContext startupContext, bool updateApp, bool inConsole)
{
LogManager.Configuration = new LoggingConfiguration();
_isConfigured = false;
Register(startupContext, updateApp, inConsole);
}
}
public enum ConsoleLogFormat

View file

@ -173,7 +173,7 @@ public void UpdateScope(IOsInfo osInfo)
{
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
scope.SetTag("is_containerized", $"{osInfo.IsContainerized}");
});
}

View file

@ -0,0 +1,9 @@
using System;
namespace NzbDrone.Common.Messaging
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class LifecycleEventAttribute : Attribute
{
}
}

View file

@ -7,6 +7,13 @@ namespace NzbDrone.Common.Reflection
{
public static class ReflectionExtensions
{
private static HashSet<Assembly> _currentAssemblies = new HashSet<Assembly>();
public static void SetCurrentAssemblies(IEnumerable<Assembly> assemblies)
{
_currentAssemblies = new HashSet<Assembly>(assemblies);
}
public static List<PropertyInfo> GetSimpleProperties(this Type type)
{
var properties = type.GetProperties();
@ -93,7 +100,18 @@ private static bool ShouldUseAssembly(Assembly assembly)
}
var name = assembly.GetName();
return name.Name == "Lidarr.Core" || name.Name.Contains("Lidarr.Plugin");
if (name.Name == "Lidarr.Core")
{
return true;
}
if (name.Name.Contains("Lidarr.Plugin"))
{
return _currentAssemblies.Count == 0 || _currentAssemblies.Contains(assembly);
}
return false;
}
public static bool HasAttribute<TAttribute>(this Type type)

View file

@ -2,6 +2,7 @@
namespace NzbDrone.Core.Lifecycle
{
[LifecycleEvent]
public class ApplicationShutdownRequested : IEvent
{
public bool Restarting { get; }

View file

@ -2,6 +2,7 @@
namespace NzbDrone.Core.Lifecycle
{
[LifecycleEvent]
public class ApplicationStartedEvent : IEvent
{
}

View file

@ -2,6 +2,7 @@
namespace NzbDrone.Core.Lifecycle
{
[LifecycleEvent]
public class ApplicationStartingEvent : IEvent
{
}

View file

@ -1,10 +1,8 @@
using NLog;
using NzbDrone.Common;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Lifecycle.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using IServiceProvider = NzbDrone.Common.IServiceProvider;
namespace NzbDrone.Core.Lifecycle
{
@ -18,41 +16,28 @@ public class LifecycleService : ILifecycleService, IExecute<ShutdownCommand>, IE
{
private readonly IEventAggregator _eventAggregator;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IServiceProvider _serviceProvider;
private readonly Logger _logger;
public LifecycleService(IEventAggregator eventAggregator,
IRuntimeInfo runtimeInfo,
IServiceProvider serviceProvider,
Logger logger)
{
_eventAggregator = eventAggregator;
_runtimeInfo = runtimeInfo;
_serviceProvider = serviceProvider;
_logger = logger;
}
public void Shutdown()
{
_logger.Info("Shutdown requested.");
_eventAggregator.PublishEvent(new ApplicationShutdownRequested());
if (_runtimeInfo.IsWindowsService)
{
_serviceProvider.Stop(ServiceProvider.SERVICE_NAME);
}
_eventAggregator.PublishEvent(new ApplicationShutdownRequested(false));
}
public void Restart()
{
_logger.Info("Restart requested.");
_runtimeInfo.RestartPending = true;
_eventAggregator.PublishEvent(new ApplicationShutdownRequested(true));
if (_runtimeInfo.IsWindowsService)
{
_serviceProvider.Restart(ServiceProvider.SERVICE_NAME);
}
}
public void Execute(ShutdownCommand message)

View file

@ -5,7 +5,9 @@
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Messaging;
using NzbDrone.Common.Reflection;
using NzbDrone.Common.TPL;
namespace NzbDrone.Core.Messaging.Events
@ -14,6 +16,7 @@ public class EventAggregator : IEventAggregator
{
private readonly Logger _logger;
private readonly IServiceFactory _serviceFactory;
private readonly IRuntimeInfo _runtimeInfo;
private readonly TaskFactory _taskFactory;
private readonly Dictionary<string, object> _eventSubscribers;
@ -39,10 +42,11 @@ public EventSubscribers(IServiceFactory serviceFactory)
}
}
public EventAggregator(Logger logger, IServiceFactory serviceFactory)
public EventAggregator(Logger logger, IServiceFactory serviceFactory, IRuntimeInfo runtimeInfo)
{
_logger = logger;
_serviceFactory = serviceFactory;
_runtimeInfo = runtimeInfo;
_taskFactory = new TaskFactory();
_eventSubscribers = new Dictionary<string, object>();
}
@ -54,6 +58,12 @@ public void PublishEvent<TEvent>(TEvent @event)
var eventName = GetEventName(@event.GetType());
if (_runtimeInfo.IsExiting && @event.GetType().HasAttribute<LifecycleEventAttribute>())
{
_logger.Warn("Event {0} blocked due to application shutdown", eventName);
return;
}
/*
int workerThreads;
int completionPortThreads;
@ -78,7 +88,15 @@ public void PublishEvent<TEvent>(TEvent @event)
{
if (!_eventSubscribers.TryGetValue(eventName, out var target))
{
_eventSubscribers[eventName] = target = new EventSubscribers<TEvent>(_serviceFactory);
try
{
_eventSubscribers[eventName] = target = new EventSubscribers<TEvent>(_serviceFactory);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to resolve event subscribers for {0}, container may be disposed", eventName);
return;
}
}
subscribers = target as EventSubscribers<TEvent>;

View file

@ -18,7 +18,6 @@ public class AppLifetime : IHostedService, IHandle<ApplicationShutdownRequested>
private readonly IRuntimeInfo _runtimeInfo;
private readonly IStartupContext _startupContext;
private readonly IBrowserService _browserService;
private readonly IProcessProvider _processProvider;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
@ -36,7 +35,6 @@ public AppLifetime(IHostApplicationLifetime appLifetime,
_runtimeInfo = runtimeInfo;
_startupContext = startupContext;
_browserService = browserService;
_processProvider = processProvider;
_eventAggregator = eventAggregator;
_logger = logger;
@ -70,12 +68,13 @@ private void OnAppStarted()
private void OnAppStopped()
{
if (_runtimeInfo.RestartPending && !_runtimeInfo.IsWindowsService)
if (_runtimeInfo.RestartPending)
{
var restartArgs = GetRestartArgs();
_logger.Info("Attempting restart with arguments: {0}", restartArgs);
_processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, restartArgs);
_logger.Info("Restart pending.");
}
else
{
_logger.Info("Application stopped without restart pending");
}
}
@ -87,33 +86,21 @@ private void Shutdown()
_appLifetime.StopApplication();
}
private string GetRestartArgs()
{
var args = _startupContext.PreservedArguments;
args += " /restart";
if (!args.Contains("/nobrowser"))
{
args += " /nobrowser";
}
return args;
}
[EventHandleOrder(EventHandleOrder.Last)]
public void Handle(ApplicationShutdownRequested message)
{
if (!_runtimeInfo.IsWindowsService)
if (message.Restarting)
{
if (message.Restarting)
{
_runtimeInfo.RestartPending = true;
}
LogManager.Configuration = null;
Shutdown();
_runtimeInfo.RestartPending = true;
_logger.Debug("Restart requested");
}
else
{
_logger.Debug("Shutdown requested");
LogManager.Configuration = null;
}
Shutdown();
}
}
}

View file

@ -8,6 +8,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
@ -25,6 +26,7 @@
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore.Extensions;
using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions;
@ -51,18 +53,15 @@ public static void Start(string[] args, Action<IHostBuilder> trayCallback = null
var appMode = GetApplicationMode(startupContext);
var config = GetConfiguration(startupContext);
switch (appMode)
if (appMode is not(ApplicationModes.Interactive or ApplicationModes.Service))
{
case ApplicationModes.Service:
StartService(startupContext);
break;
case ApplicationModes.Interactive:
StartInteractive(startupContext, trayCallback);
break;
default:
StartUtility(startupContext, appMode, config);
break;
RunUtilityMode(appMode, startupContext, config);
return;
}
RunHostUntilShutdown(args, startupContext, appMode, trayCallback);
Logger.Info("Lidarr has shut down completely");
}
catch (InvalidConfigFileException ex)
{
@ -84,88 +83,12 @@ public static void Start(string[] args, Action<IHostBuilder> trayCallback = null
SQLiteConnection.ClearAllPools();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void StartService(StartupContext context)
private static void RunUtilityMode(ApplicationModes appMode, StartupContext startupContext, IConfiguration config)
{
Logger.Debug("Service selected");
Logger.Debug("Utility mode: {0}", appMode);
var success = StartService(context, true, out var pluginRefs);
if (!success)
{
var unloadSuccess = PluginLoader.UnloadPlugins(pluginRefs);
if (unloadSuccess)
{
StartService(context, false, out _);
}
}
CreateConsoleHostBuilder(context, false, out _).UseWindowsService().Build().Run();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void StartInteractive(StartupContext context, Action<IHostBuilder> trayCallback)
{
Logger.Debug(trayCallback != null ? "Tray selected" : "Console selected");
var success = StartInteractive(context, trayCallback, true, out var pluginRefs);
if (!success)
{
var unloadSuccess = PluginLoader.UnloadPlugins(pluginRefs);
if (unloadSuccess)
{
StartInteractive(context, trayCallback, false, out _);
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool StartService(StartupContext context, bool usePlugins, out List<WeakReference> pluginRefs)
{
var builder = CreateConsoleHostBuilder(context, usePlugins, out pluginRefs).UseWindowsService();
return RunBuilder(builder, usePlugins);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool StartInteractive(StartupContext context, Action<IHostBuilder> trayCallback, bool usePlugins, out List<WeakReference> pluginRefs)
{
var builder = CreateConsoleHostBuilder(context, usePlugins, out pluginRefs);
if (trayCallback != null)
{
trayCallback(builder);
}
return RunBuilder(builder, usePlugins);
}
private static bool RunBuilder(IHostBuilder builder, bool usePlugins)
{
try
{
using var host = builder.Build();
host.Run();
}
catch (Exception e)
{
if (usePlugins)
{
Logger.Warn(e, "Error starting with plugins enabled");
}
return false;
}
return true;
}
private static void StartUtility(StartupContext context, ApplicationModes mode, IConfiguration config)
{
var assemblies = AssemblyLoader.LoadBaseAssemblies();
new HostBuilder()
.UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules())))
.ConfigureContainer<IContainer>(c =>
@ -173,9 +96,9 @@ private static void StartUtility(StartupContext context, ApplicationModes mode,
c.AutoAddServices(assemblies)
.AddNzbDroneLogger()
.AddDatabase()
.AddStartupContext(context)
.AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(mode);
.Route(appMode);
if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true))
{
@ -194,10 +117,72 @@ private static void StartUtility(StartupContext context, ApplicationModes mode,
services.Configure<ServerOptions>(config.GetSection("Lidarr:Server"));
services.Configure<LogOptions>(config.GetSection("Lidarr:Log"));
services.Configure<UpdateOptions>(config.GetSection("Lidarr:Update"));
}).Build();
})
.Build();
}
private static IHostBuilder CreateConsoleHostBuilder(StartupContext context, bool usePlugins, out List<WeakReference> pluginRef)
private static void RunHostUntilShutdown(string[] args, StartupContext startupContext, ApplicationModes appMode, Action<IHostBuilder> trayCallback)
{
Logger.Debug("Starting in {0} mode", trayCallback != null ? "Tray" : appMode.ToString());
bool shouldRestart;
do
{
var success = RunHost(args, startupContext, trayCallback, true, out var pluginRefs, out shouldRestart);
if (!success)
{
var unloadSuccess = PluginLoader.UnloadPlugins(pluginRefs);
if (unloadSuccess)
{
RunHost(args, startupContext, trayCallback, false, out _, out shouldRestart);
}
}
if (shouldRestart)
{
Logger.Info("Application restart requested, reinitializing host");
PluginLoader.UnloadPlugins(pluginRefs);
NzbDroneLogger.ResetAllTargets(startupContext, false, true);
Thread.Sleep(1000);
}
}
while (shouldRestart);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool RunHost(string[] args, StartupContext startupContext, Action<IHostBuilder> trayCallback, bool usePlugins, out List<WeakReference> pluginRefs, out bool shouldRestart)
{
shouldRestart = false;
var builder = CreateConsoleHostBuilder(args, startupContext, usePlugins, out pluginRefs);
trayCallback?.Invoke(builder);
if (OsInfo.IsWindows && WindowsServiceHelpers.IsWindowsService())
{
builder.UseWindowsService();
}
try
{
using var host = builder.Build();
shouldRestart = RunWithRestartCheck(host);
}
catch (Exception e)
{
if (usePlugins)
{
Logger.Warn(e, "Error starting with plugins enabled");
}
return false;
}
return true;
}
public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContext context, bool usePlugins, out List<WeakReference> pluginRef)
{
var config = GetConfiguration(context);
@ -224,7 +209,13 @@ private static IHostBuilder CreateConsoleHostBuilder(StartupContext context, boo
var pluginPaths = new AppFolderInfo(context).GetPluginAssemblies().ToList();
(var plugins, pluginRef) = PluginLoader.LoadPlugins(pluginPaths);
assemblies.AddRange(plugins.Where(x => x != null));
var loadedPlugins = plugins.Where(x => x != null).ToList();
assemblies.AddRange(loadedPlugins);
ReflectionExtensions.SetCurrentAssemblies(loadedPlugins);
}
else
{
ReflectionExtensions.SetCurrentAssemblies(Enumerable.Empty<Assembly>());
}
return new HostBuilder()
@ -367,5 +358,20 @@ private static X509Certificate2 ValidateSslCertificate(string cert, string passw
return certificate;
}
private static bool RunWithRestartCheck(IHost host)
{
var shouldRestart = false;
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopped.Register(() =>
{
var runtimeInfo = host.Services.GetRequiredService<IRuntimeInfo>();
shouldRestart = runtimeInfo.RestartPending;
});
host.Run();
return shouldRestart;
}
}
}

View file

@ -0,0 +1,249 @@
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Host
{
public class RestartableServiceLifetime : IHostLifetime, IDisposable
{
private static readonly object StaticLock = new object();
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(RestartableServiceLifetime));
private static WindowsServiceWrapper _singletonService;
private static bool _serviceBaseRunCalled;
private readonly IHostEnvironment _environment;
private readonly bool _isWindowsService;
private readonly ConsoleLifetime _consoleLifetime;
private readonly TaskCompletionSource<object> _waitForStartTask;
private readonly ManualResetEventSlim _hostStoppedEvent;
private CancellationTokenRegistration _stoppedRegistration;
private bool _isDisposed;
public IHostApplicationLifetime ApplicationLifetime { get; private set; }
internal TaskCompletionSource<object> WaitForStartTask => _waitForStartTask;
public RestartableServiceLifetime(
IHostEnvironment environment,
IHostApplicationLifetime applicationLifetime,
ILoggerFactory loggerFactory,
IOptions<ConsoleLifetimeOptions> consoleOptions,
IOptions<HostOptions> hostOptions)
{
_environment = environment;
_isWindowsService = WindowsServiceHelpers.IsWindowsService();
ApplicationLifetime = applicationLifetime;
if (_isWindowsService)
{
_waitForStartTask = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_hostStoppedEvent = new ManualResetEventSlim(false);
}
else
{
_consoleLifetime = new ConsoleLifetime(consoleOptions, environment, applicationLifetime, hostOptions, loggerFactory);
}
}
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(RestartableServiceLifetime));
}
if (!_isWindowsService)
{
return _consoleLifetime.WaitForStartAsync(cancellationToken);
}
lock (StaticLock)
{
_singletonService ??= new WindowsServiceWrapper(_environment.ApplicationName);
if (_singletonService.CurrentLifetime != this)
{
Logger.Trace("In-process restart detected, preparing host");
_singletonService.PrepareForRestart(this);
}
if (!_serviceBaseRunCalled)
{
_serviceBaseRunCalled = true;
Logger.Debug("Starting Windows Service");
var serviceThread = new Thread(_singletonService.RunServiceBase)
{
IsBackground = false,
Name = "Windows.Service.Lifetime"
};
serviceThread.Start();
}
}
RegisterStoppedCallback();
return _waitForStartTask.Task.WaitAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
if (_isDisposed)
{
return Task.CompletedTask;
}
if (!_isWindowsService)
{
return _consoleLifetime.StopAsync(cancellationToken);
}
try
{
_hostStoppedEvent.Wait(cancellationToken);
}
catch (OperationCanceledException)
{
Logger.Warn("Host stop was cancelled");
}
return Task.CompletedTask;
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!_isWindowsService)
{
_consoleLifetime?.Dispose();
}
else
{
_stoppedRegistration.Dispose();
if (_singletonService?.CurrentLifetime != this)
{
_hostStoppedEvent?.Dispose();
}
}
}
internal void CleanupForRestart()
{
_stoppedRegistration.Dispose();
}
internal void PrepareForRestart(IHostApplicationLifetime newApplicationLifetime)
{
ApplicationLifetime = newApplicationLifetime;
_hostStoppedEvent.Reset();
_waitForStartTask.TrySetResult(null);
}
private void OnServiceStart()
{
try
{
_waitForStartTask.TrySetResult(null);
}
catch (Exception ex)
{
Logger.Error(ex, "Service start failed");
_waitForStartTask.TrySetException(ex);
throw;
}
}
private void OnServiceStop()
{
try
{
ApplicationLifetime?.StopApplication();
}
catch (Exception ex)
{
Logger.Error(ex, "Error during service stop");
throw;
}
}
private void RegisterStoppedCallback()
{
_stoppedRegistration = ApplicationLifetime.ApplicationStopped.Register(state => ((RestartableServiceLifetime)state)._hostStoppedEvent.Set(), this);
}
private sealed class WindowsServiceWrapper : ServiceBase
{
public RestartableServiceLifetime CurrentLifetime { get; private set; }
public WindowsServiceWrapper(string serviceName)
{
ServiceName = serviceName;
}
public void PrepareForRestart(RestartableServiceLifetime newLifetime)
{
CurrentLifetime?.CleanupForRestart();
CurrentLifetime = newLifetime;
newLifetime.PrepareForRestart(newLifetime.ApplicationLifetime);
}
public void RunServiceBase()
{
try
{
Run(this);
CurrentLifetime?.WaitForStartTask.TrySetException(new InvalidOperationException("Service stopped without starting"));
}
catch (Exception ex)
{
Logger.Error(ex, "Service base execution failed");
CurrentLifetime?.WaitForStartTask.TrySetException(ex);
}
}
protected override void OnStart(string[] args)
{
CurrentLifetime?.OnServiceStart();
base.OnStart(args);
}
protected override void OnStop()
{
CurrentLifetime?.OnServiceStop();
base.OnStop();
}
protected override void OnShutdown()
{
try
{
CurrentLifetime?.ApplicationLifetime?.StopApplication();
}
catch (Exception ex)
{
Logger.Error(ex, "Error during system shutdown");
}
base.OnShutdown();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
}
}
}