mirror of
https://github.com/Lidarr/Lidarr
synced 2026-05-07 12:02:14 +02:00
Merge 90080d6f5c into 58a1d9357b
This commit is contained in:
commit
af76a2084b
21 changed files with 492 additions and 177 deletions
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface SystemStatus {
|
|||
instanceName: string;
|
||||
isAdmin: boolean;
|
||||
isDebug: boolean;
|
||||
isDocker: boolean;
|
||||
isContainerized: boolean;
|
||||
isLinux: boolean;
|
||||
isNetCore: boolean;
|
||||
isOsx: boolean;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ public void UpdateScope(IOsInfo osInfo)
|
|||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
|
||||
scope.SetTag("is_containerized", $"{osInfo.IsContainerized}");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
9
src/NzbDrone.Common/Messaging/LifecycleEventAttribute.cs
Normal file
9
src/NzbDrone.Common/Messaging/LifecycleEventAttribute.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Messaging
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class LifecycleEventAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace NzbDrone.Core.Lifecycle
|
||||
{
|
||||
[LifecycleEvent]
|
||||
public class ApplicationShutdownRequested : IEvent
|
||||
{
|
||||
public bool Restarting { get; }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace NzbDrone.Core.Lifecycle
|
||||
{
|
||||
[LifecycleEvent]
|
||||
public class ApplicationStartedEvent : IEvent
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace NzbDrone.Core.Lifecycle
|
||||
{
|
||||
[LifecycleEvent]
|
||||
public class ApplicationStartingEvent : IEvent
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
249
src/NzbDrone.Host/RestartableServiceLifetime.cs
Normal file
249
src/NzbDrone.Host/RestartableServiceLifetime.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue