mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-14 19:50:24 +01:00
Feature/version check in library migration (#14105)
This commit is contained in:
@@ -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;
|
||||
@@ -10,6 +13,7 @@ using Jellyfin.Networking.Manager;
|
||||
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 +24,9 @@ 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;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
@@ -34,8 +41,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,13 +71,73 @@ public sealed class SetupServer : IDisposable
|
||||
_configurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
|
||||
}
|
||||
|
||||
internal static ConcurrentQueue<StartupLogEntry>? 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("ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
|
||||
_startupUiRenderer = (await ParserOptionsBuilder.New()
|
||||
.WithTemplate(fileTemplate)
|
||||
.WithFormatter(
|
||||
(StartupLogEntry logEntry, IEnumerable<StartupLogEntry> children) =>
|
||||
{
|
||||
if (children.Any())
|
||||
{
|
||||
var maxLevel = logEntry.LogLevel;
|
||||
var stack = new Stack<StartupLogEntry>(children);
|
||||
|
||||
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != 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();
|
||||
var retryAfterValue = TimeSpan.FromSeconds(5);
|
||||
_startupServer = Host.CreateDefaultBuilder()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureServices(serv =>
|
||||
@@ -140,7 +209,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 +227,30 @@ 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 },
|
||||
{ "logs", startupLogEntries },
|
||||
{ "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 +266,7 @@ public sealed class SetupServer : IDisposable
|
||||
}
|
||||
|
||||
await _startupServer.StopAsync().ConfigureAwait(false);
|
||||
IsAlive = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -203,6 +279,9 @@ public sealed class SetupServer : IDisposable
|
||||
|
||||
_disposed = true;
|
||||
_startupServer?.Dispose();
|
||||
IsAlive = false;
|
||||
LogQueue?.Clear();
|
||||
LogQueue = null;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
@@ -210,11 +289,88 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class StartupLogEntry
|
||||
{
|
||||
public LogLevel LogLevel { get; set; }
|
||||
|
||||
public string? Content { get; set; }
|
||||
|
||||
public DateTimeOffset DateOfCreation { get; set; }
|
||||
|
||||
public List<StartupLogEntry> Children { get; set; } = [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user