SetupServer.cs 16 KB

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