From 1e7acec01799e3cfe6fd2a8630dbd8f6e3338251 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:31:01 +0000 Subject: Added Setup overlay app to communicate status of startup --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 131 ++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 Jellyfin.Server/ServerSetupApp/SetupServer.cs (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs new file mode 100644 index 0000000000..61fe0fdd8c --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using SQLitePCL; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +/// Creates a fake application pipeline that will only exist for as long as the main app is not started. +/// +public sealed class SetupServer : IDisposable +{ + private IHost? _startupServer; + private bool _disposed; + + /// + /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// + /// The networkmanager. + /// The application paths. + /// A Task. + public async Task RunAsync(Func networkManagerFactory, IApplicationPaths applicationPaths) + { + ThrowIfDisposed(); + _startupServer = Host.CreateDefaultBuilder() + .UseConsoleLifetime() + .ConfigureServices(serv => + { + serv.AddHealthChecks() + .AddCheck("StartupCheck"); + }) + .ConfigureWebHostDefaults(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .Configure(app => + { + app.UseHealthChecks("/health"); + + app.Map("/startup/logger", loggerRoute => + { + loggerRoute.Run(async context => + { + var networkManager = networkManagerFactory(); + if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + + var logfilePath = Directory.EnumerateFiles(applicationPaths.LogDirectoryPath).Select(e => new FileInfo(e)).OrderBy(f => f.CreationTimeUtc).FirstOrDefault()?.FullName; + if (logfilePath is not null) + { + await context.Response.SendFileAsync(logfilePath, CancellationToken.None).ConfigureAwait(false); + } + }); + }); + + app.Run((context) => + { + context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.WriteAsync("

Jellyfin Server still starting. Please wait.

"); + var networkManager = networkManagerFactory(); + if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) + { + context.Response.WriteAsync("

You can download the current logfiles here.

"); + } + + return Task.CompletedTask; + }); + }); + }) + .Build(); + await _startupServer.StartAsync().ConfigureAwait(false); + } + + /// + /// Stops the Setup server. + /// + /// A task. Duh. + public async Task StopAsync() + { + ThrowIfDisposed(); + if (_startupServer is null) + { + throw new InvalidOperationException("Tried to stop a non existing startup server"); + } + + await _startupServer.StopAsync().ConfigureAwait(false); + _startupServer.Dispose(); + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _startupServer?.Dispose(); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private class SetupHealthcheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up.")); + } + } +} -- cgit v1.2.3 From dc029d549c0da8e0747d46f51a06621a16eb61df Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 11:08:20 +0000 Subject: removed double dispose --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 61fe0fdd8c..fc0680e40f 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -101,7 +101,6 @@ public sealed class SetupServer : IDisposable } await _startupServer.StopAsync().ConfigureAwait(false); - _startupServer.Dispose(); } /// -- cgit v1.2.3 From 41c27d4e7e197308f3ff978c59e538028bbf4ef4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 16:59:12 +0000 Subject: ATV requested endpoint mock --- Jellyfin.Server/Program.cs | 18 ++++++----- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 43 ++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 9 deletions(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a3b16d6a7d..922a06802a 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static SetupServer? _setupServer = new(); - + private static CoreAppHost? _appHost; private static IHost? _jfHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; @@ -74,7 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); + await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -130,18 +130,19 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); } } while (_restartOnShutdown); } private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) { - using var appHost = new CoreAppHost( - appPaths, - _loggerFactory, - options, - startupConfig); + using CoreAppHost appHost = new CoreAppHost( + appPaths, + _loggerFactory, + options, + startupConfig); + _appHost = appHost; try { _jfHost = Host.CreateDefaultBuilder() @@ -215,6 +216,7 @@ namespace Jellyfin.Server } } + _appHost = null; _jfHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index fc0680e40f..ea4804753b 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; @@ -31,8 +33,12 @@ public sealed class SetupServer : IDisposable /// /// The networkmanager. /// The application paths. + /// The servers application host. /// A Task. - public async Task RunAsync(Func networkManagerFactory, IApplicationPaths applicationPaths) + public async Task RunAsync( + Func networkManagerFactory, + IApplicationPaths applicationPaths, + Func serverApplicationHost) { ThrowIfDisposed(); _startupServer = Host.CreateDefaultBuilder() @@ -69,6 +75,41 @@ public sealed class SetupServer : IDisposable }); }); + app.Map("/System/Info/Public", systemRoute => + { + systemRoute.Run(async context => + { + var jfApplicationHost = serverApplicationHost(); + + var retryCounter = 0; + while (jfApplicationHost is null && retryCounter < 5) + { + await Task.Delay(500).ConfigureAwait(false); + jfApplicationHost = serverApplicationHost(); + retryCounter++; + } + + if (jfApplicationHost is null) + { + context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + return; + } + + var sysInfo = new PublicSystemInfo + { + Version = jfApplicationHost.ApplicationVersionString, + ProductName = jfApplicationHost.Name, + Id = jfApplicationHost.SystemId, + ServerName = jfApplicationHost.FriendlyName, + LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request), + StartupWizardCompleted = false + }; + + await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false); + }); + }); + app.Run((context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; -- cgit v1.2.3 From 7735aafef5908f2d15f6dc2b3da3bb3795fd83d2 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:05:47 +0000 Subject: renaming of jfHost usings cleanup --- Jellyfin.Server/Program.cs | 17 ++++++++--------- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 3 --- 2 files changed, 8 insertions(+), 12 deletions(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8523639e77..3d92caac46 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; -using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; @@ -47,7 +46,7 @@ namespace Jellyfin.Server private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static SetupServer _setupServer = new(); private static CoreAppHost? _appHost; - private static IHost? _jfHost = null; + private static IHost? _jellyfinHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -74,7 +73,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -130,7 +129,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); } } while (_restartOnShutdown); } @@ -145,7 +144,7 @@ namespace Jellyfin.Server _appHost = appHost; try { - _jfHost = Host.CreateDefaultBuilder() + _jellyfinHost = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => @@ -162,7 +161,7 @@ namespace Jellyfin.Server .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = _jfHost.Services; + appHost.ServiceProvider = _jellyfinHost.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); @@ -172,7 +171,7 @@ namespace Jellyfin.Server await _setupServer.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; - await _jfHost.StartAsync().ConfigureAwait(false); + await _jellyfinHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -191,7 +190,7 @@ namespace Jellyfin.Server _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - await _jfHost.WaitForShutdownAsync().ConfigureAwait(false); + await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) @@ -217,7 +216,7 @@ namespace Jellyfin.Server } _appHost = null; - _jfHost?.Dispose(); + _jellyfinHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index ea4804753b..09b7434eff 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -4,19 +4,16 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; -using SQLitePCL; namespace Jellyfin.Server.ServerSetupApp; -- cgit v1.2.3 From 963f2357a966dd7a5a6ab248155cc52ce066753b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:06:28 +0000 Subject: simplified logfile path --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 09b7434eff..9e2cf5bc8b 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -64,10 +64,14 @@ public sealed class SetupServer : IDisposable return; } - var logfilePath = Directory.EnumerateFiles(applicationPaths.LogDirectoryPath).Select(e => new FileInfo(e)).OrderBy(f => f.CreationTimeUtc).FirstOrDefault()?.FullName; - if (logfilePath is not null) + var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath) + .EnumerateFiles() + .OrderBy(f => f.CreationTimeUtc) + .FirstOrDefault() + ?.FullName; + if (logFilePath is not null) { - await context.Response.SendFileAsync(logfilePath, CancellationToken.None).ConfigureAwait(false); + await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false); } }); }); -- cgit v1.2.3 From 7df6e0b16f8e6b3026955e31417906b3bcbba290 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 19 Apr 2025 22:08:24 +0300 Subject: Add port awareness to startup server (#13913) --- .../Extensions/WebHostBuilderExtensions.cs | 116 ++++++--- Jellyfin.Server/Program.cs | 16 +- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 74 +++++- src/Jellyfin.Networking/Manager/NetworkManager.cs | 265 ++++++++++++--------- 4 files changed, 304 insertions(+), 167 deletions(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 7695c0d9ee..be9cf0f154 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Security.Cryptography.X509Certificates; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -35,56 +39,98 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(false); + SetupJellyfinWebServer( + appHost.NetManager.GetAllBindInterfaces(false), + appHost.HttpPort, + appHost.ListenWithHttps ? appHost.HttpsPort : null, + appHost.Certificate, + startupConfig, + appPaths, + logger, + builderContext, + options); + }) + .UseStartup(context => new Startup(appHost, context.Configuration)); + } - bool flagged = false; - foreach (var netAdd in addresses) + /// + /// Configures a Kestrel type webServer to bind to the specific arguments. + /// + /// The IP addresses that should be listend to. + /// The http port. + /// If set the https port. If set you must also set the certificate. + /// The certificate used for https port. + /// The startup config. + /// The app paths. + /// A logger. + /// The kestrel build pipeline context. + /// The kestrel server options. + /// Will be thrown when a https port is set but no or an invalid certificate is provided. + public static void SetupJellyfinWebServer( + IReadOnlyList addresses, + int httpPort, + int? httpsPort, + X509Certificate2? certificate, + IConfiguration startupConfig, + IApplicationPaths appPaths, + ILogger logger, + WebHostBuilderContext builderContext, + KestrelServerOptions options) + { + bool flagged = false; + foreach (var netAdd in addresses) + { + var address = netAdd.Address; + logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address); + options.Listen(netAdd.Address, httpPort); + if (httpsPort.HasValue) + { + if (builderContext.HostingEnvironment.IsDevelopment()) { - var address = netAdd.Address; - logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address); - options.Listen(netAdd.Address, appHost.HttpPort); - if (appHost.ListenWithHttps) + try { options.Listen( address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); + httpsPort.Value, + listenOptions => listenOptions.UseHttps()); } - else if (builderContext.HostingEnvironment.IsDevelopment()) + catch (InvalidOperationException) { - try + if (!flagged) { - options.Listen( - 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; - } + 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) + else { - var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); - - // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 - if (File.Exists(socketPath)) + if (certificate is null) { - File.Delete(socketPath); + throw new InvalidOperationException("Cannot run jellyfin with https without setting a valid certificate."); } - options.ListenUnixSocket(socketPath); - logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + options.Listen( + address, + httpsPort.Value, + listenOptions => listenOptions.UseHttps(certificate)); } - }) - .UseStartup(context => new Startup(appHost, context.Configuration)); + } + } + + // 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); + } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e661d0d4ab..55a4a00878 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static SetupServer _setupServer = new(); + private static SetupServer? _setupServer; private static CoreAppHost? _appHost; private static IHost? _jellyfinHost = null; private static long _startTimestamp; @@ -75,7 +75,6 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -88,7 +87,8 @@ namespace Jellyfin.Server // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); - + _setupServer = new SetupServer(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost, _loggerFactory, startupConfig); + await _setupServer.RunAsync().ConfigureAwait(false); StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); @@ -130,10 +130,12 @@ namespace Jellyfin.Server if (_restartOnShutdown) { _startTimestamp = Stopwatch.GetTimestamp(); - _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.StopAsync().ConfigureAwait(false); + await _setupServer.RunAsync().ConfigureAwait(false); } } while (_restartOnShutdown); + + _setupServer.Dispose(); } private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) @@ -170,9 +172,7 @@ namespace Jellyfin.Server try { - await _setupServer.StopAsync().ConfigureAwait(false); - _setupServer.Dispose(); - _setupServer = null!; + await _setupServer!.StopAsync().ConfigureAwait(false); await _jellyfinHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 9e2cf5bc8b..3d4810bd7f 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -4,6 +4,9 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.Configuration; +using Emby.Server.Implementations.Serialization; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -11,9 +14,12 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Jellyfin.Server.ServerSetupApp; @@ -22,20 +28,45 @@ namespace Jellyfin.Server.ServerSetupApp; /// public sealed class SetupServer : IDisposable { + private readonly Func _networkManagerFactory; + private readonly IApplicationPaths _applicationPaths; + private readonly Func _serverFactory; + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _startupConfiguration; + private readonly ServerConfigurationManager _configurationManager; private IHost? _startupServer; private bool _disposed; /// - /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// Initializes a new instance of the class. /// /// The networkmanager. /// The application paths. - /// The servers application host. - /// A Task. - public async Task RunAsync( + /// The servers application host. + /// The logger factory. + /// The startup configuration. + public SetupServer( Func networkManagerFactory, IApplicationPaths applicationPaths, - Func serverApplicationHost) + Func serverApplicationHostFactory, + ILoggerFactory loggerFactory, + IConfiguration startupConfiguration) + { + _networkManagerFactory = networkManagerFactory; + _applicationPaths = applicationPaths; + _serverFactory = serverApplicationHostFactory; + _loggerFactory = loggerFactory; + _startupConfiguration = startupConfiguration; + var xmlSerializer = new MyXmlSerializer(); + _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer); + _configurationManager.RegisterConfiguration(); + } + + /// + /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// + /// A Task. + public async Task RunAsync() { ThrowIfDisposed(); _startupServer = Host.CreateDefaultBuilder() @@ -48,7 +79,23 @@ public sealed class SetupServer : IDisposable .ConfigureWebHostDefaults(webHostBuilder => { webHostBuilder - .UseKestrel() + .UseKestrel((builderContext, options) => + { + var config = _configurationManager.GetNetworkConfiguration()!; + var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger(), config.EnableIPv4, config.EnableIPv6); + knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6); + var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6); + Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer( + bindInterfaces, + config.InternalHttpPort, + null, + null, + _startupConfiguration, + _applicationPaths, + _loggerFactory.CreateLogger(), + builderContext, + options); + }) .Configure(app => { app.UseHealthChecks("/health"); @@ -57,14 +104,14 @@ public sealed class SetupServer : IDisposable { loggerRoute.Run(async context => { - var networkManager = networkManagerFactory(); + var networkManager = _networkManagerFactory(); if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } - var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath) + var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath) .EnumerateFiles() .OrderBy(f => f.CreationTimeUtc) .FirstOrDefault() @@ -80,20 +127,20 @@ public sealed class SetupServer : IDisposable { systemRoute.Run(async context => { - var jfApplicationHost = serverApplicationHost(); + var jfApplicationHost = _serverFactory(); var retryCounter = 0; while (jfApplicationHost is null && retryCounter < 5) { await Task.Delay(500).ConfigureAwait(false); - jfApplicationHost = serverApplicationHost(); + jfApplicationHost = _serverFactory(); retryCounter++; } if (jfApplicationHost is null) { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.Headers.RetryAfter = new StringValues("5"); return; } @@ -114,9 +161,10 @@ public sealed class SetupServer : IDisposable app.Run((context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.Headers.RetryAfter = new StringValues("5"); + context.Response.Headers.ContentType = new StringValues("text/html"); context.Response.WriteAsync("

Jellyfin Server still starting. Please wait.

"); - var networkManager = networkManagerFactory(); + var networkManager = _networkManagerFactory(); if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) { context.Response.WriteAsync("

You can download the current logfiles here.

"); diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 6f6ee51461..80a5741df9 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; +using J2N.Collections.Generic.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; @@ -214,81 +215,92 @@ public class NetworkManager : INetworkManager, IDisposable { lock (_initLock) { - _logger.LogDebug("Refreshing interfaces."); + _interfaces = GetInterfacesCore(_logger, IsIPv4Enabled, IsIPv6Enabled).ToList(); + } + } - var interfaces = new List(); + /// + /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. + /// + /// The logger. + /// If true evaluates IPV4 type ip addresses. + /// If true evaluates IPV6 type ip addresses. + /// A list of all locally known up addresses and submasks that are to be considered usable. + public static IReadOnlyList GetInterfacesCore(ILogger logger, bool isIPv4Enabled, bool isIPv6Enabled) + { + logger.LogDebug("Refreshing interfaces."); - try - { - var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.OperationalStatus == OperationalStatus.Up); + var interfaces = new List(); - foreach (NetworkInterface adapter in nics) + try + { + var nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) + { + try { - try - { - var ipProperties = adapter.GetIPProperties(); + var ipProperties = adapter.GetIPProperties(); - // Populate interface list - foreach (var info in ipProperties.UnicastAddresses) + // Populate interface list + foreach (var info in ipProperties.UnicastAddresses) + { + if (isIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) - { - Index = ipProperties.GetIPv4Properties().Index, - Name = adapter.Name, - SupportsMulticast = adapter.SupportsMulticast - }; - - interfaces.Add(interfaceObject); - } - else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + Index = ipProperties.GetIPv4Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; + + interfaces.Add(interfaceObject); + } + else if (isIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) - { - Index = ipProperties.GetIPv6Properties().Index, - Name = adapter.Name, - SupportsMulticast = adapter.SupportsMulticast - }; - - interfaces.Add(interfaceObject); - } + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; + + interfaces.Add(interfaceObject); } } - catch (Exception ex) - { - // Ignore error, and attempt to continue. - _logger.LogError(ex, "Error encountered parsing interfaces."); - } + } + catch (Exception ex) + { + // Ignore error, and attempt to continue. + logger.LogError(ex, "Error encountered parsing interfaces."); } } - catch (Exception ex) + } + catch (Exception ex) + { + logger.LogError(ex, "Error obtaining interfaces."); + } + + // If no interfaces are found, fallback to loopback interfaces. + if (interfaces.Count == 0) + { + logger.LogWarning("No interface information available. Using loopback interface(s)."); + + if (isIPv4Enabled) { - _logger.LogError(ex, "Error obtaining interfaces."); + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } - // If no interfaces are found, fallback to loopback interfaces. - if (interfaces.Count == 0) + if (isIPv6Enabled) { - _logger.LogWarning("No interface information available. Using loopback interface(s)."); - - if (IsIPv4Enabled) - { - interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); - } - - if (IsIPv6Enabled) - { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); - } + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } - - _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count); - _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); - - _interfaces = interfaces; } + + logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count); + logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); + return interfaces; } /// @@ -345,64 +357,76 @@ public class NetworkManager : INetworkManager, IDisposable { lock (_initLock) { - // Respect explicit bind addresses - var interfaces = _interfaces.ToList(); - var localNetworkAddresses = config.LocalNetworkAddresses; - if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) - { - var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) - ? network.Prefix - : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Address) - .FirstOrDefault() ?? IPAddress.None)) - .Where(x => x != IPAddress.None) - .ToHashSet(); - interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); - - if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) - { - interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); - } - - if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) - { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); - } - } + _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList(); + } + } - // Remove all interfaces matching any virtual machine interface prefix - if (config.IgnoreVirtualInterfaces) + /// + /// Filteres a list of bind addresses and exclusions on available interfaces. + /// + /// The network config to be filtered by. + /// A list of possible interfaces to be filtered. + /// If true evaluates IPV4 type ip addresses. + /// If true evaluates IPV6 type ip addresses. + /// A list of all locally known up addresses and submasks that are to be considered usable. + public static IReadOnlyList FilterBindSettings(NetworkConfiguration config, IList interfaces, bool isIPv4Enabled, bool isIPv6Enabled) + { + // Respect explicit bind addresses + var localNetworkAddresses = config.LocalNetworkAddresses; + if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) + { + var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) + ? network.Prefix + : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Address) + .FirstOrDefault() ?? IPAddress.None)) + .Where(x => x != IPAddress.None) + .ToHashSet(); + interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); + + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { - // Remove potentially existing * and split config string into prefixes - var virtualInterfacePrefixes = config.VirtualInterfaceNames - .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); - - // Check all interfaces for matches against the prefixes and remove them - if (_interfaces.Count > 0) - { - foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) - { - interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); - } - } + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); } - // Remove all IPv4 interfaces if IPv4 is disabled - if (!IsIPv4Enabled) + if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { - interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); } + } + + // Remove all interfaces matching any virtual machine interface prefix + if (config.IgnoreVirtualInterfaces) + { + // Remove potentially existing * and split config string into prefixes + var virtualInterfacePrefixes = config.VirtualInterfaceNames + .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); - // Remove all IPv6 interfaces if IPv6 is disabled - if (!IsIPv6Enabled) + // Check all interfaces for matches against the prefixes and remove them + if (interfaces.Count > 0) { - interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); + foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) + { + interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); + } } + } - // Users may have complex networking configuration that multiple interfaces sharing the same IP address - // Only return one IP for binding, and let the OS handle the rest - _interfaces = interfaces.DistinctBy(iface => iface.Address).ToList(); + // Remove all IPv4 interfaces if IPv4 is disabled + if (!isIPv4Enabled) + { + interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); } + + // Remove all IPv6 interfaces if IPv6 is disabled + if (!isIPv6Enabled) + { + interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); + } + + // Users may have complex networking configuration that multiple interfaces sharing the same IP address + // Only return one IP for binding, and let the OS handle the rest + return interfaces.DistinctBy(iface => iface.Address).ToList(); } /// @@ -720,28 +744,47 @@ public class NetworkManager : INetworkManager, IDisposable /// public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { - var config = _configurationManager.GetNetworkConfiguration(); + return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); + } + + /// + /// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound. + /// + /// Defines that only known interfaces should be used. + /// The ConfigurationManager. + /// The known interfaces that gets returned if possible or instructed. + /// Include IPV4 type interfaces. + /// Include IPV6 type interfaces. + /// A list of ip address of which jellyfin should bind to. + public static IReadOnlyList GetAllBindInterfaces( + bool individualInterfaces, + IConfigurationManager configurationManager, + IReadOnlyList knownInterfaces, + bool readIpv4, + bool readIpv6) + { + var config = configurationManager.GetNetworkConfiguration(); var localNetworkAddresses = config.LocalNetworkAddresses; - if ((localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]) && _interfaces.Count > 0) || individualInterfaces) + if ((localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]) && knownInterfaces.Count > 0) || individualInterfaces) { - return _interfaces; + return knownInterfaces; } // No bind address and no exclusions, so listen on all interfaces. var result = new List(); - if (IsIPv4Enabled && IsIPv6Enabled) + if (readIpv4 && readIpv6) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any)); } - else if (IsIPv4Enabled) + else if (readIpv4) { result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any)); } - else if (IsIPv6Enabled) + else if (readIpv6) { // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too. - foreach (var iface in _interfaces) + foreach (var iface in knownInterfaces) { if (iface.AddressFamily == AddressFamily.InterNetworkV6) { -- cgit v1.2.3 From 3cf213c4fb58784c75e51eaf89d6fe256040a276 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 1 May 2025 21:20:35 -0400 Subject: Fix startup logger log file order --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 3d4810bd7f..7ab5defc8a 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -113,7 +113,7 @@ public sealed class SetupServer : IDisposable var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath) .EnumerateFiles() - .OrderBy(f => f.CreationTimeUtc) + .OrderByDescending(f => f.CreationTimeUtc) .FirstOrDefault() ?.FullName; if (logFilePath is not null) -- cgit v1.2.3 From 88332e89c458266bc073d3304eafcb23603f15fa Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 5 Jun 2025 17:59:11 +0300 Subject: Feature/version check in library migration (#14105) --- Directory.Packages.props | 1 + Jellyfin.Server/Helpers/StartupHelpers.cs | 6 +- Jellyfin.Server/Jellyfin.Server.csproj | 4 + .../Migrations/JellyfinMigrationService.cs | 41 ++-- .../Migrations/Routines/MigrateKeyframeData.cs | 9 +- .../Migrations/Routines/MigrateLibraryDb.cs | 13 +- .../Routines/MigrateLibraryDbCompatibilityCheck.cs | 73 +++++++ .../Migrations/Routines/MigrateRatingLevels.cs | 7 +- .../Migrations/Routines/MoveExtractedFiles.cs | 7 +- .../Migrations/Routines/MoveTrickplayFiles.cs | 5 +- Jellyfin.Server/Migrations/Stages/CodeMigration.cs | 36 +++- Jellyfin.Server/Program.cs | 22 +- Jellyfin.Server/ServerSetupApp/IStartupLogger.cs | 25 +++ Jellyfin.Server/ServerSetupApp/SetupServer.cs | 174 +++++++++++++++- Jellyfin.Server/ServerSetupApp/StartupLogger.cs | 102 ++++++++++ .../ServerSetupApp/index.mstemplate.html | 225 +++++++++++++++++++++ .../JellyfinApplicationFactory.cs | 39 +++- 17 files changed, 734 insertions(+), 55 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs create mode 100644 Jellyfin.Server/ServerSetupApp/IStartupLogger.cs create mode 100644 Jellyfin.Server/ServerSetupApp/StartupLogger.cs create mode 100644 Jellyfin.Server/ServerSetupApp/index.mstemplate.html (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Directory.Packages.props b/Directory.Packages.props index d863d99fb6..a0a09d7456 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,6 +51,7 @@ + diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index bbf6d31f1f..93c9961664 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -3,18 +3,19 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; using Emby.Server.Implementations; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Serilog; +using Serilog.Extensions.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server.Helpers; @@ -257,11 +258,14 @@ public static class StartupHelpers { try { + var startupLogger = new LoggerProviderCollection(); + startupLogger.AddProvider(new SetupServer.SetupLoggerFactory()); // Serilog.Log is used by SerilogLoggerFactory when no logger is specified Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .Enrich.FromLogContext() .Enrich.WithThreadId() + .WriteTo.Async(e => e.Providers(startupLogger)) .CreateLogger(); } catch (Exception ex) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 452b03efbe..14c4285fe2 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -48,6 +48,7 @@ + @@ -79,6 +80,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs index 5c5c64ec38..5331b43e33 100644 --- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs +++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs @@ -10,13 +10,13 @@ using Emby.Server.Implementations.Serialization; using Jellyfin.Database.Implementations; using Jellyfin.Server.Implementations.SystemBackupService; using Jellyfin.Server.Migrations.Stages; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.SystemBackupService; using MediaBrowser.Model.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations; @@ -29,6 +29,7 @@ internal class JellyfinMigrationService private const string DbFilename = "library.db"; private readonly IDbContextFactory _dbContextFactory; private readonly ILoggerFactory _loggerFactory; + private readonly IStartupLogger _startupLogger; private readonly IBackupService? _backupService; private readonly IJellyfinDatabaseProvider? _jellyfinDatabaseProvider; private readonly IApplicationPaths _applicationPaths; @@ -39,18 +40,21 @@ internal class JellyfinMigrationService /// /// Provides access to the jellyfin database. /// The logger factory. + /// The startup logger for Startup UI intigration. /// Application paths for library.db backup. /// The jellyfin backup service. /// The jellyfin database provider. public JellyfinMigrationService( IDbContextFactory dbContextFactory, ILoggerFactory loggerFactory, + IStartupLogger startupLogger, IApplicationPaths applicationPaths, IBackupService? backupService = null, IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null) { _dbContextFactory = dbContextFactory; _loggerFactory = loggerFactory; + _startupLogger = startupLogger; _backupService = backupService; _jellyfinDatabaseProvider = jellyfinDatabaseProvider; _applicationPaths = applicationPaths; @@ -80,14 +84,14 @@ internal class JellyfinMigrationService private interface IInternalMigration { - Task PerformAsync(ILogger logger); + Task PerformAsync(IStartupLogger logger); } private HashSet Migrations { get; set; } public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths) { - var logger = _loggerFactory.CreateLogger(); + var logger = _startupLogger.With(_loggerFactory.CreateLogger()).BeginGroup($"Migration Startup"); logger.LogInformation("Initialise Migration service."); var xmlSerializer = new MyXmlSerializer(); var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath) @@ -173,8 +177,7 @@ internal class JellyfinMigrationService public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider) { - var logger = _loggerFactory.CreateLogger(); - logger.LogInformation("Migrate stage {Stage}.", stage); + var logger = _startupLogger.With(_loggerFactory.CreateLogger()).BeginGroup($"Migrate stage {stage}."); ICollection migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection) ?? []; var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); @@ -202,21 +205,23 @@ internal class JellyfinMigrationService foreach (var item in migrations) { + var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}"); try { - logger.LogInformation("Perform migration {Name}", item.Key); - await item.Migration.PerformAsync(_loggerFactory.CreateLogger(item.GetType().Name)).ConfigureAwait(false); - logger.LogInformation("Migration {Name} was successfully applied", item.Key); + migrationLogger.LogInformation("Perform migration {Name}", item.Key); + await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false); + migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key); } catch (Exception ex) { - logger.LogCritical(ex, "Migration {Name} failed, migration service will attempt to roll back.", item.Key); + migrationLogger.LogCritical("Error: {Error}", ex.Message); + migrationLogger.LogError(ex, "Migration {Name} failed", item.Key); if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null) { if (_backupKey.LibraryDb is not null) { - logger.LogInformation("Attempt to rollback librarydb."); + migrationLogger.LogInformation("Attempt to rollback librarydb."); try { var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename); @@ -224,33 +229,33 @@ internal class JellyfinMigrationService } catch (Exception inner) { - logger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb); + migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb); } } if (_backupKey.JellyfinDb is not null) { - logger.LogInformation("Attempt to rollback JellyfinDb."); + migrationLogger.LogInformation("Attempt to rollback JellyfinDb."); try { await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false); } catch (Exception inner) { - logger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb); + migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb); } } if (_backupKey.FullBackup is not null) { - logger.LogInformation("Attempt to rollback from backup."); + migrationLogger.LogInformation("Attempt to rollback from backup."); try { await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false); } catch (Exception inner) { - logger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path); + migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path); } } } @@ -416,9 +421,9 @@ internal class JellyfinMigrationService _dbContext = dbContext; } - public async Task PerformAsync(ILogger logger) + public async Task PerformAsync(IStartupLogger logger) { - await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false); + await _codeMigration.Perform(_serviceProvider, logger, CancellationToken.None).ConfigureAwait(false); var historyRepository = _dbContext.GetService(); var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion())); @@ -437,7 +442,7 @@ internal class JellyfinMigrationService _jellyfinDbContext = jellyfinDbContext; } - public async Task PerformAsync(ILogger logger) + public async Task PerformAsync(IStartupLogger logger) { var migrator = _jellyfinDbContext.GetService(); await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index 03a5212585..033045e639 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -9,6 +9,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions.Json; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using Microsoft.EntityFrameworkCore; @@ -22,7 +23,7 @@ namespace Jellyfin.Server.Migrations.Routines; [JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData))] public class MigrateKeyframeData : IDatabaseMigrationRoutine { - private readonly ILogger _logger; + private readonly IStartupLogger _logger; private readonly IApplicationPaths _appPaths; private readonly IDbContextFactory _dbProvider; private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; @@ -30,15 +31,15 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine /// /// Initializes a new instance of the class. /// - /// The logger. + /// The startup logger for Startup UI intigration. /// Instance of the interface. /// The EFCore db factory. public MigrateKeyframeData( - ILogger logger, + IStartupLogger startupLogger, IApplicationPaths appPaths, IDbContextFactory dbProvider) { - _logger = logger; + _logger = startupLogger; _appPaths = appPaths; _dbProvider = dbProvider; } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 309858ca70..521655a4f1 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -14,6 +14,7 @@ using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using Jellyfin.Server.Implementations.Item; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; @@ -34,7 +35,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine { private const string DbFilename = "library.db"; - private readonly ILogger _logger; + private readonly IStartupLogger _logger; private readonly IServerApplicationPaths _paths; private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; private readonly IDbContextFactory _provider; @@ -42,19 +43,17 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine /// /// Initializes a new instance of the class. /// - /// The logger. + /// The startup logger for Startup UI intigration. /// The database provider. /// The server application paths. /// The database provider for special access. - /// The Service provider. public MigrateLibraryDb( - ILogger logger, + IStartupLogger startupLogger, IDbContextFactory provider, IServerApplicationPaths paths, - IJellyfinDatabaseProvider jellyfinDatabaseProvider, - IServiceProvider serviceProvider) + IJellyfinDatabaseProvider jellyfinDatabaseProvider) { - _logger = logger; + _logger = startupLogger; _provider = provider; _paths = paths; _jellyfinDatabaseProvider = jellyfinDatabaseProvider; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs new file mode 100644 index 0000000000..2d5fc2a0db --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs @@ -0,0 +1,73 @@ +#pragma warning disable RS0030 // Do not use banned APIs + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Server.ServerSetupApp; +using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// The migration routine for checking if the current instance of Jellyfin is compatiable to be upgraded. +/// +[JellyfinMigration("2025-04-20T19:30:00", nameof(MigrateLibraryDbCompatibilityCheck))] +public class MigrateLibraryDbCompatibilityCheck : IAsyncMigrationRoutine +{ + private const string DbFilename = "library.db"; + private readonly IStartupLogger _logger; + private readonly IServerApplicationPaths _paths; + + /// + /// Initializes a new instance of the class. + /// + /// The startup logger. + /// The Path service. + public MigrateLibraryDbCompatibilityCheck(IStartupLogger startupLogger, IServerApplicationPaths paths) + { + _logger = startupLogger; + _paths = paths; + } + + /// + public async Task PerformAsync(CancellationToken cancellationToken) + { + var dataPath = _paths.DataPath; + var libraryDbPath = Path.Combine(dataPath, DbFilename); + if (!File.Exists(libraryDbPath)) + { + _logger.LogError("Cannot migrate {LibraryDb} as it does not exist..", libraryDbPath); + return; + } + + using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + CheckMigratableVersion(connection); + await connection.CloseAsync().ConfigureAwait(false); + } + + private static void CheckMigratableVersion(SqliteConnection connection) + { + CheckColumnExistance(connection, "TypedBaseItems", "lufs"); + CheckColumnExistance(connection, "TypedBaseItems", "normalizationgain"); + CheckColumnExistance(connection, "mediastreams", "dvversionmajor"); + + static void CheckColumnExistance(SqliteConnection connection, string table, string column) + { + using (var cmd = connection.CreateCommand()) + { +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + cmd.CommandText = $"Select COUNT(1) FROM pragma_table_xinfo('{table}') WHERE lower(name) = '{column}';"; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + var result = cmd.ExecuteScalar()!; + if (!result.Equals(1L)) + { + throw new InvalidOperationException("Your database does not meet the required standard. Only upgrades from server version 10.9.11 or above are supported. Please upgrade first to server version 10.10.7 before attempting to upgrade afterwards to 10.11"); + } + } + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 9aed449882..ae93557de8 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Jellyfin.Database.Implementations; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Model.Globalization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -16,18 +17,18 @@ namespace Jellyfin.Server.Migrations.Routines; #pragma warning restore CS0618 // Type or member is obsolete internal class MigrateRatingLevels : IDatabaseMigrationRoutine { - private readonly ILogger _logger; + private readonly IStartupLogger _logger; private readonly IDbContextFactory _provider; private readonly ILocalizationManager _localizationManager; public MigrateRatingLevels( IDbContextFactory provider, - ILoggerFactory loggerFactory, + IStartupLogger logger, ILocalizationManager localizationManager) { _provider = provider; _localizationManager = localizationManager; - _logger = loggerFactory.CreateLogger(); + _logger = logger; } /// diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index 38952eec96..6f650f7313 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.IO; @@ -29,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines; public class MoveExtractedFiles : IAsyncMigrationRoutine { private readonly IApplicationPaths _appPaths; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IDbContextFactory _dbProvider; private readonly IPathManager _pathManager; private readonly IFileSystem _fileSystem; @@ -39,18 +40,20 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine /// /// Instance of the interface. /// The logger. + /// The startup logger for Startup UI intigration. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public MoveExtractedFiles( IApplicationPaths appPaths, ILogger logger, + IStartupLogger startupLogger, IPathManager pathManager, IFileSystem fileSystem, IDbContextFactory dbProvider) { _appPaths = appPaths; - _logger = logger; + _logger = startupLogger.With(logger); _pathManager = pathManager; _fileSystem = fileSystem; _dbProvider = dbProvider; diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index 63b0614fd4..a674aa928b 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using Jellyfin.Data.Enums; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; @@ -23,7 +24,7 @@ public class MoveTrickplayFiles : IMigrationRoutine private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly IStartupLogger _logger; /// /// Initializes a new instance of the class. @@ -36,7 +37,7 @@ public class MoveTrickplayFiles : IMigrationRoutine ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, - ILogger logger) + IStartupLogger logger) { _trickplayManager = trickplayManager; _fileSystem = fileSystem; diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs index addbb69bfd..47ed26965b 100644 --- a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs +++ b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs @@ -2,7 +2,9 @@ using System; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Server.ServerSetupApp; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Jellyfin.Server.Migrations.Stages; @@ -16,10 +18,34 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta public string BuildCodeMigrationId() { - return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!; + return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + Metadata.Name!; } - public async Task Perform(IServiceProvider? serviceProvider, CancellationToken cancellationToken) + private ServiceCollection MigrationServices(IServiceProvider serviceProvider, IStartupLogger logger) + { + var childServiceCollection = new ServiceCollection(); + childServiceCollection.AddSingleton(serviceProvider); + childServiceCollection.AddSingleton(logger); + + foreach (ServiceDescriptor service in serviceProvider.GetRequiredService()) + { + if (service.Lifetime == ServiceLifetime.Singleton && !service.ServiceType.IsGenericTypeDefinition) + { + object? serviceInstance = serviceProvider.GetService(service.ServiceType); + if (serviceInstance != null) + { + childServiceCollection.AddSingleton(service.ServiceType, serviceInstance); + continue; + } + } + + childServiceCollection.Add(service); + } + + return childServiceCollection; + } + + public async Task Perform(IServiceProvider? serviceProvider, IStartupLogger logger, CancellationToken cancellationToken) { #pragma warning disable CS0618 // Type or member is obsolete if (typeof(IMigrationRoutine).IsAssignableFrom(MigrationType)) @@ -30,7 +56,8 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta } else { - ((IMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).Perform(); + using var migrationServices = MigrationServices(serviceProvider, logger).BuildServiceProvider(); + ((IMigrationRoutine)ActivatorUtilities.CreateInstance(migrationServices, MigrationType)).Perform(); #pragma warning restore CS0618 // Type or member is obsolete } } @@ -42,7 +69,8 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta } else { - await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false); + using var migrationServices = MigrationServices(serviceProvider, logger).BuildServiceProvider(); + await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(migrationServices, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false); } } else diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 9f2c71ce25..0b77d63ac7 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -60,6 +60,7 @@ namespace Jellyfin.Server private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; + private static IStartupLogger? _migrationLogger; private static string? _restoreFromBackup; /// @@ -98,9 +99,9 @@ namespace Jellyfin.Server // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); + StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _setupServer = new SetupServer(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost, _loggerFactory, startupConfig); await _setupServer.RunAsync().ConfigureAwait(false); - StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Use the logging framework for uncaught exceptions instead of std error @@ -131,7 +132,7 @@ namespace Jellyfin.Server } } - StorageHelper.TestCommonPathsForStorageCapacity(appPaths, _loggerFactory.CreateLogger()); + StorageHelper.TestCommonPathsForStorageCapacity(appPaths, StartupLogger.Logger.With(_loggerFactory.CreateLogger()).BeginGroup($"Storage Check")); StartupHelpers.PerformStaticInitialization(); @@ -160,6 +161,7 @@ namespace Jellyfin.Server options, startupConfig); _appHost = appHost; + var configurationCompleted = false; try { _jellyfinHost = Host.CreateDefaultBuilder() @@ -176,6 +178,7 @@ namespace Jellyfin.Server }) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) .UseSerilog() + .ConfigureServices(e => e.AddTransient().AddSingleton(e)) .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. @@ -200,6 +203,7 @@ namespace Jellyfin.Server await jellyfinMigrationService.CleanupSystemAfterMigration(_logger).ConfigureAwait(false); try { + configurationCompleted = true; await _setupServer!.StopAsync().ConfigureAwait(false); await _jellyfinHost.StartAsync().ConfigureAwait(false); @@ -228,6 +232,12 @@ namespace Jellyfin.Server { _restartOnShutdown = false; _logger.LogCritical(ex, "Error while starting server"); + if (_setupServer!.IsAlive && !configurationCompleted) + { + _setupServer!.SoftStop(); + await Task.Delay(TimeSpan.FromMinutes(10)).ConfigureAwait(false); + await _setupServer!.StopAsync().ConfigureAwait(false); + } } finally { @@ -258,13 +268,17 @@ namespace Jellyfin.Server /// A task. public static async Task ApplyStartupMigrationAsync(ServerApplicationPaths appPaths, IConfiguration startupConfig) { + _migrationLogger = StartupLogger.Logger.BeginGroup($"Migration Service"); var startupConfigurationManager = new ServerConfigurationManager(appPaths, _loggerFactory, new MyXmlSerializer()); startupConfigurationManager.AddParts([new DatabaseConfigurationFactory()]); var migrationStartupServiceProvider = new ServiceCollection() .AddLogging(d => d.AddSerilog()) .AddJellyfinDbContext(startupConfigurationManager, startupConfig) .AddSingleton(appPaths) - .AddSingleton(appPaths); + .AddSingleton(appPaths) + .AddSingleton(_migrationLogger); + + migrationStartupServiceProvider.AddSingleton(migrationStartupServiceProvider); var startupService = migrationStartupServiceProvider.BuildServiceProvider(); PrepareDatabaseProvider(startupService); @@ -285,7 +299,7 @@ namespace Jellyfin.Server /// A task. public static async Task ApplyCoreMigrationsAsync(IServiceProvider serviceProvider, Migrations.Stages.JellyfinMigrationStageTypes jellyfinMigrationStage) { - var jellyfinMigrationService = ActivatorUtilities.CreateInstance(serviceProvider); + var jellyfinMigrationService = ActivatorUtilities.CreateInstance(serviceProvider, _migrationLogger!); await jellyfinMigrationService.MigrateStepAsync(jellyfinMigrationStage, serviceProvider).ConfigureAwait(false); } diff --git a/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs b/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs new file mode 100644 index 0000000000..2c2ef05f8a --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs @@ -0,0 +1,25 @@ +using System; +using Morestachio.Helper.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +/// Defines the Startup Logger. This logger acts an an aggregate logger that will push though all log messages to both the attached logger as well as the startup UI. +/// +public interface IStartupLogger : ILogger +{ + /// + /// Adds another logger instance to this logger for combined logging. + /// + /// Other logger to rely messages to. + /// A combined logger. + IStartupLogger With(ILogger logger); + + /// + /// Opens a new Group logger within the parent logger. + /// + /// Defines the log message that introduces the new group. + /// A new logger that can write to the group. + IStartupLogger BeginGroup(FormattableString logEntry); +} diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 7ab5defc8a..751cf7f428 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -10,6 +13,7 @@ using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; +using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -20,6 +24,9 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; +using Morestachio; +using Morestachio.Framework.IO.SingleStream; +using Morestachio.Rendering; namespace Jellyfin.Server.ServerSetupApp; @@ -34,8 +41,10 @@ public sealed class SetupServer : IDisposable private readonly ILoggerFactory _loggerFactory; private readonly IConfiguration _startupConfiguration; private readonly ServerConfigurationManager _configurationManager; + private IRenderer? _startupUiRenderer; private IHost? _startupServer; private bool _disposed; + private bool _isUnhealthy; /// /// Initializes a new instance of the class. @@ -62,13 +71,73 @@ public sealed class SetupServer : IDisposable _configurationManager.RegisterConfiguration(); } + internal static ConcurrentQueue? LogQueue { get; set; } = new(); + + /// + /// Gets a value indicating whether Startup server is currently running. + /// + public bool IsAlive { get; internal set; } + /// /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. /// /// A Task. public async Task RunAsync() { + var fileTemplate = await File.ReadAllTextAsync(Path.Combine("ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false); + _startupUiRenderer = (await ParserOptionsBuilder.New() + .WithTemplate(fileTemplate) + .WithFormatter( + (StartupLogEntry logEntry, IEnumerable children) => + { + if (children.Any()) + { + var maxLevel = logEntry.LogLevel; + var stack = new Stack(children); + + while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level. + { + maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel; + foreach (var child in logEntry.Children) + { + stack.Push(child); + } + } + + return maxLevel; + } + + return logEntry.LogLevel; + }, + "FormatLogLevel") + .WithFormatter( + (LogLevel logLevel) => + { + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + case LogLevel.None: + return "success"; + case LogLevel.Information: + return "info"; + case LogLevel.Warning: + return "warn"; + case LogLevel.Error: + return "danger"; + case LogLevel.Critical: + return "danger-strong"; + } + + return string.Empty; + }, + "ToString") + .BuildAndParseAsync() + .ConfigureAwait(false)) + .CreateCompiledRenderer(); + ThrowIfDisposed(); + var retryAfterValue = TimeSpan.FromSeconds(5); _startupServer = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(serv => @@ -140,7 +209,7 @@ public sealed class SetupServer : IDisposable if (jfApplicationHost is null) { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new StringValues("5"); + context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture)); return; } @@ -158,24 +227,30 @@ public sealed class SetupServer : IDisposable }); }); - app.Run((context) => + app.Run(async (context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new StringValues("5"); + context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture)); context.Response.Headers.ContentType = new StringValues("text/html"); - context.Response.WriteAsync("

Jellyfin Server still starting. Please wait.

"); var networkManager = _networkManagerFactory(); - if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) - { - context.Response.WriteAsync("

You can download the current logfiles here.

"); - } - return Task.CompletedTask; + var startupLogEntries = LogQueue?.ToArray() ?? []; + await _startupUiRenderer.RenderAsync( + new Dictionary() + { + { "isInReportingMode", _isUnhealthy }, + { "retryValue", retryAfterValue }, + { "logs", startupLogEntries }, + { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) } + }, + new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions)) + .ConfigureAwait(false); }); }); }) .Build(); await _startupServer.StartAsync().ConfigureAwait(false); + IsAlive = true; } /// @@ -191,6 +266,7 @@ public sealed class SetupServer : IDisposable } await _startupServer.StopAsync().ConfigureAwait(false); + IsAlive = false; } /// @@ -203,6 +279,9 @@ public sealed class SetupServer : IDisposable _disposed = true; _startupServer?.Dispose(); + IsAlive = false; + LogQueue?.Clear(); + LogQueue = null; } private void ThrowIfDisposed() @@ -210,11 +289,88 @@ public sealed class SetupServer : IDisposable ObjectDisposedException.ThrowIf(_disposed, this); } + internal void SoftStop() + { + _isUnhealthy = true; + } + private class SetupHealthcheck : IHealthCheck { + private readonly SetupServer _startupServer; + + public SetupHealthcheck(SetupServer startupServer) + { + _startupServer = startupServer; + } + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (_startupServer._isUnhealthy) + { + return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs.")); + } + return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up.")); } } + + internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable + { + private bool _disposed; + + public ILogger CreateLogger(string categoryName) + { + return new CatchingSetupServerLogger(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } + } + + internal sealed class CatchingSetupServerLogger : ILogger + { + public IDisposable? BeginScope(TState state) + where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel is LogLevel.Error or LogLevel.Critical; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + LogQueue?.Enqueue(new() + { + LogLevel = logLevel, + Content = formatter(state, exception), + DateOfCreation = DateTimeOffset.Now + }); + } + } + + internal class StartupLogEntry + { + public LogLevel LogLevel { get; set; } + + public string? Content { get; set; } + + public DateTimeOffset DateOfCreation { get; set; } + + public List Children { get; set; } = []; + } } diff --git a/Jellyfin.Server/ServerSetupApp/StartupLogger.cs b/Jellyfin.Server/ServerSetupApp/StartupLogger.cs new file mode 100644 index 0000000000..2b86dc0c1a --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/StartupLogger.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Jellyfin.Server.Migrations.Routines; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +public class StartupLogger : IStartupLogger +{ + private readonly SetupServer.StartupLogEntry? _groupEntry; + + /// + /// Initializes a new instance of the class. + /// + public StartupLogger() + { + Loggers = []; + } + + /// + /// Initializes a new instance of the class. + /// + private StartupLogger(SetupServer.StartupLogEntry? groupEntry) : this() + { + _groupEntry = groupEntry; + } + + internal static IStartupLogger Logger { get; } = new StartupLogger(); + + private List Loggers { get; set; } + + /// + public IStartupLogger BeginGroup(FormattableString logEntry) + { + var startupEntry = new SetupServer.StartupLogEntry() + { + Content = logEntry.ToString(CultureInfo.InvariantCulture), + DateOfCreation = DateTimeOffset.Now + }; + + if (_groupEntry is null) + { + SetupServer.LogQueue?.Enqueue(startupEntry); + } + else + { + _groupEntry.Children.Add(startupEntry); + } + + return new StartupLogger(startupEntry); + } + + /// + public IDisposable? BeginScope(TState state) + where TState : notnull + { + return null; + } + + /// + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + foreach (var item in Loggers.Where(e => e.IsEnabled(logLevel))) + { + item.Log(logLevel, eventId, state, exception, formatter); + } + + var startupEntry = new SetupServer.StartupLogEntry() + { + LogLevel = logLevel, + Content = formatter(state, exception), + DateOfCreation = DateTimeOffset.Now + }; + + if (_groupEntry is null) + { + SetupServer.LogQueue?.Enqueue(startupEntry); + } + else + { + _groupEntry.Children.Add(startupEntry); + } + } + + /// + public IStartupLogger With(ILogger logger) + { + return new StartupLogger(_groupEntry) + { + Loggers = [.. Loggers, logger] + }; + } +} diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html new file mode 100644 index 0000000000..747835b2a8 --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html @@ -0,0 +1,225 @@ + + + + + + + {{#IF isInReportingMode}} + ❌ + {{/IF}} + Jellyfin Startup + + + + + +
+
+ + {{^IF isInReportingMode}} +

Jellyfin Server still starting. Please wait.

+ {{#ELSE}} +

Jellyfin Server has encountered an error and was not able to start.

+ {{/ELSE}} + {{/IF}} + + {{#IF localNetworkRequest}} +

You can download the current log file here.

+ {{/IF}} +
+ + {{#DECLARE LogEntry |--}} + {{#LET children = Children}} +
  • + {{--| #IF children.Count > 0}} +
    + {{DateOfCreation}} - {{Content}} +
      + {{--| #EACH children.Reverse() |-}} + {{#IMPORT 'LogEntry'}} + {{--| /EACH |-}} +
    +
    + {{--| #ELSE |-}} + {{DateOfCreation}} - {{Content}} + {{--| /ELSE |--}} + {{--| /IF |-}} +
  • + {{--| /DECLARE}} + +
    +
      + {{#FOREACH log IN logs.Reverse()}} + {{#IMPORT 'LogEntry' #WITH log}} + {{/FOREACH}} +
    +
    +
    + + +{{^IF isInReportingMode}} + +{{/IF}} + + diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index b2cde2aabc..725e359d7d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -5,6 +5,7 @@ using System.IO; using Emby.Server.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Hosting; @@ -16,6 +17,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Serilog; +using Serilog.Core; using Serilog.Extensions.Logging; namespace Jellyfin.Server.Integration.Tests @@ -95,7 +97,8 @@ namespace Jellyfin.Server.Integration.Tests .AddInMemoryCollection(ConfigurationOptions.DefaultConfiguration) .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); - }); + }) + .ConfigureServices(e => e.AddSingleton().AddSingleton(e)); } /// @@ -128,5 +131,39 @@ namespace Jellyfin.Server.Integration.Tests base.Dispose(disposing); } + + private sealed class NullStartupLogger : IStartupLogger + { + public IStartupLogger BeginGroup(FormattableString logEntry) + { + return this; + } + + public IDisposable? BeginScope(TState state) + where TState : notnull + { + return NullLogger.Instance.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return NullLogger.Instance.IsEnabled(logLevel); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + NullLogger.Instance.Log(logLevel, eventId, state, exception, formatter); + } + + public Microsoft.Extensions.Logging.ILogger With(Microsoft.Extensions.Logging.ILogger logger) + { + return this; + } + + IStartupLogger IStartupLogger.With(Microsoft.Extensions.Logging.ILogger logger) + { + return this; + } + } } } -- cgit v1.2.3 From 04422250ebe13a96629a2f2d5c51c846daf0b6ca Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 6 Jun 2025 10:59:15 +0000 Subject: Fix source directory for setup template --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 751cf7f428..d88dbee577 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -84,7 +84,7 @@ public sealed class SetupServer : IDisposable /// A Task. public async Task RunAsync() { - var fileTemplate = await File.ReadAllTextAsync(Path.Combine("ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false); + var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false); _startupUiRenderer = (await ParserOptionsBuilder.New() .WithTemplate(fileTemplate) .WithFormatter( -- cgit v1.2.3 From 1e9e4ffda9abe30b71ceb1de2f4c3143805c66a9 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 9 Jun 2025 04:52:39 +0300 Subject: Rework startup topic handling and reenable output to logging framework (#14243) --- .../Migrations/JellyfinMigrationService.cs | 2 +- .../Migrations/Routines/MigrateKeyframeData.cs | 2 +- .../Migrations/Routines/MigrateLibraryDb.cs | 2 +- .../Routines/MigrateLibraryDbCompatibilityCheck.cs | 2 +- .../Migrations/Routines/MigrateRatingLevels.cs | 2 +- .../Migrations/Routines/MoveExtractedFiles.cs | 2 +- .../Migrations/Routines/MoveTrickplayFiles.cs | 2 +- Jellyfin.Server/Migrations/Stages/CodeMigration.cs | 18 +++-- Jellyfin.Server/Program.cs | 11 +-- Jellyfin.Server/ServerSetupApp/IStartupLogger.cs | 43 +++++++++++- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 17 +---- Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs | 31 +++++++++ Jellyfin.Server/ServerSetupApp/StartupLogger.cs | 78 ++++++++++++++-------- .../ServerSetupApp/StartupLoggerExtensions.cs | 18 +++++ .../ServerSetupApp/StartupLoggerOfCategory.cs | 56 ++++++++++++++++ .../JellyfinApplicationFactory.cs | 29 +++++++- 16 files changed, 255 insertions(+), 60 deletions(-) create mode 100644 Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs create mode 100644 Jellyfin.Server/ServerSetupApp/StartupLoggerExtensions.cs create mode 100644 Jellyfin.Server/ServerSetupApp/StartupLoggerOfCategory.cs (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs index 5331b43e33..31a2201185 100644 --- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs +++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs @@ -47,7 +47,7 @@ internal class JellyfinMigrationService public JellyfinMigrationService( IDbContextFactory dbContextFactory, ILoggerFactory loggerFactory, - IStartupLogger startupLogger, + IStartupLogger startupLogger, IApplicationPaths applicationPaths, IBackupService? backupService = null, IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index 033045e639..c199ee4d6b 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -35,7 +35,7 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine /// Instance of the interface. /// The EFCore db factory. public MigrateKeyframeData( - IStartupLogger startupLogger, + IStartupLogger startupLogger, IApplicationPaths appPaths, IDbContextFactory dbProvider) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 521655a4f1..0953030fad 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -48,7 +48,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine /// The server application paths. /// The database provider for special access. public MigrateLibraryDb( - IStartupLogger startupLogger, + IStartupLogger startupLogger, IDbContextFactory provider, IServerApplicationPaths paths, IJellyfinDatabaseProvider jellyfinDatabaseProvider) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs index 2d5fc2a0db..d4cc9bbeed 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs @@ -26,7 +26,7 @@ public class MigrateLibraryDbCompatibilityCheck : IAsyncMigrationRoutine ///
    /// The startup logger. /// The Path service. - public MigrateLibraryDbCompatibilityCheck(IStartupLogger startupLogger, IServerApplicationPaths paths) + public MigrateLibraryDbCompatibilityCheck(IStartupLogger startupLogger, IServerApplicationPaths paths) { _logger = startupLogger; _paths = paths; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index ae93557de8..2a6db01cf3 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -23,7 +23,7 @@ internal class MigrateRatingLevels : IDatabaseMigrationRoutine public MigrateRatingLevels( IDbContextFactory provider, - IStartupLogger logger, + IStartupLogger logger, ILocalizationManager localizationManager) { _provider = provider; diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index 6f650f7313..8b394dd7aa 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -47,7 +47,7 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine public MoveExtractedFiles( IApplicationPaths appPaths, ILogger logger, - IStartupLogger startupLogger, + IStartupLogger startupLogger, IPathManager pathManager, IFileSystem fileSystem, IDbContextFactory dbProvider) diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index a674aa928b..0f55465e86 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -37,7 +37,7 @@ public class MoveTrickplayFiles : IMigrationRoutine ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, - IStartupLogger logger) + IStartupLogger logger) { _trickplayManager = trickplayManager; _fileSystem = fileSystem; diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs index 47ed26965b..c3592f62a1 100644 --- a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs +++ b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Jellyfin.Server.ServerSetupApp; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Stages; @@ -21,11 +22,13 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + Metadata.Name!; } - private ServiceCollection MigrationServices(IServiceProvider serviceProvider, IStartupLogger logger) + private IServiceCollection MigrationServices(IServiceProvider serviceProvider, IStartupLogger logger) { - var childServiceCollection = new ServiceCollection(); - childServiceCollection.AddSingleton(serviceProvider); - childServiceCollection.AddSingleton(logger); + var childServiceCollection = new ServiceCollection() + .AddSingleton(serviceProvider) + .AddSingleton(logger) + .AddSingleton(typeof(IStartupLogger<>), typeof(NestedStartupLogger<>)) + .AddSingleton(logger.Topic!); foreach (ServiceDescriptor service in serviceProvider.GetRequiredService()) { @@ -78,4 +81,11 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta throw new InvalidOperationException($"The type {MigrationType} does not implement either IMigrationRoutine or IAsyncMigrationRoutine and is not a valid migration type"); } } + + private class NestedStartupLogger : StartupLogger, IStartupLogger + { + public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic) + { + } + } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 0b77d63ac7..dc7fa5eb36 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -60,7 +60,7 @@ namespace Jellyfin.Server private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; - private static IStartupLogger? _migrationLogger; + private static IStartupLogger? _migrationLogger; private static string? _restoreFromBackup; /// @@ -103,6 +103,7 @@ namespace Jellyfin.Server _setupServer = new SetupServer(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost, _loggerFactory, startupConfig); await _setupServer.RunAsync().ConfigureAwait(false); _logger = _loggerFactory.CreateLogger("Main"); + StartupLogger.Logger = new StartupLogger(_logger); // Use the logging framework for uncaught exceptions instead of std error AppDomain.CurrentDomain.UnhandledException += (_, e) @@ -178,7 +179,9 @@ namespace Jellyfin.Server }) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) .UseSerilog() - .ConfigureServices(e => e.AddTransient().AddSingleton(e)) + .ConfigureServices(e => e + .RegisterStartupLogger() + .AddSingleton(e)) .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. @@ -268,7 +271,7 @@ namespace Jellyfin.Server /// A task. public static async Task ApplyStartupMigrationAsync(ServerApplicationPaths appPaths, IConfiguration startupConfig) { - _migrationLogger = StartupLogger.Logger.BeginGroup($"Migration Service"); + _migrationLogger = StartupLogger.Logger.BeginGroup($"Migration Service"); var startupConfigurationManager = new ServerConfigurationManager(appPaths, _loggerFactory, new MyXmlSerializer()); startupConfigurationManager.AddParts([new DatabaseConfigurationFactory()]); var migrationStartupServiceProvider = new ServiceCollection() @@ -276,7 +279,7 @@ namespace Jellyfin.Server .AddJellyfinDbContext(startupConfigurationManager, startupConfig) .AddSingleton(appPaths) .AddSingleton(appPaths) - .AddSingleton(_migrationLogger); + .RegisterStartupLogger(); migrationStartupServiceProvider.AddSingleton(migrationStartupServiceProvider); var startupService = migrationStartupServiceProvider.BuildServiceProvider(); diff --git a/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs b/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs index 2c2ef05f8a..e7c1939368 100644 --- a/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs +++ b/Jellyfin.Server/ServerSetupApp/IStartupLogger.cs @@ -1,5 +1,4 @@ using System; -using Morestachio.Helper.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server.ServerSetupApp; @@ -9,6 +8,11 @@ namespace Jellyfin.Server.ServerSetupApp; /// public interface IStartupLogger : ILogger { + /// + /// Gets the topic this logger is assigned to. + /// + StartupLogTopic? Topic { get; } + /// /// Adds another logger instance to this logger for combined logging. /// @@ -22,4 +26,41 @@ public interface IStartupLogger : ILogger /// Defines the log message that introduces the new group. /// A new logger that can write to the group. IStartupLogger BeginGroup(FormattableString logEntry); + + /// + /// Adds another logger instance to this logger for combined logging. + /// + /// Other logger to rely messages to. + /// A combined logger. + /// The logger cateogry. + IStartupLogger With(ILogger logger); + + /// + /// Opens a new Group logger within the parent logger. + /// + /// Defines the log message that introduces the new group. + /// A new logger that can write to the group. + /// The logger cateogry. + IStartupLogger BeginGroup(FormattableString logEntry); +} + +/// +/// Defines a logger that can be injected via DI to get a startup logger initialised with an logger framework connected . +/// +/// The logger cateogry. +public interface IStartupLogger : IStartupLogger +{ + /// + /// Adds another logger instance to this logger for combined logging. + /// + /// Other logger to rely messages to. + /// A combined logger. + new IStartupLogger With(ILogger logger); + + /// + /// Opens a new Group logger within the parent logger. + /// + /// Defines the log message that introduces the new group. + /// A new logger that can write to the group. + new IStartupLogger BeginGroup(FormattableString logEntry); } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index d88dbee577..6d58e3c4e1 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -71,7 +71,7 @@ public sealed class SetupServer : IDisposable _configurationManager.RegisterConfiguration(); } - internal static ConcurrentQueue? LogQueue { get; set; } = new(); + internal static ConcurrentQueue? LogQueue { get; set; } = new(); /// /// Gets a value indicating whether Startup server is currently running. @@ -88,12 +88,12 @@ public sealed class SetupServer : IDisposable _startupUiRenderer = (await ParserOptionsBuilder.New() .WithTemplate(fileTemplate) .WithFormatter( - (StartupLogEntry logEntry, IEnumerable children) => + (StartupLogTopic logEntry, IEnumerable children) => { if (children.Any()) { var maxLevel = logEntry.LogLevel; - var stack = new Stack(children); + var stack = new Stack(children); while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level. { @@ -362,15 +362,4 @@ public sealed class SetupServer : IDisposable }); } } - - internal class StartupLogEntry - { - public LogLevel LogLevel { get; set; } - - public string? Content { get; set; } - - public DateTimeOffset DateOfCreation { get; set; } - - public List Children { get; set; } = []; - } } diff --git a/Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs b/Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs new file mode 100644 index 0000000000..cd440a9b53 --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.ObjectModel; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +/// Defines a topic for the Startup UI. +/// +public class StartupLogTopic +{ + /// + /// Gets or Sets the LogLevel. + /// + public LogLevel LogLevel { get; set; } + + /// + /// Gets or Sets the descriptor for the topic. + /// + public string? Content { get; set; } + + /// + /// Gets or sets the time the topic was created. + /// + public DateTimeOffset DateOfCreation { get; set; } + + /// + /// Gets the child items of this topic. + /// + public Collection Children { get; } = []; +} diff --git a/Jellyfin.Server/ServerSetupApp/StartupLogger.cs b/Jellyfin.Server/ServerSetupApp/StartupLogger.cs index 2b86dc0c1a..0121854ce3 100644 --- a/Jellyfin.Server/ServerSetupApp/StartupLogger.cs +++ b/Jellyfin.Server/ServerSetupApp/StartupLogger.cs @@ -1,56 +1,86 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using Jellyfin.Server.Migrations.Routines; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Jellyfin.Server.ServerSetupApp; /// public class StartupLogger : IStartupLogger { - private readonly SetupServer.StartupLogEntry? _groupEntry; + private readonly StartupLogTopic? _topic; /// /// Initializes a new instance of the class. /// - public StartupLogger() + /// The underlying base logger. + public StartupLogger(ILogger logger) { - Loggers = []; + BaseLogger = logger; } /// /// Initializes a new instance of the class. /// - private StartupLogger(SetupServer.StartupLogEntry? groupEntry) : this() + /// The underlying base logger. + /// The group for this logger. + internal StartupLogger(ILogger logger, StartupLogTopic? topic) : this(logger) { - _groupEntry = groupEntry; + _topic = topic; } - internal static IStartupLogger Logger { get; } = new StartupLogger(); + internal static IStartupLogger Logger { get; set; } = new StartupLogger(NullLogger.Instance); - private List Loggers { get; set; } + /// + public StartupLogTopic? Topic => _topic; + + /// + /// Gets or Sets the underlying base logger. + /// + protected ILogger BaseLogger { get; set; } /// public IStartupLogger BeginGroup(FormattableString logEntry) { - var startupEntry = new SetupServer.StartupLogEntry() + return new StartupLogger(BaseLogger, AddToTopic(logEntry)); + } + + /// + public IStartupLogger With(ILogger logger) + { + return new StartupLogger(logger, Topic); + } + + /// + public IStartupLogger With(ILogger logger) + { + return new StartupLogger(logger, Topic); + } + + /// + public IStartupLogger BeginGroup(FormattableString logEntry) + { + return new StartupLogger(BaseLogger, AddToTopic(logEntry)); + } + + private StartupLogTopic AddToTopic(FormattableString logEntry) + { + var startupEntry = new StartupLogTopic() { Content = logEntry.ToString(CultureInfo.InvariantCulture), DateOfCreation = DateTimeOffset.Now }; - if (_groupEntry is null) + if (Topic is null) { SetupServer.LogQueue?.Enqueue(startupEntry); } else { - _groupEntry.Children.Add(startupEntry); + Topic.Children.Add(startupEntry); } - return new StartupLogger(startupEntry); + return startupEntry; } /// @@ -69,34 +99,26 @@ public class StartupLogger : IStartupLogger /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - foreach (var item in Loggers.Where(e => e.IsEnabled(logLevel))) + if (BaseLogger.IsEnabled(logLevel)) { - item.Log(logLevel, eventId, state, exception, formatter); + // if enabled allow the base logger also to receive the message + BaseLogger.Log(logLevel, eventId, state, exception, formatter); } - var startupEntry = new SetupServer.StartupLogEntry() + var startupEntry = new StartupLogTopic() { LogLevel = logLevel, Content = formatter(state, exception), DateOfCreation = DateTimeOffset.Now }; - if (_groupEntry is null) + if (Topic is null) { SetupServer.LogQueue?.Enqueue(startupEntry); } else { - _groupEntry.Children.Add(startupEntry); + Topic.Children.Add(startupEntry); } } - - /// - public IStartupLogger With(ILogger logger) - { - return new StartupLogger(_groupEntry) - { - Loggers = [.. Loggers, logger] - }; - } } diff --git a/Jellyfin.Server/ServerSetupApp/StartupLoggerExtensions.cs b/Jellyfin.Server/ServerSetupApp/StartupLoggerExtensions.cs new file mode 100644 index 0000000000..ada4b56a7e --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/StartupLoggerExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Jellyfin.Server.ServerSetupApp; + +internal static class StartupLoggerExtensions +{ + public static IServiceCollection RegisterStartupLogger(this IServiceCollection services) + { + return services + .AddTransient>() + .AddTransient(typeof(IStartupLogger<>), typeof(StartupLogger<>)); + } +} diff --git a/Jellyfin.Server/ServerSetupApp/StartupLoggerOfCategory.cs b/Jellyfin.Server/ServerSetupApp/StartupLoggerOfCategory.cs new file mode 100644 index 0000000000..64da0ce88a --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/StartupLoggerOfCategory.cs @@ -0,0 +1,56 @@ +using System; +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +/// Startup logger for usage with DI that utilises an underlying logger from the DI. +/// +/// The category of the underlying logger. +#pragma warning disable SA1649 // File name should match first type name +public class StartupLogger : StartupLogger, IStartupLogger +#pragma warning restore SA1649 // File name should match first type name +{ + /// + /// Initializes a new instance of the class. + /// + /// The injected base logger. + public StartupLogger(ILogger logger) : base(logger) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying base logger. + /// The group for this logger. + internal StartupLogger(ILogger logger, StartupLogTopic? groupEntry) : base(logger, groupEntry) + { + } + + IStartupLogger IStartupLogger.BeginGroup(FormattableString logEntry) + { + var startupEntry = new StartupLogTopic() + { + Content = logEntry.ToString(CultureInfo.InvariantCulture), + DateOfCreation = DateTimeOffset.Now + }; + + if (Topic is null) + { + SetupServer.LogQueue?.Enqueue(startupEntry); + } + else + { + Topic.Children.Add(startupEntry); + } + + return new StartupLogger(BaseLogger, startupEntry); + } + + IStartupLogger IStartupLogger.With(ILogger logger) + { + return new StartupLogger(logger, Topic); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 725e359d7d..0952fb8b63 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -98,7 +98,10 @@ namespace Jellyfin.Server.Integration.Tests .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); }) - .ConfigureServices(e => e.AddSingleton().AddSingleton(e)); + .ConfigureServices(e => e + .AddSingleton>() + .AddTransient(typeof(IStartupLogger<>), typeof(NullStartupLogger<>)) + .AddSingleton(e)); } /// @@ -132,13 +135,20 @@ namespace Jellyfin.Server.Integration.Tests base.Dispose(disposing); } - private sealed class NullStartupLogger : IStartupLogger + private sealed class NullStartupLogger : IStartupLogger { + public StartupLogTopic? Topic => throw new NotImplementedException(); + public IStartupLogger BeginGroup(FormattableString logEntry) { return this; } + public IStartupLogger BeginGroup(FormattableString logEntry) + { + return new NullStartupLogger(); + } + public IDisposable? BeginScope(TState state) where TState : notnull { @@ -160,10 +170,25 @@ namespace Jellyfin.Server.Integration.Tests return this; } + public IStartupLogger With(Microsoft.Extensions.Logging.ILogger logger) + { + return new NullStartupLogger(); + } + + IStartupLogger IStartupLogger.BeginGroup(FormattableString logEntry) + { + return new NullStartupLogger(); + } + IStartupLogger IStartupLogger.With(Microsoft.Extensions.Logging.ILogger logger) { return this; } + + IStartupLogger IStartupLogger.With(Microsoft.Extensions.Logging.ILogger logger) + { + return this; + } } } } -- cgit v1.2.3 From a8601b379775180599cb935236fba4e5a89ee89d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 9 Jun 2025 04:52:48 +0300 Subject: util forward headers on startup api (#14246) --- .../Extensions/ApiServiceCollectionExtensions.cs | 45 ++++++++++++---------- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 10 ++++- 2 files changed, 33 insertions(+), 22 deletions(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 09a4e2ed31..3038841dfa 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -116,26 +116,7 @@ namespace Jellyfin.Server.Extensions .AddTransient() .Configure(options => { - // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs - // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. - - if (config.KnownProxies.Length == 0) - { - options.ForwardedHeaders = ForwardedHeaders.None; - options.KnownNetworks.Clear(); - options.KnownProxies.Clear(); - } - else - { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; - AddProxyAddresses(config, config.KnownProxies, options); - } - - // Only set forward limit if we have some known proxies or some known networks. - if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0) - { - options.ForwardLimit = null; - } + ConfigureForwardHeaders(config, options); }) .AddMvc(opts => { @@ -183,6 +164,30 @@ namespace Jellyfin.Server.Extensions return mvcBuilder.AddControllersAsServices(); } + internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions options) + { + // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs + // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. + + if (config.KnownProxies.Length == 0) + { + options.ForwardedHeaders = ForwardedHeaders.None; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + } + else + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; + AddProxyAddresses(config, config.KnownProxies, options); + } + + // Only set forward limit if we have some known proxies or some known networks. + if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0) + { + options.ForwardLimit = null; + } + } + /// /// Adds Swagger to the service collection. /// diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 6d58e3c4e1..43a966e9b4 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Serialization; using Jellyfin.Networking.Manager; +using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -18,6 +19,7 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -138,19 +140,23 @@ public sealed class SetupServer : IDisposable ThrowIfDisposed(); var retryAfterValue = TimeSpan.FromSeconds(5); + var config = _configurationManager.GetNetworkConfiguration()!; _startupServer = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(serv => { serv.AddHealthChecks() .AddCheck("StartupCheck"); + serv.Configure(options => + { + ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options); + }); }) .ConfigureWebHostDefaults(webHostBuilder => { webHostBuilder .UseKestrel((builderContext, options) => { - var config = _configurationManager.GetNetworkConfiguration()!; var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger(), config.EnableIPv4, config.EnableIPv6); knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6); var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6); @@ -168,7 +174,7 @@ public sealed class SetupServer : IDisposable .Configure(app => { app.UseHealthChecks("/health"); - + app.UseForwardedHeaders(); app.Map("/startup/logger", loggerRoute => { loggerRoute.Run(async context => -- cgit v1.2.3 From d1d9c8ed06c2f21607626d7f89fdf5e98be6e984 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 14 Jun 2025 00:26:09 +0300 Subject: Remove appsettings.json loading component from startup server (#14275) --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 43a966e9b4..53e63f0f70 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -141,7 +141,7 @@ public sealed class SetupServer : IDisposable ThrowIfDisposed(); var retryAfterValue = TimeSpan.FromSeconds(5); var config = _configurationManager.GetNetworkConfiguration()!; - _startupServer = Host.CreateDefaultBuilder() + _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"]) .UseConsoleLifetime() .ConfigureServices(serv => { -- cgit v1.2.3 From 7256c9c89dc0d54b805658ef166ab1c3adb67d4f Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 16 Jun 2025 18:32:29 -0600 Subject: Fix startup logger, startup health check --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs') diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 53e63f0f70..92e0129409 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -19,7 +19,6 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -29,6 +28,8 @@ using Microsoft.Extensions.Primitives; using Morestachio; using Morestachio.Framework.IO.SingleStream; using Morestachio.Rendering; +using Serilog; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server.ServerSetupApp; @@ -143,8 +144,10 @@ public sealed class SetupServer : IDisposable var config = _configurationManager.GetNetworkConfiguration()!; _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"]) .UseConsoleLifetime() + .UseSerilog() .ConfigureServices(serv => { + serv.AddSingleton(this); serv.AddHealthChecks() .AddCheck("StartupCheck"); serv.Configure(options => -- cgit v1.2.3