< 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: 232
Coverable lines: 233
Total lines: 385
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 2/13/2026 - 12:11:21 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: 3775/22/2026 - 12:15:17 AM Line coverage: 0.4% (1/233) Branch coverage: 0% (0/20) Total lines: 385 2/13/2026 - 12:11:21 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: 3775/22/2026 - 12:15:17 AM Line coverage: 0.4% (1/233) Branch coverage: 0% (0/20) Total lines: 385

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            (Version version, int arg) =>
 095            {
 096                // version type does not for some stupid reason implement IFormattable which morestachio relies on for T
 097                return version.ToString(arg);
 098            },
 099            "ToString")
 0100            .WithFormatter(
 0101                (StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
 0102                {
 0103                    if (children.Any())
 0104                    {
 0105                        var maxLevel = logEntry.LogLevel;
 0106                        var stack = new Stack<StartupLogTopic>(children);
 0107
 0108                        while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) //
 0109                        {
 0110                            maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
 0111                            foreach (var child in logEntry.Children)
 0112                            {
 0113                                stack.Push(child);
 0114                            }
 0115                        }
 0116
 0117                        return maxLevel;
 0118                    }
 0119
 0120                    return logEntry.LogLevel;
 0121                },
 0122                "FormatLogLevel")
 0123            .WithFormatter(
 0124                (LogLevel logLevel) =>
 0125                {
 0126                    switch (logLevel)
 0127                    {
 0128                        case LogLevel.Trace:
 0129                        case LogLevel.Debug:
 0130                        case LogLevel.None:
 0131                            return "success";
 0132                        case LogLevel.Information:
 0133                            return "info";
 0134                        case LogLevel.Warning:
 0135                            return "warn";
 0136                        case LogLevel.Error:
 0137                            return "danger";
 0138                        case LogLevel.Critical:
 0139                            return "danger-strong";
 0140                    }
 0141
 0142                    return string.Empty;
 0143                },
 0144                "ToString")
 0145            .BuildAndParseAsync()
 0146            .ConfigureAwait(false))
 0147            .CreateCompiledRenderer();
 148
 0149        ThrowIfDisposed();
 0150        var retryAfterValue = TimeSpan.FromSeconds(5);
 0151        var config = _configurationManager.GetNetworkConfiguration()!;
 0152        _startupServer?.Dispose();
 0153        _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
 0154            .UseConsoleLifetime()
 0155            .UseSerilog()
 0156            .ConfigureServices(serv =>
 0157            {
 0158                serv.AddSingleton(this);
 0159                serv.AddHealthChecks()
 0160                    .AddCheck<SetupHealthcheck>("StartupCheck");
 0161                serv.Configure<ForwardedHeadersOptions>(options =>
 0162                {
 0163                    ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);
 0164                });
 0165            })
 0166            .ConfigureWebHostDefaults(webHostBuilder =>
 0167                    {
 0168                        webHostBuilder
 0169                                .UseKestrel((builderContext, options) =>
 0170                                {
 0171                                    var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogg
 0172                                    knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.
 0173                                    var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger
 0174                                    Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
 0175                                        bindInterfaces,
 0176                                        config.InternalHttpPort,
 0177                                        null,
 0178                                        null,
 0179                                        _startupConfiguration,
 0180                                        _applicationPaths,
 0181                                        _loggerFactory.CreateLogger<SetupServer>(),
 0182                                        builderContext,
 0183                                        options);
 0184                                })
 0185                                .Configure(app =>
 0186                                {
 0187                                    app.UseHealthChecks("/health");
 0188                                    app.UseForwardedHeaders();
 0189                                    app.Map("/startup/logger", loggerRoute =>
 0190                                    {
 0191                                        loggerRoute.Run(async context =>
 0192                                        {
 0193                                            var networkManager = _networkManagerFactory();
 0194                                            if (context.Connection.RemoteIpAddress is null || networkManager is null || 
 0195                                            {
 0196                                                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
 0197                                                return;
 0198                                            }
 0199
 0200                                            var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
 0201                                                .EnumerateFiles()
 0202                                                .OrderByDescending(f => f.CreationTimeUtc)
 0203                                                .FirstOrDefault()
 0204                                                ?.FullName;
 0205                                            if (logFilePath is not null)
 0206                                            {
 0207                                                await context.Response.SendFileAsync(logFilePath, CancellationToken.None
 0208                                            }
 0209                                        });
 0210                                    });
 0211
 0212                                    app.Map("/System/Info/Public", systemRoute =>
 0213                                    {
 0214                                        systemRoute.Run(async context =>
 0215                                        {
 0216                                            var jfApplicationHost = _serverFactory();
 0217
 0218                                            var retryCounter = 0;
 0219                                            while (jfApplicationHost is null && retryCounter < 5)
 0220                                            {
 0221                                                await Task.Delay(500).ConfigureAwait(false);
 0222                                                jfApplicationHost = _serverFactory();
 0223                                                retryCounter++;
 0224                                            }
 0225
 0226                                            if (jfApplicationHost is null)
 0227                                            {
 0228                                                context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 0229                                                context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.T
 0230                                                return;
 0231                                            }
 0232
 0233                                            var sysInfo = new PublicSystemInfo
 0234                                            {
 0235                                                Version = jfApplicationHost.ApplicationVersionString,
 0236                                                ProductName = jfApplicationHost.Name,
 0237                                                Id = jfApplicationHost.SystemId,
 0238                                                ServerName = jfApplicationHost.FriendlyName,
 0239                                                LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
 0240                                                StartupWizardCompleted = false
 0241                                            };
 0242
 0243                                            await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
 0244                                        });
 0245                                    });
 0246
 0247                                    var version = typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName()
 0248                                    app.Run(async (context) =>
 0249                                    {
 0250                                        context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 0251                                        context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeco
 0252                                        context.Response.Headers.ContentType = new StringValues("text/html");
 0253                                        var networkManager = _networkManagerFactory();
 0254
 0255                                        var startupLogEntries = LogQueue?.ToArray() ?? [];
 0256                                        await _startupUiRenderer.RenderAsync(
 0257                                            new Dictionary<string, object>()
 0258                                            {
 0259                                                { "isInReportingMode", _isUnhealthy },
 0260                                                { "retryValue", retryAfterValue },
 0261                                                { "version", version },
 0262                                                { "logs", startupLogEntries },
 0263                                                { "networkManagerReady", networkManager is not null },
 0264                                                { "localNetworkRequest", networkManager is not null && context.Connectio
 0265                                            },
 0266                                            new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.Fil
 0267                                            .ConfigureAwait(false);
 0268                                    });
 0269                                });
 0270                    })
 0271                    .Build();
 0272        await _startupServer.StartAsync().ConfigureAwait(false);
 0273        IsAlive = true;
 0274    }
 275
 276    /// <summary>
 277    /// Stops the Setup server.
 278    /// </summary>
 279    /// <returns>A task. Duh.</returns>
 280    public async Task StopAsync()
 281    {
 0282        ThrowIfDisposed();
 0283        if (_startupServer is null)
 284        {
 0285            throw new InvalidOperationException("Tried to stop a non existing startup server");
 286        }
 287
 0288        await _startupServer.StopAsync().ConfigureAwait(false);
 0289        IsAlive = false;
 0290    }
 291
 292    /// <inheritdoc/>
 293    public void Dispose()
 294    {
 0295        if (_disposed)
 296        {
 0297            return;
 298        }
 299
 0300        _disposed = true;
 0301        _startupServer?.Dispose();
 0302        IsAlive = false;
 0303        LogQueue?.Clear();
 0304        LogQueue = null;
 0305    }
 306
 307    private void ThrowIfDisposed()
 308    {
 0309        ObjectDisposedException.ThrowIf(_disposed, this);
 0310    }
 311
 312    internal void SoftStop()
 313    {
 0314        _isUnhealthy = true;
 0315    }
 316
 317    private class SetupHealthcheck : IHealthCheck
 318    {
 319        private readonly SetupServer _startupServer;
 320
 321        public SetupHealthcheck(SetupServer startupServer)
 322        {
 0323            _startupServer = startupServer;
 0324        }
 325
 326        public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken 
 327        {
 0328            if (_startupServer._isUnhealthy)
 329            {
 0330                return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs."))
 331            }
 332
 0333            return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
 334        }
 335    }
 336
 337    internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable
 338    {
 339        private bool _disposed;
 340
 341        public ILogger CreateLogger(string categoryName)
 342        {
 0343            return new CatchingSetupServerLogger();
 344        }
 345
 346        public void Dispose()
 347        {
 0348            if (_disposed)
 349            {
 0350                return;
 351            }
 352
 0353            _disposed = true;
 0354        }
 355    }
 356
 357    internal sealed class CatchingSetupServerLogger : ILogger
 358    {
 359        public IDisposable? BeginScope<TState>(TState state)
 360            where TState : notnull
 361        {
 0362            return null;
 363        }
 364
 365        public bool IsEnabled(LogLevel logLevel)
 366        {
 0367            return logLevel is LogLevel.Error or LogLevel.Critical;
 368        }
 369
 370        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exc
 371        {
 0372            if (!IsEnabled(logLevel))
 373            {
 0374                return;
 375            }
 376
 0377            LogQueue?.Enqueue(new()
 0378            {
 0379                LogLevel = logLevel,
 0380                Content = formatter(state, exception),
 0381                DateOfCreation = DateTimeOffset.Now
 0382            });
 0383        }
 384    }
 385}