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