diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 295fb8112f..0bbcfa6a64 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,10 +7,13 @@ 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; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -42,6 +45,9 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static SetupServer? _setupServer = new(); + + private static IHost? _jfHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -68,6 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); + await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -122,6 +129,8 @@ namespace Jellyfin.Server if (_restartOnShutdown) { _startTimestamp = Stopwatch.GetTimestamp(); + _setupServer = new SetupServer(); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); } } while (_restartOnShutdown); } @@ -133,11 +142,9 @@ namespace Jellyfin.Server _loggerFactory, options, startupConfig); - - IHost? host = null; try { - host = Host.CreateDefaultBuilder() + _jfHost = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => @@ -154,14 +161,18 @@ 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 = host.Services; + appHost.ServiceProvider = _jfHost.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await host.StartAsync().ConfigureAwait(false); + await Task.Delay(50000).ConfigureAwait(false); + await _setupServer!.StopAsync().ConfigureAwait(false); + _setupServer.Dispose(); + _setupServer = null!; + await _jfHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -180,7 +191,7 @@ namespace Jellyfin.Server _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - await host.WaitForShutdownAsync().ConfigureAwait(false); + await _jfHost.WaitForShutdownAsync().ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) @@ -205,7 +216,7 @@ namespace Jellyfin.Server } } - host?.Dispose(); + _jfHost?.Dispose(); } } 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.")); + } + } +} diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 5a13cc4173..7a22dd8526 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -921,6 +921,19 @@ public class NetworkManager : INetworkManager, IDisposable /// public bool IsInLocalNetwork(IPAddress address) + { + return NetworkManager.IsInLocalNetwork(address, TrustAllIPv6Interfaces, _lanSubnets, _excludedSubnets); + } + + /// + /// Checks a ip address to match any lansubnet given but not to be in any excluded subnet. + /// + /// The IP address to checl. + /// Whenever all IPV6 subnet address shall be permitted. + /// The list of subnets to permit. + /// The list of subnets to never permit. + /// The check if the given IP address is in any provided subnet. + public static bool IsInLocalNetwork(IPAddress address, bool trustAllIpv6, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { ArgumentNullException.ThrowIfNull(address); @@ -930,23 +943,23 @@ public class NetworkManager : INetworkManager, IDisposable address = address.MapToIPv4(); } - if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((trustAllIpv6 && address.AddressFamily == AddressFamily.InterNetworkV6) || IPAddress.IsLoopback(address)) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return CheckIfLanAndNotExcluded(address); + return CheckIfLanAndNotExcluded(address, lanSubnets, excludedSubnets); } - private bool CheckIfLanAndNotExcluded(IPAddress address) + private static bool CheckIfLanAndNotExcluded(IPAddress address, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { - foreach (var lanSubnet in _lanSubnets) + foreach (var lanSubnet in lanSubnets) { if (lanSubnet.Contains(address)) { - foreach (var excludedSubnet in _excludedSubnets) + foreach (var excludedSubnet in excludedSubnets) { if (excludedSubnet.Contains(address)) {