< Summary - Jellyfin

Information
Class: Jellyfin.Server.ServerSetupApp.SetupServer
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/ServerSetupApp/SetupServer.cs
Line coverage
2%
Covered lines: 1
Uncovered lines: 42
Coverable lines: 43
Total lines: 374
Line coverage: 2.3%
Branch coverage
0%
Covered branches: 0
Total branches: 16
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.cctor()100%11100%
Dispose()0%4260%
ThrowIfDisposed()100%210%
SoftStop()100%210%
.ctor(...)100%210%
CheckHealthAsync(...)0%620%
CreateLogger(...)100%210%
Dispose()0%620%
BeginScope(...)100%210%
IsEnabled(...)0%620%
Log(...)0%2040%

File(s)

/srv/git/jellyfin/Jellyfin.Server/ServerSetupApp/SetupServer.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Net;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Emby.Server.Implementations.Configuration;
 11using Emby.Server.Implementations.Serialization;
 12using Jellyfin.Networking.Manager;
 13using Jellyfin.Server.Extensions;
 14using MediaBrowser.Common.Configuration;
 15using MediaBrowser.Common.Net;
 16using MediaBrowser.Controller;
 17using MediaBrowser.Model.IO;
 18using MediaBrowser.Model.System;
 19using Microsoft.AspNetCore.Builder;
 20using Microsoft.AspNetCore.Hosting;
 21using Microsoft.AspNetCore.Http;
 22using Microsoft.Extensions.Configuration;
 23using Microsoft.Extensions.DependencyInjection;
 24using Microsoft.Extensions.Diagnostics.HealthChecks;
 25using Microsoft.Extensions.Hosting;
 26using Microsoft.Extensions.Logging;
 27using Microsoft.Extensions.Primitives;
 28using Morestachio;
 29using Morestachio.Framework.IO.SingleStream;
 30using Morestachio.Rendering;
 31using Serilog;
 32using ILogger = Microsoft.Extensions.Logging.ILogger;
 33
 34namespace Jellyfin.Server.ServerSetupApp;
 35
 36/// <summary>
 37/// Creates a fake application pipeline that will only exist for as long as the main app is not started.
 38/// </summary>
 39public sealed class SetupServer : IDisposable
 40{
 41    private readonly Func<INetworkManager?> _networkManagerFactory;
 42    private readonly IApplicationPaths _applicationPaths;
 43    private readonly Func<IServerApplicationHost?> _serverFactory;
 44    private readonly ILoggerFactory _loggerFactory;
 45    private readonly IConfiguration _startupConfiguration;
 46    private readonly ServerConfigurationManager _configurationManager;
 47    private IRenderer? _startupUiRenderer;
 48    private IHost? _startupServer;
 49    private bool _disposed;
 50    private bool _isUnhealthy;
 51
 52    /// <summary>
 53    /// Initializes a new instance of the <see cref="SetupServer"/> class.
 54    /// </summary>
 55    /// <param name="networkManagerFactory">The networkmanager.</param>
 56    /// <param name="applicationPaths">The application paths.</param>
 57    /// <param name="serverApplicationHostFactory">The servers application host.</param>
 58    /// <param name="loggerFactory">The logger factory.</param>
 59    /// <param name="startupConfiguration">The startup configuration.</param>
 60    public SetupServer(
 61        Func<INetworkManager?> networkManagerFactory,
 62        IApplicationPaths applicationPaths,
 63        Func<IServerApplicationHost?> serverApplicationHostFactory,
 64        ILoggerFactory loggerFactory,
 65        IConfiguration startupConfiguration)
 66    {
 067        _networkManagerFactory = networkManagerFactory;
 068        _applicationPaths = applicationPaths;
 069        _serverFactory = serverApplicationHostFactory;
 070        _loggerFactory = loggerFactory;
 071        _startupConfiguration = startupConfiguration;
 072        var xmlSerializer = new MyXmlSerializer();
 073        _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer);
 074        _configurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
 075    }
 76
 177    internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();
 78
 79    /// <summary>
 80    /// Gets a value indicating whether Startup server is currently running.
 81    /// </summary>
 82    public bool IsAlive { get; internal set; }
 83
 84    /// <summary>
 85    /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
 86    /// </summary>
 87    /// <returns>A Task.</returns>
 88    public async Task RunAsync()
 89    {
 90        var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.m
 91        _startupUiRenderer = (await ParserOptionsBuilder.New()
 92            .WithTemplate(fileTemplate)
 93            .WithFormatter(
 94                (StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
 95                {
 96                    if (children.Any())
 97                    {
 98                        var maxLevel = logEntry.LogLevel;
 99                        var stack = new Stack<StartupLogTopic>(children);
 100
 101                        while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // err
 102                        {
 103                            maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
 104                            foreach (var child in logEntry.Children)
 105                            {
 106                                stack.Push(child);
 107                            }
 108                        }
 109
 110                        return maxLevel;
 111                    }
 112
 113                    return logEntry.LogLevel;
 114                },
 115                "FormatLogLevel")
 116            .WithFormatter(
 117                (LogLevel logLevel) =>
 118                {
 119                    switch (logLevel)
 120                    {
 121                        case LogLevel.Trace:
 122                        case LogLevel.Debug:
 123                        case LogLevel.None:
 124                            return "success";
 125                        case LogLevel.Information:
 126                            return "info";
 127                        case LogLevel.Warning:
 128                            return "warn";
 129                        case LogLevel.Error:
 130                            return "danger";
 131                        case LogLevel.Critical:
 132                            return "danger-strong";
 133                    }
 134
 135                    return string.Empty;
 136                },
 137                "ToString")
 138            .BuildAndParseAsync()
 139            .ConfigureAwait(false))
 140            .CreateCompiledRenderer();
 141
 142        ThrowIfDisposed();
 143        var retryAfterValue = TimeSpan.FromSeconds(5);
 144        var config = _configurationManager.GetNetworkConfiguration()!;
 145        _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
 146            .UseConsoleLifetime()
 147            .UseSerilog()
 148            .ConfigureServices(serv =>
 149            {
 150                serv.AddSingleton(this);
 151                serv.AddHealthChecks()
 152                    .AddCheck<SetupHealthcheck>("StartupCheck");
 153                serv.Configure<ForwardedHeadersOptions>(options =>
 154                {
 155                    ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);
 156                });
 157            })
 158            .ConfigureWebHostDefaults(webHostBuilder =>
 159                    {
 160                        webHostBuilder
 161                                .UseKestrel((builderContext, options) =>
 162                                {
 163                                    var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogg
 164                                    knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.
 165                                    var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManage
 166                                    Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
 167                                        bindInterfaces,
 168                                        config.InternalHttpPort,
 169                                        null,
 170                                        null,
 171                                        _startupConfiguration,
 172                                        _applicationPaths,
 173                                        _loggerFactory.CreateLogger<SetupServer>(),
 174                                        builderContext,
 175                                        options);
 176                                })
 177                                .Configure(app =>
 178                                {
 179                                    app.UseHealthChecks("/health");
 180                                    app.UseForwardedHeaders();
 181                                    app.Map("/startup/logger", loggerRoute =>
 182                                    {
 183                                        loggerRoute.Run(async context =>
 184                                        {
 185                                            var networkManager = _networkManagerFactory();
 186                                            if (context.Connection.RemoteIpAddress is null || networkManager is null || 
 187                                            {
 188                                                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
 189                                                return;
 190                                            }
 191
 192                                            var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
 193                                                .EnumerateFiles()
 194                                                .OrderByDescending(f => f.CreationTimeUtc)
 195                                                .FirstOrDefault()
 196                                                ?.FullName;
 197                                            if (logFilePath is not null)
 198                                            {
 199                                                await context.Response.SendFileAsync(logFilePath, CancellationToken.None
 200                                            }
 201                                        });
 202                                    });
 203
 204                                    app.Map("/System/Info/Public", systemRoute =>
 205                                    {
 206                                        systemRoute.Run(async context =>
 207                                        {
 208                                            var jfApplicationHost = _serverFactory();
 209
 210                                            var retryCounter = 0;
 211                                            while (jfApplicationHost is null && retryCounter < 5)
 212                                            {
 213                                                await Task.Delay(500).ConfigureAwait(false);
 214                                                jfApplicationHost = _serverFactory();
 215                                                retryCounter++;
 216                                            }
 217
 218                                            if (jfApplicationHost is null)
 219                                            {
 220                                                context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 221                                                context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.T
 222                                                return;
 223                                            }
 224
 225                                            var sysInfo = new PublicSystemInfo
 226                                            {
 227                                                Version = jfApplicationHost.ApplicationVersionString,
 228                                                ProductName = jfApplicationHost.Name,
 229                                                Id = jfApplicationHost.SystemId,
 230                                                ServerName = jfApplicationHost.FriendlyName,
 231                                                LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
 232                                                StartupWizardCompleted = false
 233                                            };
 234
 235                                            await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
 236                                        });
 237                                    });
 238
 239                                    app.Run(async (context) =>
 240                                    {
 241                                        context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 242                                        context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeco
 243                                        context.Response.Headers.ContentType = new StringValues("text/html");
 244                                        var networkManager = _networkManagerFactory();
 245
 246                                        var startupLogEntries = LogQueue?.ToArray() ?? [];
 247                                        await _startupUiRenderer.RenderAsync(
 248                                            new Dictionary<string, object>()
 249                                            {
 250                                                { "isInReportingMode", _isUnhealthy },
 251                                                { "retryValue", retryAfterValue },
 252                                                { "logs", startupLogEntries },
 253                                                { "localNetworkRequest", networkManager is not null && context.Connectio
 254                                            },
 255                                            new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.Fil
 256                                            .ConfigureAwait(false);
 257                                    });
 258                                });
 259                    })
 260                    .Build();
 261        await _startupServer.StartAsync().ConfigureAwait(false);
 262        IsAlive = true;
 263    }
 264
 265    /// <summary>
 266    /// Stops the Setup server.
 267    /// </summary>
 268    /// <returns>A task. Duh.</returns>
 269    public async Task StopAsync()
 270    {
 271        ThrowIfDisposed();
 272        if (_startupServer is null)
 273        {
 274            throw new InvalidOperationException("Tried to stop a non existing startup server");
 275        }
 276
 277        await _startupServer.StopAsync().ConfigureAwait(false);
 278        IsAlive = false;
 279    }
 280
 281    /// <inheritdoc/>
 282    public void Dispose()
 283    {
 0284        if (_disposed)
 285        {
 0286            return;
 287        }
 288
 0289        _disposed = true;
 0290        _startupServer?.Dispose();
 0291        IsAlive = false;
 0292        LogQueue?.Clear();
 0293        LogQueue = null;
 0294    }
 295
 296    private void ThrowIfDisposed()
 297    {
 0298        ObjectDisposedException.ThrowIf(_disposed, this);
 0299    }
 300
 301    internal void SoftStop()
 302    {
 0303        _isUnhealthy = true;
 0304    }
 305
 306    private class SetupHealthcheck : IHealthCheck
 307    {
 308        private readonly SetupServer _startupServer;
 309
 310        public SetupHealthcheck(SetupServer startupServer)
 311        {
 0312            _startupServer = startupServer;
 0313        }
 314
 315        public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken 
 316        {
 0317            if (_startupServer._isUnhealthy)
 318            {
 0319                return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs."))
 320            }
 321
 0322            return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
 323        }
 324    }
 325
 326    internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable
 327    {
 328        private bool _disposed;
 329
 330        public ILogger CreateLogger(string categoryName)
 331        {
 0332            return new CatchingSetupServerLogger();
 333        }
 334
 335        public void Dispose()
 336        {
 0337            if (_disposed)
 338            {
 0339                return;
 340            }
 341
 0342            _disposed = true;
 0343        }
 344    }
 345
 346    internal sealed class CatchingSetupServerLogger : ILogger
 347    {
 348        public IDisposable? BeginScope<TState>(TState state)
 349            where TState : notnull
 350        {
 0351            return null;
 352        }
 353
 354        public bool IsEnabled(LogLevel logLevel)
 355        {
 0356            return logLevel is LogLevel.Error or LogLevel.Critical;
 357        }
 358
 359        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exc
 360        {
 0361            if (!IsEnabled(logLevel))
 362            {
 0363                return;
 364            }
 365
 0366            LogQueue?.Enqueue(new()
 0367            {
 0368                LogLevel = logLevel,
 0369                Content = formatter(state, exception),
 0370                DateOfCreation = DateTimeOffset.Now
 0371            });
 0372        }
 373    }
 374}