diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7b3d07dfc..3db48e42f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -193,11 +193,6 @@ namespace Emby.Server.Implementations /// private string PublishedServerUrl => _startupConfig[AddressOverrideKey]; - /// - /// Gets a value indicating whether this instance can self restart. - /// - public bool CanSelfRestart => _startupOptions.RestartPath is not null; - public bool CoreStartupHasCompleted { get; private set; } public virtual bool CanLaunchWebBrowser @@ -935,17 +930,13 @@ namespace Emby.Server.Implementations /// public void Restart() { - if (!CanSelfRestart) - { - throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually."); - } - if (IsShuttingDown) { return; } IsShuttingDown = true; + _pluginManager.UnloadAssemblies(); Task.Run(async () => { @@ -1047,7 +1038,6 @@ namespace Emby.Server.Implementations CachePath = ApplicationPaths.CachePath, OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(), OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name, - CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 3769ae4dd..b7bcaace1 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -20,16 +20,6 @@ namespace Emby.Server.Implementations /// string? PackageName { get; } - /// - /// Gets the value of the --restartpath command line option. - /// - string? RestartPath { get; } - - /// - /// Gets the value of the --restartargs command line option. - /// - string? RestartArgs { get; } - /// /// Gets the value of the --published-server-url command line option. /// diff --git a/Emby.Server.Implementations/Plugins/PluginLoadContext.cs b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs new file mode 100644 index 000000000..d04e9cf68 --- /dev/null +++ b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Emby.Server.Implementations.Plugins; + +/// +/// A custom for loading Jellyfin plugins. +/// +public class PluginLoadContext : AssemblyLoadContext +{ + private readonly AssemblyDependencyResolver _resolver; + + /// + /// Initializes a new instance of the class. + /// + /// The path of the plugin assembly. + public PluginLoadContext(string path) : base(true) + { + _resolver = new AssemblyDependencyResolver(path); + } + + /// + protected override Assembly? Load(AssemblyName assemblyName) + { + var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath is not null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } +} diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 14e7c2269..f2212f4dc 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Runtime.Loader; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -30,6 +31,7 @@ namespace Emby.Server.Implementations.Plugins { private readonly string _pluginsPath; private readonly Version _appVersion; + private readonly List _assemblyLoadContexts; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger _logger; private readonly IApplicationHost _appHost; @@ -76,6 +78,8 @@ namespace Emby.Server.Implementations.Plugins _appHost = appHost; _minimumVersion = new Version(0, 0, 0, 1); _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List(); + + _assemblyLoadContexts = new List(); } private IHttpClientFactory HttpClientFactory @@ -124,7 +128,10 @@ namespace Emby.Server.Implementations.Plugins Assembly assembly; try { - assembly = Assembly.LoadFrom(file); + var assemblyLoadContext = new PluginLoadContext(file); + _assemblyLoadContexts.Add(assemblyLoadContext); + + assembly = assemblyLoadContext.LoadFromAssemblyPath(file); // Load all required types to verify that the plugin will load assembly.GetTypes(); @@ -156,6 +163,15 @@ namespace Emby.Server.Implementations.Plugins } } + /// + public void UnloadAssemblies() + { + foreach (var assemblyLoadContext in _assemblyLoadContexts) + { + assemblyLoadContext.Unload(); + } + } + /// /// Creates all the plugin instances. /// diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs similarity index 94% rename from Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs rename to Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs index ea8c5ecdb..8f1f5dd94 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs @@ -2,7 +2,7 @@ using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Camel Case Json Profile Formatter. diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs similarity index 97% rename from Jellyfin.Server/Formatters/CssOutputFormatter.cs rename to Jellyfin.Api/Formatters/CssOutputFormatter.cs index fdaa48f84..e88c8ad1b 100644 --- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Css output formatter. diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs similarity index 95% rename from Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs rename to Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs index 03ca7dda7..5d77dbf4c 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs @@ -3,7 +3,7 @@ using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Pascal Case Json Profile Formatter. diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs similarity index 96% rename from Jellyfin.Server/Formatters/XmlOutputFormatter.cs rename to Jellyfin.Api/Formatters/XmlOutputFormatter.cs index 156368d69..df8b1650b 100644 --- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Xml output formatter. diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs similarity index 99% rename from Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs rename to Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs index 6ee5bf38a..6bd9e0b08 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Redirect requests without baseurl prefix to the baseurl prefixed URL. diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs similarity index 99% rename from Jellyfin.Server/Middleware/ExceptionMiddleware.cs rename to Jellyfin.Api/Middleware/ExceptionMiddleware.cs index 91dbce19a..6b3aeb187 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Exception Middleware. diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs rename to Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs index 0afcd61a0..f7af91e48 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs @@ -4,7 +4,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Validates the IP of requests coming from local networks wrt. remote access. diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/LanFilteringMiddleware.cs rename to Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 67bf24d2a..18f13bbce 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -5,7 +5,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Validates the LAN host IP based on application configuration. diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs similarity index 98% rename from Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs rename to Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs index b214299df..b73923c1e 100644 --- a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs +++ b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Removes /emby and /mediabrowser from requested route. diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs rename to Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs index 24807ce38..4b6304e0e 100644 --- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs +++ b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// URL decodes the querystring before binding. diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs similarity index 98% rename from Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs rename to Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs index 531897cd4..3701d0f45 100644 --- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs +++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Response time middleware. diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs rename to Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs index fabcd2da7..2e69580be 100644 --- a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Redirect requests to robots.txt to web/robots.txt. diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs similarity index 98% rename from Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs rename to Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs index 2ec063392..dcd64401a 100644 --- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs +++ b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Shows a custom message during server startup. diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs similarity index 98% rename from Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs rename to Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs index 2f1d79157..d35e0fcfd 100644 --- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs +++ b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Defines the . diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs rename to Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs index b7a5d2b34..2cf1e5e4a 100644 --- a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs +++ b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Handles WebSocket requests. diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index e29167747..463ca7321 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Jellyfin.Api.Middleware; using Jellyfin.Networking.Configuration; -using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index f74152405..e9af1cf83 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -20,13 +20,13 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; -using Jellyfin.Server.Formatters; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs new file mode 100644 index 000000000..58d3e1b2d --- /dev/null +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Net; +using Jellyfin.Server.Helpers; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Extensions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Extensions; + +/// +/// Extensions for configuring the web host builder. +/// +public static class WebHostBuilderExtensions +{ + /// + /// Configure the web host builder. + /// + /// The builder to configure. + /// The application host. + /// The application configuration. + /// The application paths. + /// The logger. + /// The configured web host builder. + public static IWebHostBuilder ConfigureWebHostBuilder( + this IWebHostBuilder builder, + CoreAppHost appHost, + IConfiguration startupConfig, + IApplicationPaths appPaths, + ILogger logger) + { + return builder + .UseKestrel((builderContext, options) => + { + var addresses = appHost.NetManager.GetAllBindInterfaces(); + + bool flagged = false; + foreach (IPObject netAdd in addresses) + { + logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd); + options.Listen(netAdd.Address, appHost.HttpPort); + if (appHost.ListenWithHttps) + { + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps(appHost.Certificate)); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + try + { + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps()); + } + catch (InvalidOperationException) + { + if (!flagged) + { + logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); + flagged = true; + } + } + } + } + + // Bind to unix socket (only on unix systems) + if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) + { + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); + + // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + options.ListenUnixSocket(socketPath); + logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + } + }) + .UseStartup(_ => new Startup(appHost)); + } +} diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs new file mode 100644 index 000000000..f1bb9b283 --- /dev/null +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -0,0 +1,326 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using Emby.Server.Implementations; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Serilog; +using SQLitePCL; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Jellyfin.Server.Helpers; + +/// +/// A class containing helper methods for server startup. +/// +public static class StartupHelpers +{ + /// + /// Create the data, config and log paths from the variety of inputs(command line args, + /// environment variables) or decide on what default to use. For Windows it's %AppPath% + /// for everything else the + /// XDG approach + /// is followed. + /// + /// The for this instance. + /// . + public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) + { + // dataDir + // IF --datadir + // ELSE IF $JELLYFIN_DATA_DIR + // ELSE IF windows, use <%APPDATA%>/jellyfin + // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin + // ELSE use $HOME/.local/share/jellyfin + var dataDir = options.DataDir; + if (string.IsNullOrEmpty(dataDir)) + { + dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); + + if (string.IsNullOrEmpty(dataDir)) + { + // LocalApplicationData follows the XDG spec on unix machines + dataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "jellyfin"); + } + } + + // configDir + // IF --configdir + // ELSE IF $JELLYFIN_CONFIG_DIR + // ELSE IF --datadir, use /config (assume portable run) + // ELSE IF /config exists, use that + // ELSE IF windows, use /config + // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin + // ELSE $HOME/.config/jellyfin + var configDir = options.ConfigDir; + if (string.IsNullOrEmpty(configDir)) + { + configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); + + if (string.IsNullOrEmpty(configDir)) + { + if (options.DataDir is not null + || Directory.Exists(Path.Combine(dataDir, "config")) + || OperatingSystem.IsWindows()) + { + // Hang config folder off already set dataDir + configDir = Path.Combine(dataDir, "config"); + } + else + { + // $XDG_CONFIG_HOME defines the base directory relative to which + // user specific configuration files should be stored. + configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + + // If $XDG_CONFIG_HOME is either not set or empty, + // a default equal to $HOME /.config should be used. + if (string.IsNullOrEmpty(configDir)) + { + configDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config"); + } + + configDir = Path.Combine(configDir, "jellyfin"); + } + } + } + + // cacheDir + // IF --cachedir + // ELSE IF $JELLYFIN_CACHE_DIR + // ELSE IF windows, use /cache + // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin + // ELSE HOME/.cache/jellyfin + var cacheDir = options.CacheDir; + if (string.IsNullOrEmpty(cacheDir)) + { + cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); + + if (string.IsNullOrEmpty(cacheDir)) + { + if (OperatingSystem.IsWindows()) + { + // Hang cache folder off already set dataDir + cacheDir = Path.Combine(dataDir, "cache"); + } + else + { + // $XDG_CACHE_HOME defines the base directory relative to which + // user specific non-essential data files should be stored. + cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + + // If $XDG_CACHE_HOME is either not set or empty, + // a default equal to $HOME/.cache should be used. + if (string.IsNullOrEmpty(cacheDir)) + { + cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cache"); + } + + cacheDir = Path.Combine(cacheDir, "jellyfin"); + } + } + } + + // webDir + // IF --webdir + // ELSE IF $JELLYFIN_WEB_DIR + // ELSE /jellyfin-web + var webDir = options.WebDir; + if (string.IsNullOrEmpty(webDir)) + { + webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); + + if (string.IsNullOrEmpty(webDir)) + { + // Use default location under ResourcesPath + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); + } + } + + // logDir + // IF --logdir + // ELSE IF $JELLYFIN_LOG_DIR + // ELSE IF --datadir, use /log (assume portable run) + // ELSE /log + var logDir = options.LogDir; + if (string.IsNullOrEmpty(logDir)) + { + logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); + + if (string.IsNullOrEmpty(logDir)) + { + // Hang log folder off already set dataDir + logDir = Path.Combine(dataDir, "log"); + } + } + + // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162 + dataDir = Path.GetFullPath(dataDir); + logDir = Path.GetFullPath(logDir); + configDir = Path.GetFullPath(configDir); + cacheDir = Path.GetFullPath(cacheDir); + webDir = Path.GetFullPath(webDir); + + // Ensure the main folders exist before we continue + try + { + Directory.CreateDirectory(dataDir); + Directory.CreateDirectory(logDir); + Directory.CreateDirectory(configDir); + Directory.CreateDirectory(cacheDir); + } + catch (IOException ex) + { + Console.Error.WriteLine("Error whilst attempting to create folder"); + Console.Error.WriteLine(ex.ToString()); + Environment.Exit(1); + } + + return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); + } + + /// + /// Gets the path for the unix socket Kestrel should bind to. + /// + /// The startup config. + /// The application paths. + /// The path for Kestrel to bind to. + public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths) + { + var socketPath = startupConfig.GetUnixSocketPath(); + + if (string.IsNullOrEmpty(socketPath)) + { + var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + var socketFile = "jellyfin.sock"; + if (xdgRuntimeDir is null) + { + // Fall back to config dir + socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile); + } + else + { + socketPath = Path.Join(xdgRuntimeDir, socketFile); + } + } + + return socketPath; + } + + /// + /// Sets the unix file permissions for Kestrel's socket file. + /// + /// The startup config. + /// The socket path. + /// The logger. + [UnsupportedOSPlatform("windows")] + public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger) + { + var socketPerms = startupConfig.GetUnixSocketPermissions(); + + if (!string.IsNullOrEmpty(socketPerms)) + { + File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8)); + logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms); + } + } + + /// + /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist + /// already. + /// + /// The application paths. + /// A task representing the creation of the configuration file, or a completed task if the file already exists. + public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) + { + // Do nothing if the config file already exists + string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault); + if (File.Exists(configPath)) + { + return; + } + + // Get a stream of the resource contents + // NOTE: The .csproj name is used instead of the assembly name in the resource path + const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; + Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) + ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); + await using (resource.ConfigureAwait(false)) + { + Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (dst.ConfigureAwait(false)) + { + // Copy the resource contents to the expected file path for the config file + await resource.CopyToAsync(dst).ConfigureAwait(false); + } + } + } + + /// + /// Initialize Serilog using configuration and fall back to defaults on failure. + /// + /// The configuration object. + /// The application paths. + public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) + { + try + { + // Serilog.Log is used by SerilogLoggerFactory when no logger is specified + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .CreateLogger(); + } + catch (Exception ex) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}", + formatProvider: CultureInfo.InvariantCulture) + .WriteTo.Async(x => x.File( + Path.Combine(appPaths.LogDirectoryPath, "log_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", + formatProvider: CultureInfo.InvariantCulture, + encoding: Encoding.UTF8)) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .CreateLogger(); + + Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); + } + } + + /// + /// Call static initialization methods for the application. + /// + public static void PerformStaticInitialization() + { + // Make sure we have all the code pages we can get + // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // Increase the max http request limit + // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. + ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); + + // Disable the "Expect: 100-Continue" header by default + // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c + ServicePointManager.Expect100Continue = false; + + Batteries_V2.Init(); + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 540375dce..dded20347 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,22 +1,18 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Reflection; -using System.Runtime.Versioning; -using System.Text; using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Server.Extensions; +using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.IO; -using Microsoft.AspNetCore.Hosting; +using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -25,7 +21,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; -using SQLitePCL; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -46,8 +41,9 @@ namespace Jellyfin.Server /// public const string LoggingConfigFileSystem = "logging.json"; - private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static CancellationTokenSource _tokenSource = new(); + private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -92,14 +88,14 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { - var startTimestamp = Stopwatch.GetTimestamp(); + _startTimestamp = Stopwatch.GetTimestamp(); // Log all uncaught exceptions to std error static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => - Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString()); + Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject); AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; - ServerApplicationPaths appPaths = CreateApplicationPaths(options); + ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -108,13 +104,12 @@ namespace Jellyfin.Server Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1"); Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1"); - await InitLoggingConfigFile(appPaths).ConfigureAwait(false); + await StartupHelpers.InitLoggingConfigFile(appPaths).ConfigureAwait(false); // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); - // Initialize logging framework - InitializeLoggingFramework(startupConfig, appPaths); + StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Log uncaught exceptions to the logging instead of std error @@ -158,14 +153,14 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string? webContentPath = appPaths.WebPath; + var webContentPath = appPaths.WebPath; if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any()) { _logger.LogError( "The server is expected to host the web client, but the provided content directory is either " + "invalid or empty: {WebContentPath}. If you do not want to host the web client with the " + "server, you may set the '--nowebclient' command line flag, or set" + - "'{ConfigKey}=false' in your config settings.", + "'{ConfigKey}=false' in your config settings", webContentPath, HostWebClientKey); Environment.ExitCode = 1; @@ -173,20 +168,36 @@ namespace Jellyfin.Server } } - PerformStaticInitialization(); + StartupHelpers.PerformStaticInitialization(); Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory); + do + { + _restartOnShutdown = false; + await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); + + if (_restartOnShutdown) + { + _tokenSource = new CancellationTokenSource(); + _startTimestamp = Stopwatch.GetTimestamp(); + } + } while (_restartOnShutdown); + } + + private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) + { var appHost = new CoreAppHost( appPaths, _loggerFactory, options, startupConfig); + IHost? host = null; try { - var host = Host.CreateDefaultBuilder() + host = Host.CreateDefaultBuilder() .ConfigureServices(services => appHost.Init(services)) - .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths)) + .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) .UseSerilog() .Build(); @@ -203,20 +214,20 @@ namespace Jellyfin.Server if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { - var socketPath = GetUnixSocketPath(startupConfig, appPaths); + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); - SetUnixSocketPermissions(startupConfig, socketPath); + StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); } } catch (Exception ex) when (ex is not TaskCanceledException) { - _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again."); + _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again"); throw; } await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); - _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(startTimestamp)); + _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); // Block main thread until shutdown await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); @@ -227,7 +238,7 @@ namespace Jellyfin.Server } catch (Exception ex) { - _logger.LogCritical(ex, "Error while starting server."); + _logger.LogCritical(ex, "Error while starting server"); } finally { @@ -247,300 +258,7 @@ namespace Jellyfin.Server } await appHost.DisposeAsync().ConfigureAwait(false); - } - - if (_restartOnShutdown) - { - StartNewInstance(options); - } - } - - /// - /// Call static initialization methods for the application. - /// - public static void PerformStaticInitialization() - { - // Make sure we have all the code pages we can get - // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - - // Increase the max http request limit - // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - - // Disable the "Expect: 100-Continue" header by default - // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c - ServicePointManager.Expect100Continue = false; - - Batteries_V2.Init(); - } - - /// - /// Configure the web host builder. - /// - /// The builder to configure. - /// The application host. - /// The application configuration. - /// The application paths. - /// The configured web host builder. - public static IWebHostBuilder ConfigureWebHostBuilder( - this IWebHostBuilder builder, - CoreAppHost appHost, - IConfiguration startupConfig, - IApplicationPaths appPaths) - { - return builder - .UseKestrel((builderContext, options) => - { - var addresses = appHost.NetManager.GetAllBindInterfaces(); - - bool flagged = false; - foreach (IPObject netAdd in addresses) - { - _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd); - options.Listen(netAdd.Address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen( - netAdd.Address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen( - netAdd.Address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps()); - } - catch (InvalidOperationException) - { - if (!flagged) - { - _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); - flagged = true; - } - } - } - } - - // Bind to unix socket (only on unix systems) - if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) - { - var socketPath = GetUnixSocketPath(startupConfig, appPaths); - - // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 - if (File.Exists(socketPath)) - { - File.Delete(socketPath); - } - - options.ListenUnixSocket(socketPath); - _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); - } - }) - .UseStartup(_ => new Startup(appHost)); - } - - /// - /// Create the data, config and log paths from the variety of inputs(command line args, - /// environment variables) or decide on what default to use. For Windows it's %AppPath% - /// for everything else the - /// XDG approach - /// is followed. - /// - /// The for this instance. - /// . - private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) - { - // dataDir - // IF --datadir - // ELSE IF $JELLYFIN_DATA_DIR - // ELSE IF windows, use <%APPDATA%>/jellyfin - // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin - // ELSE use $HOME/.local/share/jellyfin - var dataDir = options.DataDir; - if (string.IsNullOrEmpty(dataDir)) - { - dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); - - if (string.IsNullOrEmpty(dataDir)) - { - // LocalApplicationData follows the XDG spec on unix machines - dataDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "jellyfin"); - } - } - - // configDir - // IF --configdir - // ELSE IF $JELLYFIN_CONFIG_DIR - // ELSE IF --datadir, use /config (assume portable run) - // ELSE IF /config exists, use that - // ELSE IF windows, use /config - // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin - // ELSE $HOME/.config/jellyfin - var configDir = options.ConfigDir; - if (string.IsNullOrEmpty(configDir)) - { - configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); - - if (string.IsNullOrEmpty(configDir)) - { - if (options.DataDir is not null - || Directory.Exists(Path.Combine(dataDir, "config")) - || OperatingSystem.IsWindows()) - { - // Hang config folder off already set dataDir - configDir = Path.Combine(dataDir, "config"); - } - else - { - // $XDG_CONFIG_HOME defines the base directory relative to which - // user specific configuration files should be stored. - configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); - - // If $XDG_CONFIG_HOME is either not set or empty, - // a default equal to $HOME /.config should be used. - if (string.IsNullOrEmpty(configDir)) - { - configDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".config"); - } - - configDir = Path.Combine(configDir, "jellyfin"); - } - } - } - - // cacheDir - // IF --cachedir - // ELSE IF $JELLYFIN_CACHE_DIR - // ELSE IF windows, use /cache - // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin - // ELSE HOME/.cache/jellyfin - var cacheDir = options.CacheDir; - if (string.IsNullOrEmpty(cacheDir)) - { - cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); - - if (string.IsNullOrEmpty(cacheDir)) - { - if (OperatingSystem.IsWindows()) - { - // Hang cache folder off already set dataDir - cacheDir = Path.Combine(dataDir, "cache"); - } - else - { - // $XDG_CACHE_HOME defines the base directory relative to which - // user specific non-essential data files should be stored. - cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); - - // If $XDG_CACHE_HOME is either not set or empty, - // a default equal to $HOME/.cache should be used. - if (string.IsNullOrEmpty(cacheDir)) - { - cacheDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cache"); - } - - cacheDir = Path.Combine(cacheDir, "jellyfin"); - } - } - } - - // webDir - // IF --webdir - // ELSE IF $JELLYFIN_WEB_DIR - // ELSE /jellyfin-web - var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) - { - webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); - - if (string.IsNullOrEmpty(webDir)) - { - // Use default location under ResourcesPath - webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); - } - } - - // logDir - // IF --logdir - // ELSE IF $JELLYFIN_LOG_DIR - // ELSE IF --datadir, use /log (assume portable run) - // ELSE /log - var logDir = options.LogDir; - if (string.IsNullOrEmpty(logDir)) - { - logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); - - if (string.IsNullOrEmpty(logDir)) - { - // Hang log folder off already set dataDir - logDir = Path.Combine(dataDir, "log"); - } - } - - // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162 - dataDir = Path.GetFullPath(dataDir); - logDir = Path.GetFullPath(logDir); - configDir = Path.GetFullPath(configDir); - cacheDir = Path.GetFullPath(cacheDir); - webDir = Path.GetFullPath(webDir); - - // Ensure the main folders exist before we continue - try - { - Directory.CreateDirectory(dataDir); - Directory.CreateDirectory(logDir); - Directory.CreateDirectory(configDir); - Directory.CreateDirectory(cacheDir); - } - catch (IOException ex) - { - Console.Error.WriteLine("Error whilst attempting to create folder"); - Console.Error.WriteLine(ex.ToString()); - Environment.Exit(1); - } - - return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); - } - - /// - /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist - /// already. - /// - /// The application paths. - /// A task representing the creation of the configuration file, or a completed task if the file already exists. - public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) - { - // Do nothing if the config file already exists - string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); - if (File.Exists(configPath)) - { - return; - } - - // Get a stream of the resource contents - // NOTE: The .csproj name is used instead of the assembly name in the resource path - const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; - Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) - ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); - await using (resource.ConfigureAwait(false)) - { - Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await using (dst.ConfigureAwait(false)) - { - // Copy the resource contents to the expected file path for the config file - await resource.CopyToAsync(dst).ConfigureAwait(false); - } + host?.Dispose(); } } @@ -578,112 +296,5 @@ namespace Jellyfin.Server .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } - - /// - /// Initialize Serilog using configuration and fall back to defaults on failure. - /// - private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) - { - try - { - // Serilog.Log is used by SerilogLoggerFactory when no logger is specified - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .CreateLogger(); - } - catch (Exception ex) - { - Log.Logger = new LoggerConfiguration() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}", - formatProvider: CultureInfo.InvariantCulture) - .WriteTo.Async(x => x.File( - Path.Combine(appPaths.LogDirectoryPath, "log_.log"), - rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", - formatProvider: CultureInfo.InvariantCulture, - encoding: Encoding.UTF8)) - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .CreateLogger(); - - Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); - } - } - - private static void StartNewInstance(StartupOptions options) - { - _logger.LogInformation("Starting new instance"); - - var module = options.RestartPath; - - if (string.IsNullOrWhiteSpace(module)) - { - module = Environment.GetCommandLineArgs()[0]; - } - - string commandLineArgsString; - if (options.RestartArgs is not null) - { - commandLineArgsString = options.RestartArgs; - } - else - { - commandLineArgsString = string.Join( - ' ', - Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument)); - } - - _logger.LogInformation("Executable: {0}", module); - _logger.LogInformation("Arguments: {0}", commandLineArgsString); - - Process.Start(module, commandLineArgsString); - } - - private static string NormalizeCommandLineArgument(string arg) - { - if (!arg.Contains(' ', StringComparison.Ordinal)) - { - return arg; - } - - return "\"" + arg + "\""; - } - - private static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths) - { - var socketPath = startupConfig.GetUnixSocketPath(); - - if (string.IsNullOrEmpty(socketPath)) - { - var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); - var socketFile = "jellyfin.sock"; - if (xdgRuntimeDir is null) - { - // Fall back to config dir - socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile); - } - else - { - socketPath = Path.Join(xdgRuntimeDir, socketFile); - } - } - - return socketPath; - } - - [UnsupportedOSPlatform("windows")] - private static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath) - { - var socketPerms = startupConfig.GetUnixSocketPermissions(); - - if (!string.IsNullOrEmpty(socketPerms)) - { - File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8)); - _logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms); - } - } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index f89f81c76..0062b8c05 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; +using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; @@ -12,7 +13,6 @@ using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; -using Jellyfin.Server.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 0d9f379e0..c3989751c 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -63,14 +63,6 @@ namespace Jellyfin.Server [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] public string? PackageName { get; set; } - /// - [Option("restartpath", Required = false, HelpText = "Path to restart script.")] - public string? RestartPath { get; set; } - - /// - [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] - public string? RestartArgs { get; set; } - /// [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")] public string? PublishedServerUrl { get; set; } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 53683cdbd..96ee701b3 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -47,12 +47,6 @@ namespace MediaBrowser.Common /// true if this instance is shutting down; otherwise, false. bool IsShuttingDown { get; } - /// - /// Gets a value indicating whether this instance can self restart. - /// - /// true if this instance can self restart; otherwise, false. - bool CanSelfRestart { get; } - /// /// Gets the application version. /// diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index 176bcbbd5..fa92d383a 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -29,6 +29,11 @@ namespace MediaBrowser.Common.Plugins /// An IEnumerable{Assembly}. IEnumerable LoadAssemblies(); + /// + /// Unloads all of the assemblies. + /// + void UnloadAssemblies(); + /// /// Registers the plugin's services with the DI. /// Note: DI is not yet instantiated yet. diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index a82c1c8c0..9e56849c7 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -79,8 +79,9 @@ namespace MediaBrowser.Model.System /// /// Gets or sets a value indicating whether this instance can self restart. /// - /// true if this instance can self restart; otherwise, false. - public bool CanSelfRestart { get; set; } + /// true. + [Obsolete("This is always true")] + public bool CanSelfRestart { get; set; } = true; public bool CanLaunchWebBrowser { get; set; } diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 1bfa5996d..55bc43455 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.IO; using System.Threading; using Emby.Server.Implementations; +using Jellyfin.Server.Extensions; +using Jellyfin.Server.Helpers; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -11,6 +13,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; @@ -33,7 +36,7 @@ namespace Jellyfin.Server.Integration.Tests Log.Logger = new LoggerConfiguration() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) .CreateLogger(); - Program.PerformStaticInitialization(); + StartupHelpers.PerformStaticInitialization(); } /// @@ -63,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests // Create the logging config file // TODO: We shouldn't need to do this since we are only logging to console - Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); + StartupHelpers.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); // Create a copy of the application configuration to use for startup var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); @@ -81,7 +84,7 @@ namespace Jellyfin.Server.Integration.Tests _disposableComponents.Add(appHost); builder.ConfigureServices(services => appHost.Init(services)) - .ConfigureWebHostBuilder(appHost, startupConfig, appPaths) + .ConfigureWebHostBuilder(appHost, startupConfig, appPaths, NullLogger.Instance) .ConfigureAppConfiguration((context, builder) => { builder diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs index d15c9d6f5..797fc8f64 100644 --- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs +++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Jellyfin.Server.Middleware; +using Jellyfin.Api.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives;