SetupServer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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. namespace Jellyfin.Server.ServerSetupApp;
  31. /// <summary>
  32. /// Creates a fake application pipeline that will only exist for as long as the main app is not started.
  33. /// </summary>
  34. public sealed class SetupServer : IDisposable
  35. {
  36. private readonly Func<INetworkManager?> _networkManagerFactory;
  37. private readonly IApplicationPaths _applicationPaths;
  38. private readonly Func<IServerApplicationHost?> _serverFactory;
  39. private readonly ILoggerFactory _loggerFactory;
  40. private readonly IConfiguration _startupConfiguration;
  41. private readonly ServerConfigurationManager _configurationManager;
  42. private IRenderer? _startupUiRenderer;
  43. private IHost? _startupServer;
  44. private bool _disposed;
  45. private bool _isUnhealthy;
  46. /// <summary>
  47. /// Initializes a new instance of the <see cref="SetupServer"/> class.
  48. /// </summary>
  49. /// <param name="networkManagerFactory">The networkmanager.</param>
  50. /// <param name="applicationPaths">The application paths.</param>
  51. /// <param name="serverApplicationHostFactory">The servers application host.</param>
  52. /// <param name="loggerFactory">The logger factory.</param>
  53. /// <param name="startupConfiguration">The startup configuration.</param>
  54. public SetupServer(
  55. Func<INetworkManager?> networkManagerFactory,
  56. IApplicationPaths applicationPaths,
  57. Func<IServerApplicationHost?> serverApplicationHostFactory,
  58. ILoggerFactory loggerFactory,
  59. IConfiguration startupConfiguration)
  60. {
  61. _networkManagerFactory = networkManagerFactory;
  62. _applicationPaths = applicationPaths;
  63. _serverFactory = serverApplicationHostFactory;
  64. _loggerFactory = loggerFactory;
  65. _startupConfiguration = startupConfiguration;
  66. var xmlSerializer = new MyXmlSerializer();
  67. _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer);
  68. _configurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
  69. }
  70. internal static ConcurrentQueue<StartupLogEntry>? LogQueue { get; set; } = new();
  71. /// <summary>
  72. /// Gets a value indicating whether Startup server is currently running.
  73. /// </summary>
  74. public bool IsAlive { get; internal set; }
  75. /// <summary>
  76. /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
  77. /// </summary>
  78. /// <returns>A Task.</returns>
  79. public async Task RunAsync()
  80. {
  81. var fileTemplate = await File.ReadAllTextAsync(Path.Combine("ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
  82. _startupUiRenderer = (await ParserOptionsBuilder.New()
  83. .WithTemplate(fileTemplate)
  84. .WithFormatter(
  85. (StartupLogEntry logEntry, IEnumerable<StartupLogEntry> children) =>
  86. {
  87. if (children.Any())
  88. {
  89. var maxLevel = logEntry.LogLevel;
  90. var stack = new Stack<StartupLogEntry>(children);
  91. while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level.
  92. {
  93. maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
  94. foreach (var child in logEntry.Children)
  95. {
  96. stack.Push(child);
  97. }
  98. }
  99. return maxLevel;
  100. }
  101. return logEntry.LogLevel;
  102. },
  103. "FormatLogLevel")
  104. .WithFormatter(
  105. (LogLevel logLevel) =>
  106. {
  107. switch (logLevel)
  108. {
  109. case LogLevel.Trace:
  110. case LogLevel.Debug:
  111. case LogLevel.None:
  112. return "success";
  113. case LogLevel.Information:
  114. return "info";
  115. case LogLevel.Warning:
  116. return "warn";
  117. case LogLevel.Error:
  118. return "danger";
  119. case LogLevel.Critical:
  120. return "danger-strong";
  121. }
  122. return string.Empty;
  123. },
  124. "ToString")
  125. .BuildAndParseAsync()
  126. .ConfigureAwait(false))
  127. .CreateCompiledRenderer();
  128. ThrowIfDisposed();
  129. var retryAfterValue = TimeSpan.FromSeconds(5);
  130. _startupServer = Host.CreateDefaultBuilder()
  131. .UseConsoleLifetime()
  132. .ConfigureServices(serv =>
  133. {
  134. serv.AddHealthChecks()
  135. .AddCheck<SetupHealthcheck>("StartupCheck");
  136. })
  137. .ConfigureWebHostDefaults(webHostBuilder =>
  138. {
  139. webHostBuilder
  140. .UseKestrel((builderContext, options) =>
  141. {
  142. var config = _configurationManager.GetNetworkConfiguration()!;
  143. var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6);
  144. knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6);
  145. var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
  146. Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer(
  147. bindInterfaces,
  148. config.InternalHttpPort,
  149. null,
  150. null,
  151. _startupConfiguration,
  152. _applicationPaths,
  153. _loggerFactory.CreateLogger<SetupServer>(),
  154. builderContext,
  155. options);
  156. })
  157. .Configure(app =>
  158. {
  159. app.UseHealthChecks("/health");
  160. app.Map("/startup/logger", loggerRoute =>
  161. {
  162. loggerRoute.Run(async context =>
  163. {
  164. var networkManager = _networkManagerFactory();
  165. if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
  166. {
  167. context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
  168. return;
  169. }
  170. var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
  171. .EnumerateFiles()
  172. .OrderByDescending(f => f.CreationTimeUtc)
  173. .FirstOrDefault()
  174. ?.FullName;
  175. if (logFilePath is not null)
  176. {
  177. await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false);
  178. }
  179. });
  180. });
  181. app.Map("/System/Info/Public", systemRoute =>
  182. {
  183. systemRoute.Run(async context =>
  184. {
  185. var jfApplicationHost = _serverFactory();
  186. var retryCounter = 0;
  187. while (jfApplicationHost is null && retryCounter < 5)
  188. {
  189. await Task.Delay(500).ConfigureAwait(false);
  190. jfApplicationHost = _serverFactory();
  191. retryCounter++;
  192. }
  193. if (jfApplicationHost is null)
  194. {
  195. context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
  196. context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture));
  197. return;
  198. }
  199. var sysInfo = new PublicSystemInfo
  200. {
  201. Version = jfApplicationHost.ApplicationVersionString,
  202. ProductName = jfApplicationHost.Name,
  203. Id = jfApplicationHost.SystemId,
  204. ServerName = jfApplicationHost.FriendlyName,
  205. LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
  206. StartupWizardCompleted = false
  207. };
  208. await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
  209. });
  210. });
  211. app.Run(async (context) =>
  212. {
  213. context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
  214. context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture));
  215. context.Response.Headers.ContentType = new StringValues("text/html");
  216. var networkManager = _networkManagerFactory();
  217. var startupLogEntries = LogQueue?.ToArray() ?? [];
  218. await _startupUiRenderer.RenderAsync(
  219. new Dictionary<string, object>()
  220. {
  221. { "isInReportingMode", _isUnhealthy },
  222. { "retryValue", retryAfterValue },
  223. { "logs", startupLogEntries },
  224. { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
  225. },
  226. new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
  227. .ConfigureAwait(false);
  228. });
  229. });
  230. })
  231. .Build();
  232. await _startupServer.StartAsync().ConfigureAwait(false);
  233. IsAlive = true;
  234. }
  235. /// <summary>
  236. /// Stops the Setup server.
  237. /// </summary>
  238. /// <returns>A task. Duh.</returns>
  239. public async Task StopAsync()
  240. {
  241. ThrowIfDisposed();
  242. if (_startupServer is null)
  243. {
  244. throw new InvalidOperationException("Tried to stop a non existing startup server");
  245. }
  246. await _startupServer.StopAsync().ConfigureAwait(false);
  247. IsAlive = false;
  248. }
  249. /// <inheritdoc/>
  250. public void Dispose()
  251. {
  252. if (_disposed)
  253. {
  254. return;
  255. }
  256. _disposed = true;
  257. _startupServer?.Dispose();
  258. IsAlive = false;
  259. LogQueue?.Clear();
  260. LogQueue = null;
  261. }
  262. private void ThrowIfDisposed()
  263. {
  264. ObjectDisposedException.ThrowIf(_disposed, this);
  265. }
  266. internal void SoftStop()
  267. {
  268. _isUnhealthy = true;
  269. }
  270. private class SetupHealthcheck : IHealthCheck
  271. {
  272. private readonly SetupServer _startupServer;
  273. public SetupHealthcheck(SetupServer startupServer)
  274. {
  275. _startupServer = startupServer;
  276. }
  277. public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
  278. {
  279. if (_startupServer._isUnhealthy)
  280. {
  281. return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs."));
  282. }
  283. return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
  284. }
  285. }
  286. internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable
  287. {
  288. private bool _disposed;
  289. public ILogger CreateLogger(string categoryName)
  290. {
  291. return new CatchingSetupServerLogger();
  292. }
  293. public void Dispose()
  294. {
  295. if (_disposed)
  296. {
  297. return;
  298. }
  299. _disposed = true;
  300. }
  301. }
  302. internal sealed class CatchingSetupServerLogger : ILogger
  303. {
  304. public IDisposable? BeginScope<TState>(TState state)
  305. where TState : notnull
  306. {
  307. return null;
  308. }
  309. public bool IsEnabled(LogLevel logLevel)
  310. {
  311. return logLevel is LogLevel.Error or LogLevel.Critical;
  312. }
  313. public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
  314. {
  315. if (!IsEnabled(logLevel))
  316. {
  317. return;
  318. }
  319. LogQueue?.Enqueue(new()
  320. {
  321. LogLevel = logLevel,
  322. Content = formatter(state, exception),
  323. DateOfCreation = DateTimeOffset.Now
  324. });
  325. }
  326. }
  327. internal class StartupLogEntry
  328. {
  329. public LogLevel LogLevel { get; set; }
  330. public string? Content { get; set; }
  331. public DateTimeOffset DateOfCreation { get; set; }
  332. public List<StartupLogEntry> Children { get; set; } = [];
  333. }
  334. }