| | | 1 | | using System; |
| | | 2 | | using System.Collections.Concurrent; |
| | | 3 | | using System.Collections.Generic; |
| | | 4 | | using System.Globalization; |
| | | 5 | | using System.IO; |
| | | 6 | | using System.Linq; |
| | | 7 | | using System.Net; |
| | | 8 | | using System.Threading; |
| | | 9 | | using System.Threading.Tasks; |
| | | 10 | | using Emby.Server.Implementations.Configuration; |
| | | 11 | | using Emby.Server.Implementations.Serialization; |
| | | 12 | | using Jellyfin.Networking.Manager; |
| | | 13 | | using Jellyfin.Server.Extensions; |
| | | 14 | | using MediaBrowser.Common.Configuration; |
| | | 15 | | using MediaBrowser.Common.Net; |
| | | 16 | | using MediaBrowser.Controller; |
| | | 17 | | using MediaBrowser.Model.IO; |
| | | 18 | | using MediaBrowser.Model.System; |
| | | 19 | | using Microsoft.AspNetCore.Builder; |
| | | 20 | | using Microsoft.AspNetCore.Hosting; |
| | | 21 | | using Microsoft.AspNetCore.Http; |
| | | 22 | | using Microsoft.Extensions.Configuration; |
| | | 23 | | using Microsoft.Extensions.DependencyInjection; |
| | | 24 | | using Microsoft.Extensions.Diagnostics.HealthChecks; |
| | | 25 | | using Microsoft.Extensions.Hosting; |
| | | 26 | | using Microsoft.Extensions.Logging; |
| | | 27 | | using Microsoft.Extensions.Primitives; |
| | | 28 | | using Morestachio; |
| | | 29 | | using Morestachio.Framework.IO.SingleStream; |
| | | 30 | | using Morestachio.Rendering; |
| | | 31 | | using Serilog; |
| | | 32 | | using ILogger = Microsoft.Extensions.Logging.ILogger; |
| | | 33 | | |
| | | 34 | | namespace 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> |
| | | 39 | | public 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 | | { |
| | 0 | 67 | | _networkManagerFactory = networkManagerFactory; |
| | 0 | 68 | | _applicationPaths = applicationPaths; |
| | 0 | 69 | | _serverFactory = serverApplicationHostFactory; |
| | 0 | 70 | | _loggerFactory = loggerFactory; |
| | 0 | 71 | | _startupConfiguration = startupConfiguration; |
| | 0 | 72 | | var xmlSerializer = new MyXmlSerializer(); |
| | 0 | 73 | | _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer); |
| | 0 | 74 | | _configurationManager.RegisterConfiguration<NetworkConfigurationFactory>(); |
| | 0 | 75 | | } |
| | | 76 | | |
| | 1 | 77 | | 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()) is not null) // |
| | | 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 | | { |
| | 0 | 284 | | if (_disposed) |
| | | 285 | | { |
| | 0 | 286 | | return; |
| | | 287 | | } |
| | | 288 | | |
| | 0 | 289 | | _disposed = true; |
| | 0 | 290 | | _startupServer?.Dispose(); |
| | 0 | 291 | | IsAlive = false; |
| | 0 | 292 | | LogQueue?.Clear(); |
| | 0 | 293 | | LogQueue = null; |
| | 0 | 294 | | } |
| | | 295 | | |
| | | 296 | | private void ThrowIfDisposed() |
| | | 297 | | { |
| | 0 | 298 | | ObjectDisposedException.ThrowIf(_disposed, this); |
| | 0 | 299 | | } |
| | | 300 | | |
| | | 301 | | internal void SoftStop() |
| | | 302 | | { |
| | 0 | 303 | | _isUnhealthy = true; |
| | 0 | 304 | | } |
| | | 305 | | |
| | | 306 | | private class SetupHealthcheck : IHealthCheck |
| | | 307 | | { |
| | | 308 | | private readonly SetupServer _startupServer; |
| | | 309 | | |
| | | 310 | | public SetupHealthcheck(SetupServer startupServer) |
| | | 311 | | { |
| | 0 | 312 | | _startupServer = startupServer; |
| | 0 | 313 | | } |
| | | 314 | | |
| | | 315 | | public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken |
| | | 316 | | { |
| | 0 | 317 | | if (_startupServer._isUnhealthy) |
| | | 318 | | { |
| | 0 | 319 | | return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs.")) |
| | | 320 | | } |
| | | 321 | | |
| | 0 | 322 | | 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 | | { |
| | 0 | 332 | | return new CatchingSetupServerLogger(); |
| | | 333 | | } |
| | | 334 | | |
| | | 335 | | public void Dispose() |
| | | 336 | | { |
| | 0 | 337 | | if (_disposed) |
| | | 338 | | { |
| | 0 | 339 | | return; |
| | | 340 | | } |
| | | 341 | | |
| | 0 | 342 | | _disposed = true; |
| | 0 | 343 | | } |
| | | 344 | | } |
| | | 345 | | |
| | | 346 | | internal sealed class CatchingSetupServerLogger : ILogger |
| | | 347 | | { |
| | | 348 | | public IDisposable? BeginScope<TState>(TState state) |
| | | 349 | | where TState : notnull |
| | | 350 | | { |
| | 0 | 351 | | return null; |
| | | 352 | | } |
| | | 353 | | |
| | | 354 | | public bool IsEnabled(LogLevel logLevel) |
| | | 355 | | { |
| | 0 | 356 | | 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 | | { |
| | 0 | 361 | | if (!IsEnabled(logLevel)) |
| | | 362 | | { |
| | 0 | 363 | | return; |
| | | 364 | | } |
| | | 365 | | |
| | 0 | 366 | | LogQueue?.Enqueue(new() |
| | 0 | 367 | | { |
| | 0 | 368 | | LogLevel = logLevel, |
| | 0 | 369 | | Content = formatter(state, exception), |
| | 0 | 370 | | DateOfCreation = DateTimeOffset.Now |
| | 0 | 371 | | }); |
| | 0 | 372 | | } |
| | | 373 | | } |
| | | 374 | | } |