SetupServer.cs 16 KB

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