< Summary - Jellyfin

Information
Class: Jellyfin.Server.ServerSetupApp.SetupServer
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/ServerSetupApp/SetupServer.cs
Line coverage
0%
Covered lines: 1
Uncovered lines: 224
Coverable lines: 225
Total lines: 377
Line coverage: 0.4%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 2.3% (1/43) Branch coverage: 0% (0/16) Total lines: 3764/14/2026 - 12:13:23 AM Line coverage: 2.3% (1/43) Branch coverage: 0% (0/16) Total lines: 3774/19/2026 - 12:14:27 AM Line coverage: 0.4% (1/225) Branch coverage: 0% (0/20) Total lines: 377 1/23/2026 - 12:11:06 AM Line coverage: 2.3% (1/43) Branch coverage: 0% (0/16) Total lines: 3764/14/2026 - 12:13:23 AM Line coverage: 2.3% (1/43) Branch coverage: 0% (0/16) Total lines: 3774/19/2026 - 12:14:27 AM Line coverage: 0.4% (1/225) Branch coverage: 0% (0/20) Total lines: 377

Coverage delta

Coverage delta 2 -2

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.cctor()100%11100%
RunAsync()0%620%
StopAsync()0%620%
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    {
 090        var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.m
 091        _startupUiRenderer = (await ParserOptionsBuilder.New()
 092            .WithTemplate(fileTemplate)
 093            .WithFormatter(
 094                (StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
 095                {
 096                    if (children.Any())
 097                    {
 098                        var maxLevel = logEntry.LogLevel;
 099                        var stack = new Stack<StartupLogTopic>(children);
 0100
 0101                        while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) //
 0102                        {
 0103                            maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
 0104                            foreach (var child in logEntry.Children)
 0105                            {
 0106                                stack.Push(child);
 0107                            }
 0108                        }
 0109
 0110                        return maxLevel;
 0111                    }
 0112
 0113                    return logEntry.LogLevel;
 0114                },
 0115                "FormatLogLevel")
 0116            .WithFormatter(
 0117                (LogLevel logLevel) =>
 0118                {
 0119                    switch (logLevel)
 0120                    {
 0121                        case LogLevel.Trace:
 0122                        case LogLevel.Debug:
 0123                        case LogLevel.None:
 0124                            return "success";
 0125                        case LogLevel.Information:
 0126                            return "info";
 0127                        case LogLevel.Warning:
 0128                            return "warn";
 0129                        case LogLevel.Error:
 0130                            return "danger";
 0131                        case LogLevel.Critical:
 0132                            return "danger-strong";
 0133                    }
 0134
 0135                    return string.Empty;
 0136                },
 0137                "ToString")
 0138            .BuildAndParseAsync()
 0139            .ConfigureAwait(false))
 0140            .CreateCompiledRenderer();
 141
 0142        ThrowIfDisposed();
 0143        var retryAfterValue = TimeSpan.FromSeconds(5);
 0144        var config = _configurationManager.GetNetworkConfiguration()!;
 0145        _startupServer?.Dispose();
 0146        _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
 0147            .UseConsoleLifetime()
 0148            .UseSerilog()
 0149            .ConfigureServices(serv =>
 0150            {
 0151                serv.AddSingleton(this);
 0152                serv.AddHealthChecks()
 0153                    .AddCheck<SetupHealthcheck>("StartupCheck");
 0154                serv.Configure<ForwardedHeadersOptions>(options =>
 0155                {
 0156                    ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);
 0157                });
 0158            })
 0159            .ConfigureWebHostDefaults(webHostBuilder =>
 0160                    {
 0161                        webHostBuilder
 0162                                .UseKestrel((builderContext, options) =>
 0163                                {
 0164                                    var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogg
 0165                                    knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.
 0166                                    var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger
 0167                                    Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
 0168                                        bindInterfaces,
 0169                                        config.InternalHttpPort,
 0170                                        null,
 0171                                        null,
 0172                                        _startupConfiguration,
 0173                                        _applicationPaths,
 0174                                        _loggerFactory.CreateLogger<SetupServer>(),
 0175                                        builderContext,
 0176                                        options);
 0177                                })
 0178                                .Configure(app =>
 0179                                {
 0180                                    app.UseHealthChecks("/health");
 0181                                    app.UseForwardedHeaders();
 0182                                    app.Map("/startup/logger", loggerRoute =>
 0183                                    {
 0184                                        loggerRoute.Run(async context =>
 0185                                        {
 0186                                            var networkManager = _networkManagerFactory();
 0187                                            if (context.Connection.RemoteIpAddress is null || networkManager is null || 
 0188                                            {
 0189                                                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
 0190                                                return;
 0191                                            }
 0192
 0193                                            var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
 0194                                                .EnumerateFiles()
 0195                                                .OrderByDescending(f => f.CreationTimeUtc)
 0196                                                .FirstOrDefault()
 0197                                                ?.FullName;
 0198                                            if (logFilePath is not null)
 0199                                            {
 0200                                                await context.Response.SendFileAsync(logFilePath, CancellationToken.None
 0201                                            }
 0202                                        });
 0203                                    });
 0204
 0205                                    app.Map("/System/Info/Public", systemRoute =>
 0206                                    {
 0207                                        systemRoute.Run(async context =>
 0208                                        {
 0209                                            var jfApplicationHost = _serverFactory();
 0210
 0211                                            var retryCounter = 0;
 0212                                            while (jfApplicationHost is null && retryCounter < 5)
 0213                                            {
 0214                                                await Task.Delay(500).ConfigureAwait(false);
 0215                                                jfApplicationHost = _serverFactory();
 0216                                                retryCounter++;
 0217                                            }
 0218
 0219                                            if (jfApplicationHost is null)
 0220                                            {
 0221                                                context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 0222                                                context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.T
 0223                                                return;
 0224                                            }
 0225
 0226                                            var sysInfo = new PublicSystemInfo
 0227                                            {
 0228                                                Version = jfApplicationHost.ApplicationVersionString,
 0229                                                ProductName = jfApplicationHost.Name,
 0230                                                Id = jfApplicationHost.SystemId,
 0231                                                ServerName = jfApplicationHost.FriendlyName,
 0232                                                LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
 0233                                                StartupWizardCompleted = false
 0234                                            };
 0235
 0236                                            await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
 0237                                        });
 0238                                    });
 0239
 0240                                    app.Run(async (context) =>
 0241                                    {
 0242                                        context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 0243                                        context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeco
 0244                                        context.Response.Headers.ContentType = new StringValues("text/html");
 0245                                        var networkManager = _networkManagerFactory();
 0246
 0247                                        var startupLogEntries = LogQueue?.ToArray() ?? [];
 0248                                        await _startupUiRenderer.RenderAsync(
 0249                                            new Dictionary<string, object>()
 0250                                            {
 0251                                                { "isInReportingMode", _isUnhealthy },
 0252                                                { "retryValue", retryAfterValue },
 0253                                                { "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembl
 0254                                                { "logs", startupLogEntries },
 0255                                                { "networkManagerReady", networkManager is not null },
 0256                                                { "localNetworkRequest", networkManager is not null && context.Connectio
 0257                                            },
 0258                                            new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.Fil
 0259                                            .ConfigureAwait(false);
 0260                                    });
 0261                                });
 0262                    })
 0263                    .Build();
 0264        await _startupServer.StartAsync().ConfigureAwait(false);
 0265        IsAlive = true;
 0266    }
 267
 268    /// <summary>
 269    /// Stops the Setup server.
 270    /// </summary>
 271    /// <returns>A task. Duh.</returns>
 272    public async Task StopAsync()
 273    {
 0274        ThrowIfDisposed();
 0275        if (_startupServer is null)
 276        {
 0277            throw new InvalidOperationException("Tried to stop a non existing startup server");
 278        }
 279
 0280        await _startupServer.StopAsync().ConfigureAwait(false);
 0281        IsAlive = false;
 0282    }
 283
 284    /// <inheritdoc/>
 285    public void Dispose()
 286    {
 0287        if (_disposed)
 288        {
 0289            return;
 290        }
 291
 0292        _disposed = true;
 0293        _startupServer?.Dispose();
 0294        IsAlive = false;
 0295        LogQueue?.Clear();
 0296        LogQueue = null;
 0297    }
 298
 299    private void ThrowIfDisposed()
 300    {
 0301        ObjectDisposedException.ThrowIf(_disposed, this);
 0302    }
 303
 304    internal void SoftStop()
 305    {
 0306        _isUnhealthy = true;
 0307    }
 308
 309    private class SetupHealthcheck : IHealthCheck
 310    {
 311        private readonly SetupServer _startupServer;
 312
 313        public SetupHealthcheck(SetupServer startupServer)
 314        {
 0315            _startupServer = startupServer;
 0316        }
 317
 318        public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken 
 319        {
 0320            if (_startupServer._isUnhealthy)
 321            {
 0322                return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs."))
 323            }
 324
 0325            return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
 326        }
 327    }
 328
 329    internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable
 330    {
 331        private bool _disposed;
 332
 333        public ILogger CreateLogger(string categoryName)
 334        {
 0335            return new CatchingSetupServerLogger();
 336        }
 337
 338        public void Dispose()
 339        {
 0340            if (_disposed)
 341            {
 0342                return;
 343            }
 344
 0345            _disposed = true;
 0346        }
 347    }
 348
 349    internal sealed class CatchingSetupServerLogger : ILogger
 350    {
 351        public IDisposable? BeginScope<TState>(TState state)
 352            where TState : notnull
 353        {
 0354            return null;
 355        }
 356
 357        public bool IsEnabled(LogLevel logLevel)
 358        {
 0359            return logLevel is LogLevel.Error or LogLevel.Critical;
 360        }
 361
 362        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exc
 363        {
 0364            if (!IsEnabled(logLevel))
 365            {
 0366                return;
 367            }
 368
 0369            LogQueue?.Enqueue(new()
 0370            {
 0371                LogLevel = logLevel,
 0372                Content = formatter(state, exception),
 0373                DateOfCreation = DateTimeOffset.Now
 0374            });
 0375        }
 376    }
 377}