| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 | using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.Globalization;using System.IO;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 Jellyfin.Server.Extensions;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;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;using Morestachio;using Morestachio.Framework.IO.SingleStream;using Morestachio.Rendering;using Serilog;using ILogger = Microsoft.Extensions.Logging.ILogger;namespace Jellyfin.Server.ServerSetupApp;/// <summary>/// Creates a fake application pipeline that will only exist for as long as the main app is not started./// </summary>public sealed class SetupServer : IDisposable{    private readonly Func<INetworkManager?> _networkManagerFactory;    private readonly IApplicationPaths _applicationPaths;    private readonly Func<IServerApplicationHost?> _serverFactory;    private readonly ILoggerFactory _loggerFactory;    private readonly IConfiguration _startupConfiguration;    private readonly ServerConfigurationManager _configurationManager;    private IRenderer? _startupUiRenderer;    private IHost? _startupServer;    private bool _disposed;    private bool _isUnhealthy;    /// <summary>    /// Initializes a new instance of the <see cref="SetupServer"/> class.    /// </summary>    /// <param name="networkManagerFactory">The networkmanager.</param>    /// <param name="applicationPaths">The application paths.</param>    /// <param name="serverApplicationHostFactory">The servers application host.</param>    /// <param name="loggerFactory">The logger factory.</param>    /// <param name="startupConfiguration">The startup configuration.</param>    public SetupServer(        Func<INetworkManager?> networkManagerFactory,        IApplicationPaths applicationPaths,        Func<IServerApplicationHost?> 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<NetworkConfigurationFactory>();    }    internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();    /// <summary>    /// Gets a value indicating whether Startup server is currently running.    /// </summary>    public bool IsAlive { get; internal set; }    /// <summary>    /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.    /// </summary>    /// <returns>A Task.</returns>    public async Task RunAsync()    {        var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);        _startupUiRenderer = (await ParserOptionsBuilder.New()            .WithTemplate(fileTemplate)            .WithFormatter(                (StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>                {                    if (children.Any())                    {                        var maxLevel = logEntry.LogLevel;                        var stack = new Stack<StartupLogTopic>(children);                        while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not 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);        var config = _configurationManager.GetNetworkConfiguration()!;        _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])            .UseConsoleLifetime()            .UseSerilog()            .ConfigureServices(serv =>            {                serv.AddSingleton(this);                serv.AddHealthChecks()                    .AddCheck<SetupHealthcheck>("StartupCheck");                serv.Configure<ForwardedHeadersOptions>(options =>                {                    ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);                });            })            .ConfigureWebHostDefaults(webHostBuilder =>                    {                        webHostBuilder                                .UseKestrel((builderContext, options) =>                                {                                    var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), 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<SetupServer>(),                                        builderContext,                                        options);                                })                                .Configure(app =>                                {                                    app.UseHealthChecks("/health");                                    app.UseForwardedHeaders();                                    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 = new DirectoryInfo(_applicationPaths.LogDirectoryPath)                                                .EnumerateFiles()                                                .OrderByDescending(f => f.CreationTimeUtc)                                                .FirstOrDefault()                                                ?.FullName;                                            if (logFilePath is not null)                                            {                                                await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false);                                            }                                        });                                    });                                    app.Map("/System/Info/Public", systemRoute =>                                    {                                        systemRoute.Run(async context =>                                        {                                            var jfApplicationHost = _serverFactory();                                            var retryCounter = 0;                                            while (jfApplicationHost is null && retryCounter < 5)                                            {                                                await Task.Delay(500).ConfigureAwait(false);                                                jfApplicationHost = _serverFactory();                                                retryCounter++;                                            }                                            if (jfApplicationHost is null)                                            {                                                context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;                                                context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture));                                                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(async (context) =>                                    {                                        context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;                                        context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture));                                        context.Response.Headers.ContentType = new StringValues("text/html");                                        var networkManager = _networkManagerFactory();                                        var startupLogEntries = LogQueue?.ToArray() ?? [];                                        await _startupUiRenderer.RenderAsync(                                            new Dictionary<string, object>()                                            {                                                { "isInReportingMode", _isUnhealthy },                                                { "retryValue", retryAfterValue },                                                { "logs", startupLogEntries },                                                { "networkManagerReady", networkManager is not null },                                                { "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;    }    /// <summary>    /// Stops the Setup server.    /// </summary>    /// <returns>A task. Duh.</returns>    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);        IsAlive = false;    }    /// <inheritdoc/>    public void Dispose()    {        if (_disposed)        {            return;        }        _disposed = true;        _startupServer?.Dispose();        IsAlive = false;        LogQueue?.Clear();        LogQueue = null;    }    private void ThrowIfDisposed()    {        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<HealthCheckResult> 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>(TState state)            where TState : notnull        {            return null;        }        public bool IsEnabled(LogLevel logLevel)        {            return logLevel is LogLevel.Error or LogLevel.Critical;        }        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)        {            if (!IsEnabled(logLevel))            {                return;            }            LogQueue?.Enqueue(new()            {                LogLevel = logLevel,                Content = formatter(state, exception),                DateOfCreation = DateTimeOffset.Now            });        }    }}
 |