< 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: 4
Uncovered lines: 179
Coverable lines: 183
Total lines: 344
Line coverage: 2.1%
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 3/26/2026 - 12:14:14 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: 3856/28/2026 - 12:15:35 AM Line coverage: 2.1% (4/183) Branch coverage: 0% (0/20) Total lines: 344 3/26/2026 - 12:14:14 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: 3856/28/2026 - 12:15:35 AM Line coverage: 2.1% (4/183) Branch coverage: 0% (0/20) Total lines: 344

Coverage delta

Coverage delta 2 -2

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%210%
get_CurrentActivity()100%210%
RunAsync()0%620%
StopAsync()0%620%
Dispose()0%4260%
ThrowIfDisposed()100%210%
ReportActivity(...)100%11100%
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.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 Serilog;
 28using ILogger = Microsoft.Extensions.Logging.ILogger;
 29
 30namespace Jellyfin.Server.ServerSetupApp;
 31
 32/// <summary>
 33/// Creates a fake application pipeline that will only exist for as long as the main app is not started.
 34/// </summary>
 35public sealed class SetupServer : IDisposable
 36{
 37    private readonly Func<INetworkManager?> _networkManagerFactory;
 38    private readonly IApplicationPaths _applicationPaths;
 39    private readonly Func<IServerApplicationHost?> _serverFactory;
 40    private readonly ILoggerFactory _loggerFactory;
 41    private readonly IConfiguration _startupConfiguration;
 42    private readonly ServerConfigurationManager _configurationManager;
 143    private static volatile string _currentActivity = StartupActivity.Starting;
 44    private StartupUiRenderer? _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<StartupLogTopic>? LogQueue { get; set; } = new();
 75
 76    /// <summary>
 77    /// Gets a generic, non-identifying summary of what startup is currently doing. This is shown in the
 78    /// always-visible header of the startup UI to unauthenticated clients, so it never contains server specific details
 79    /// </summary>
 080    internal static string CurrentActivity => _currentActivity;
 81
 82    /// <summary>
 83    /// Gets a value indicating whether Startup server is currently running.
 84    /// </summary>
 85    public bool IsAlive { get; internal set; }
 86
 87    /// <summary>
 88    /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
 89    /// </summary>
 90    /// <returns>A Task.</returns>
 91    public async Task RunAsync()
 92    {
 093        ReportActivity(StartupActivity.Starting);
 094        _startupUiRenderer = await StartupUiRenderer.CreateAsync(
 095            Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
 96
 097        ThrowIfDisposed();
 098        var retryAfterValue = TimeSpan.FromSeconds(5);
 099        var config = _configurationManager.GetNetworkConfiguration()!;
 0100        _startupServer?.Dispose();
 0101        _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
 0102            .UseConsoleLifetime()
 0103            .UseSerilog()
 0104            .ConfigureServices(serv =>
 0105            {
 0106                serv.AddSingleton(this);
 0107                serv.AddHealthChecks()
 0108                    .AddCheck<SetupHealthcheck>("StartupCheck");
 0109                serv.Configure<ForwardedHeadersOptions>(options =>
 0110                {
 0111                    ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);
 0112                });
 0113            })
 0114            .ConfigureWebHostDefaults(webHostBuilder =>
 0115                    {
 0116                        webHostBuilder
 0117                                .UseKestrel((builderContext, options) =>
 0118                                {
 0119                                    var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogg
 0120                                    knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.
 0121                                    var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger
 0122                                    Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
 0123                                        bindInterfaces,
 0124                                        config.InternalHttpPort,
 0125                                        null,
 0126                                        null,
 0127                                        _startupConfiguration,
 0128                                        _applicationPaths,
 0129                                        _loggerFactory.CreateLogger<SetupServer>(),
 0130                                        builderContext,
 0131                                        options);
 0132                                })
 0133                                .Configure(app =>
 0134                                {
 0135                                    app.UseHealthChecks("/health");
 0136                                    app.UseForwardedHeaders();
 0137                                    app.Map("/startup/logger", loggerRoute =>
 0138                                    {
 0139                                        loggerRoute.Run(async context =>
 0140                                        {
 0141                                            var networkManager = _networkManagerFactory();
 0142                                            if (context.Connection.RemoteIpAddress is null || networkManager is null || 
 0143                                            {
 0144                                                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
 0145                                                return;
 0146                                            }
 0147
 0148                                            var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
 0149                                                .EnumerateFiles()
 0150                                                .OrderByDescending(f => f.CreationTimeUtc)
 0151                                                .FirstOrDefault()
 0152                                                ?.FullName;
 0153                                            if (logFilePath is not null)
 0154                                            {
 0155                                                await context.Response.SendFileAsync(logFilePath, CancellationToken.None
 0156                                            }
 0157                                        });
 0158                                    });
 0159
 0160                                    app.Map("/System/Info/Public", systemRoute =>
 0161                                    {
 0162                                        systemRoute.Run(async context =>
 0163                                        {
 0164                                            var jfApplicationHost = _serverFactory();
 0165
 0166                                            var retryCounter = 0;
 0167                                            while (jfApplicationHost is null && retryCounter < 5)
 0168                                            {
 0169                                                await Task.Delay(500).ConfigureAwait(false);
 0170                                                jfApplicationHost = _serverFactory();
 0171                                                retryCounter++;
 0172                                            }
 0173
 0174                                            if (jfApplicationHost is null)
 0175                                            {
 0176                                                context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 0177                                                context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.T
 0178                                                return;
 0179                                            }
 0180
 0181                                            var sysInfo = new PublicSystemInfo
 0182                                            {
 0183                                                Version = jfApplicationHost.ApplicationVersionString,
 0184                                                ProductName = jfApplicationHost.Name,
 0185                                                Id = jfApplicationHost.SystemId,
 0186                                                ServerName = jfApplicationHost.FriendlyName,
 0187                                                LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
 0188                                                StartupWizardCompleted = false
 0189                                            };
 0190
 0191                                            await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
 0192                                        });
 0193                                    });
 0194
 0195                                    var version = typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName()
 0196                                    app.Run(async (context) =>
 0197                                    {
 0198                                        context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
 0199                                        context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeco
 0200                                        context.Response.Headers.ContentType = new StringValues("text/html");
 0201                                        var networkManager = _networkManagerFactory();
 0202
 0203                                        var startupLogEntries = LogQueue?.ToArray() ?? [];
 0204                                        await _startupUiRenderer.RenderAsync(
 0205                                            new Dictionary<string, object>()
 0206                                            {
 0207                                                { "isInReportingMode", _isUnhealthy },
 0208                                                { "currentActivity", CurrentActivity },
 0209                                                { "retryValue", retryAfterValue },
 0210                                                { "version", version },
 0211                                                { "logs", startupLogEntries },
 0212                                                { "networkManagerReady", networkManager is not null },
 0213                                                { "localNetworkRequest", networkManager is not null && context.Connectio
 0214                                            },
 0215                                            context.Response.BodyWriter.AsStream())
 0216                                            .ConfigureAwait(false);
 0217                                    });
 0218                                });
 0219                    })
 0220                    .Build();
 0221        await _startupServer.StartAsync().ConfigureAwait(false);
 0222        IsAlive = true;
 0223    }
 224
 225    /// <summary>
 226    /// Stops the Setup server.
 227    /// </summary>
 228    /// <returns>A task. Duh.</returns>
 229    public async Task StopAsync()
 230    {
 0231        ThrowIfDisposed();
 0232        if (_startupServer is null)
 233        {
 0234            throw new InvalidOperationException("Tried to stop a non existing startup server");
 235        }
 236
 0237        await _startupServer.StopAsync().ConfigureAwait(false);
 0238        IsAlive = false;
 0239    }
 240
 241    /// <inheritdoc/>
 242    public void Dispose()
 243    {
 0244        if (_disposed)
 245        {
 0246            return;
 247        }
 248
 0249        _disposed = true;
 0250        _startupServer?.Dispose();
 0251        IsAlive = false;
 0252        LogQueue?.Clear();
 0253        LogQueue = null;
 0254    }
 255
 256    private void ThrowIfDisposed()
 257    {
 0258        ObjectDisposedException.ThrowIf(_disposed, this);
 0259    }
 260
 261    /// <summary>
 262    /// Reports the current startup activity shown to all clients in the startup UI header.
 263    /// Only pass generic, non-identifying text from <see cref="StartupActivity"/>.
 264    /// </summary>
 265    /// <param name="activity">A generic description such as <see cref="StartupActivity.PreparingMigrations"/>.</param>
 266    internal static void ReportActivity(string activity)
 267    {
 1188268        _currentActivity = activity;
 1188269    }
 270
 271    internal void SoftStop()
 272    {
 0273        _isUnhealthy = true;
 0274    }
 275
 276    private class SetupHealthcheck : IHealthCheck
 277    {
 278        private readonly SetupServer _startupServer;
 279
 280        public SetupHealthcheck(SetupServer startupServer)
 281        {
 0282            _startupServer = startupServer;
 0283        }
 284
 285        public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken 
 286        {
 0287            if (_startupServer._isUnhealthy)
 288            {
 0289                return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs."))
 290            }
 291
 0292            return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
 293        }
 294    }
 295
 296    internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable
 297    {
 298        private bool _disposed;
 299
 300        public ILogger CreateLogger(string categoryName)
 301        {
 0302            return new CatchingSetupServerLogger();
 303        }
 304
 305        public void Dispose()
 306        {
 0307            if (_disposed)
 308            {
 0309                return;
 310            }
 311
 0312            _disposed = true;
 0313        }
 314    }
 315
 316    internal sealed class CatchingSetupServerLogger : ILogger
 317    {
 318        public IDisposable? BeginScope<TState>(TState state)
 319            where TState : notnull
 320        {
 0321            return null;
 322        }
 323
 324        public bool IsEnabled(LogLevel logLevel)
 325        {
 0326            return logLevel is LogLevel.Error or LogLevel.Critical;
 327        }
 328
 329        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exc
 330        {
 0331            if (!IsEnabled(logLevel))
 332            {
 0333                return;
 334            }
 335
 0336            LogQueue?.Enqueue(new()
 0337            {
 0338                LogLevel = logLevel,
 0339                Content = formatter(state, exception),
 0340                DateOfCreation = DateTimeOffset.Now
 0341            });
 0342        }
 343    }
 344}