aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/ServerSetupApp/SetupServer.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server/ServerSetupApp/SetupServer.cs')
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs182
1 files changed, 169 insertions, 13 deletions
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 3d4810bd7..4340969a3 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -7,9 +10,11 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Configuration;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Networking.Manager;
+using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -20,6 +25,11 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
+using Morestachio;
+using Morestachio.Framework.IO.SingleStream;
+using Morestachio.Rendering;
+using Serilog;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server.ServerSetupApp;
@@ -34,8 +44,10 @@ public sealed class SetupServer : IDisposable
private readonly ILoggerFactory _loggerFactory;
private readonly IConfiguration _startupConfiguration;
private readonly ServerConfigurationManager _configurationManager;
+ private IRenderer? _startupUiRenderer;
private IHost? _startupServer;
private bool _disposed;
+ private bool _isUnhealthy;
/// <summary>
/// Initializes a new instance of the <see cref="SetupServer"/> class.
@@ -62,26 +74,92 @@ public sealed class SetupServer : IDisposable
_configurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
}
+ internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();
+
+ /// <summary>
+ /// Gets a value indicating whether Startup server is currently running.
+ /// </summary>
+ public bool IsAlive { get; internal set; }
+
/// <summary>
/// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
/// </summary>
/// <returns>A Task.</returns>
public async Task RunAsync()
{
+ var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
+ _startupUiRenderer = (await ParserOptionsBuilder.New()
+ .WithTemplate(fileTemplate)
+ .WithFormatter(
+ (StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
+ {
+ if (children.Any())
+ {
+ var maxLevel = logEntry.LogLevel;
+ var stack = new Stack<StartupLogTopic>(children);
+
+ while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level.
+ {
+ maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
+ foreach (var child in logEntry.Children)
+ {
+ stack.Push(child);
+ }
+ }
+
+ return maxLevel;
+ }
+
+ return logEntry.LogLevel;
+ },
+ "FormatLogLevel")
+ .WithFormatter(
+ (LogLevel logLevel) =>
+ {
+ switch (logLevel)
+ {
+ case LogLevel.Trace:
+ case LogLevel.Debug:
+ case LogLevel.None:
+ return "success";
+ case LogLevel.Information:
+ return "info";
+ case LogLevel.Warning:
+ return "warn";
+ case LogLevel.Error:
+ return "danger";
+ case LogLevel.Critical:
+ return "danger-strong";
+ }
+
+ return string.Empty;
+ },
+ "ToString")
+ .BuildAndParseAsync()
+ .ConfigureAwait(false))
+ .CreateCompiledRenderer();
+
ThrowIfDisposed();
- _startupServer = Host.CreateDefaultBuilder()
+ var retryAfterValue = TimeSpan.FromSeconds(5);
+ var config = _configurationManager.GetNetworkConfiguration()!;
+ _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
.UseConsoleLifetime()
+ .UseSerilog()
.ConfigureServices(serv =>
{
+ serv.AddSingleton(this);
serv.AddHealthChecks()
.AddCheck<SetupHealthcheck>("StartupCheck");
+ serv.Configure<ForwardedHeadersOptions>(options =>
+ {
+ ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);
+ });
})
.ConfigureWebHostDefaults(webHostBuilder =>
{
webHostBuilder
.UseKestrel((builderContext, options) =>
{
- var config = _configurationManager.GetNetworkConfiguration()!;
var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6);
knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6);
var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
@@ -99,7 +177,7 @@ public sealed class SetupServer : IDisposable
.Configure(app =>
{
app.UseHealthChecks("/health");
-
+ app.UseForwardedHeaders();
app.Map("/startup/logger", loggerRoute =>
{
loggerRoute.Run(async context =>
@@ -113,7 +191,7 @@ public sealed class SetupServer : IDisposable
var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath)
.EnumerateFiles()
- .OrderBy(f => f.CreationTimeUtc)
+ .OrderByDescending(f => f.CreationTimeUtc)
.FirstOrDefault()
?.FullName;
if (logFilePath is not null)
@@ -140,7 +218,7 @@ public sealed class SetupServer : IDisposable
if (jfApplicationHost is null)
{
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
- context.Response.Headers.RetryAfter = new StringValues("5");
+ context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture));
return;
}
@@ -158,24 +236,32 @@ public sealed class SetupServer : IDisposable
});
});
- app.Run((context) =>
+ app.Run(async (context) =>
{
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
- context.Response.Headers.RetryAfter = new StringValues("5");
+ context.Response.Headers.RetryAfter = new StringValues(retryAfterValue.TotalSeconds.ToString("000", CultureInfo.InvariantCulture));
context.Response.Headers.ContentType = new StringValues("text/html");
- context.Response.WriteAsync("<p>Jellyfin Server still starting. Please wait.</p>");
var networkManager = _networkManagerFactory();
- if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
- {
- context.Response.WriteAsync("<p>You can download the current logfiles <a href='/startup/logger'>here</a>.</p>");
- }
- return Task.CompletedTask;
+ var startupLogEntries = LogQueue?.ToArray() ?? [];
+ await _startupUiRenderer.RenderAsync(
+ new Dictionary<string, object>()
+ {
+ { "isInReportingMode", _isUnhealthy },
+ { "retryValue", retryAfterValue },
+ { "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version! },
+ { "logs", startupLogEntries },
+ { "networkManagerReady", networkManager is not null },
+ { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
+ },
+ new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
+ .ConfigureAwait(false);
});
});
})
.Build();
await _startupServer.StartAsync().ConfigureAwait(false);
+ IsAlive = true;
}
/// <summary>
@@ -191,6 +277,7 @@ public sealed class SetupServer : IDisposable
}
await _startupServer.StopAsync().ConfigureAwait(false);
+ IsAlive = false;
}
/// <inheritdoc/>
@@ -203,6 +290,9 @@ public sealed class SetupServer : IDisposable
_disposed = true;
_startupServer?.Dispose();
+ IsAlive = false;
+ LogQueue?.Clear();
+ LogQueue = null;
}
private void ThrowIfDisposed()
@@ -210,11 +300,77 @@ public sealed class SetupServer : IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
}
+ internal void SoftStop()
+ {
+ _isUnhealthy = true;
+ }
+
private class SetupHealthcheck : IHealthCheck
{
+ private readonly SetupServer _startupServer;
+
+ public SetupHealthcheck(SetupServer startupServer)
+ {
+ _startupServer = startupServer;
+ }
+
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
+ if (_startupServer._isUnhealthy)
+ {
+ return Task.FromResult(HealthCheckResult.Unhealthy("Server is could not complete startup. Check logs."));
+ }
+
return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
}
}
+
+ internal sealed class SetupLoggerFactory : ILoggerProvider, IDisposable
+ {
+ private bool _disposed;
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new CatchingSetupServerLogger();
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ }
+ }
+
+ internal sealed class CatchingSetupServerLogger : ILogger
+ {
+ public IDisposable? BeginScope<TState>(TState state)
+ where TState : notnull
+ {
+ return null;
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return logLevel is LogLevel.Error or LogLevel.Critical;
+ }
+
+ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ LogQueue?.Enqueue(new()
+ {
+ LogLevel = logLevel,
+ Content = formatter(state, exception),
+ DateOfCreation = DateTimeOffset.Now
+ });
+ }
+ }
}