| | 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 MediaBrowser.Common.Configuration; |
| | 14 | | using MediaBrowser.Common.Net; |
| | 15 | | using MediaBrowser.Controller; |
| | 16 | | using MediaBrowser.Model.IO; |
| | 17 | | using MediaBrowser.Model.System; |
| | 18 | | using Microsoft.AspNetCore.Builder; |
| | 19 | | using Microsoft.AspNetCore.Hosting; |
| | 20 | | using Microsoft.AspNetCore.Http; |
| | 21 | | using Microsoft.Extensions.Configuration; |
| | 22 | | using Microsoft.Extensions.DependencyInjection; |
| | 23 | | using Microsoft.Extensions.Diagnostics.HealthChecks; |
| | 24 | | using Microsoft.Extensions.Hosting; |
| | 25 | | using Microsoft.Extensions.Logging; |
| | 26 | | using Microsoft.Extensions.Primitives; |
| | 27 | | using Morestachio; |
| | 28 | | using Morestachio.Framework.IO.SingleStream; |
| | 29 | | using Morestachio.Rendering; |
| | 30 | |
|
| | 31 | | namespace 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> |
| | 36 | | public 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 | | { |
| 0 | 64 | | _networkManagerFactory = networkManagerFactory; |
| 0 | 65 | | _applicationPaths = applicationPaths; |
| 0 | 66 | | _serverFactory = serverApplicationHostFactory; |
| 0 | 67 | | _loggerFactory = loggerFactory; |
| 0 | 68 | | _startupConfiguration = startupConfiguration; |
| 0 | 69 | | var xmlSerializer = new MyXmlSerializer(); |
| 0 | 70 | | _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer); |
| 0 | 71 | | _configurationManager.RegisterConfiguration<NetworkConfigurationFactory>(); |
| 0 | 72 | | } |
| | 73 | |
|
| 1 | 74 | | 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 | | { |
| 0 | 275 | | if (_disposed) |
| | 276 | | { |
| 0 | 277 | | return; |
| | 278 | | } |
| | 279 | |
|
| 0 | 280 | | _disposed = true; |
| 0 | 281 | | _startupServer?.Dispose(); |
| 0 | 282 | | IsAlive = false; |
| 0 | 283 | | LogQueue?.Clear(); |
| 0 | 284 | | LogQueue = null; |
| 0 | 285 | | } |
| | 286 | |
|
| | 287 | | private void ThrowIfDisposed() |
| | 288 | | { |
| 0 | 289 | | ObjectDisposedException.ThrowIf(_disposed, this); |
| 0 | 290 | | } |
| | 291 | |
|
| | 292 | | internal void SoftStop() |
| | 293 | | { |
| 0 | 294 | | _isUnhealthy = true; |
| 0 | 295 | | } |
| | 296 | |
|
| | 297 | | private class SetupHealthcheck : IHealthCheck |
| | 298 | | { |
| | 299 | | private readonly SetupServer _startupServer; |
| | 300 | |
|
| | 301 | | public SetupHealthcheck(SetupServer startupServer) |
| | 302 | | { |
| 0 | 303 | | _startupServer = startupServer; |
| 0 | 304 | | } |
| | 305 | |
|
| | 306 | | public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken |
| | 307 | | { |
| 0 | 308 | | if (_startupServer._isUnhealthy) |
| | 309 | | { |
| 0 | 310 | | return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs.")) |
| | 311 | | } |
| | 312 | |
|
| 0 | 313 | | 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 | | { |
| 0 | 323 | | return new CatchingSetupServerLogger(); |
| | 324 | | } |
| | 325 | |
|
| | 326 | | public void Dispose() |
| | 327 | | { |
| 0 | 328 | | if (_disposed) |
| | 329 | | { |
| 0 | 330 | | return; |
| | 331 | | } |
| | 332 | |
|
| 0 | 333 | | _disposed = true; |
| 0 | 334 | | } |
| | 335 | | } |
| | 336 | |
|
| | 337 | | internal sealed class CatchingSetupServerLogger : ILogger |
| | 338 | | { |
| | 339 | | public IDisposable? BeginScope<TState>(TState state) |
| | 340 | | where TState : notnull |
| | 341 | | { |
| 0 | 342 | | return null; |
| | 343 | | } |
| | 344 | |
|
| | 345 | | public bool IsEnabled(LogLevel logLevel) |
| | 346 | | { |
| 0 | 347 | | 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 | | { |
| 0 | 352 | | if (!IsEnabled(logLevel)) |
| | 353 | | { |
| 0 | 354 | | return; |
| | 355 | | } |
| | 356 | |
|
| 0 | 357 | | LogQueue?.Enqueue(new() |
| 0 | 358 | | { |
| 0 | 359 | | LogLevel = logLevel, |
| 0 | 360 | | Content = formatter(state, exception), |
| 0 | 361 | | DateOfCreation = DateTimeOffset.Now |
| 0 | 362 | | }); |
| 0 | 363 | | } |
| | 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 | | } |