mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-28 10:30:57 +01:00
Merge pull request #17154 from joshuaboniface/enhance-startup-ux
Revamp startup UI for visual style and usability
This commit is contained in:
@@ -215,8 +215,11 @@ internal class JellyfinMigrationService
|
||||
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
|
||||
migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
|
||||
|
||||
var migrationIndex = 0;
|
||||
foreach (var item in migrations)
|
||||
{
|
||||
// Surface generic "Running migration X of Y" progress in the always-visible startup UI header.
|
||||
SetupServer.ReportActivity(StartupActivity.Migration(++migrationIndex, migrations.Length));
|
||||
var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
|
||||
try
|
||||
{
|
||||
|
||||
@@ -133,10 +133,12 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
SetupServer.ReportActivity(StartupActivity.CheckingStorage);
|
||||
StorageHelper.TestCommonPathsForStorageCapacity(appPaths, StartupLogger.Logger.With(_loggerFactory.CreateLogger<Startup>()).BeginGroup($"Storage Check"));
|
||||
|
||||
StartupHelpers.PerformStaticInitialization();
|
||||
|
||||
SetupServer.ReportActivity(StartupActivity.Initializing);
|
||||
await ApplyStartupMigrationAsync(appPaths, startupConfig, options).ConfigureAwait(false);
|
||||
|
||||
do
|
||||
@@ -195,6 +197,7 @@ namespace Jellyfin.Server
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_restoreFromBackup))
|
||||
{
|
||||
SetupServer.ReportActivity(StartupActivity.RestoringBackup);
|
||||
await appHost.ServiceProvider.GetService<IBackupService>()!.RestoreBackupAsync(_restoreFromBackup).ConfigureAwait(false);
|
||||
_restoreFromBackup = null;
|
||||
_restartOnShutdown = true;
|
||||
@@ -202,9 +205,13 @@ namespace Jellyfin.Server
|
||||
}
|
||||
|
||||
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(appHost.ServiceProvider);
|
||||
SetupServer.ReportActivity(StartupActivity.PreparingMigrations);
|
||||
await jellyfinMigrationService.PrepareSystemForMigration(_logger).ConfigureAwait(false);
|
||||
// "Preparing migrations" carries through the DB read; per-migration progress is reported
|
||||
// as "Running migration X of Y" from inside the step once the pending set is known.
|
||||
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
|
||||
|
||||
SetupServer.ReportActivity(StartupActivity.InitializingServices);
|
||||
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
|
||||
_appHost = appHost;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ 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;
|
||||
@@ -25,9 +24,6 @@ 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;
|
||||
|
||||
@@ -44,7 +40,8 @@ public sealed class SetupServer : IDisposable
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IConfiguration _startupConfiguration;
|
||||
private readonly ServerConfigurationManager _configurationManager;
|
||||
private IRenderer? _startupUiRenderer;
|
||||
private static volatile string _currentActivity = StartupActivity.Starting;
|
||||
private StartupUiRenderer? _startupUiRenderer;
|
||||
private IHost? _startupServer;
|
||||
private bool _disposed;
|
||||
private bool _isUnhealthy;
|
||||
@@ -76,6 +73,12 @@ public sealed class SetupServer : IDisposable
|
||||
|
||||
internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a generic, non-identifying summary of what startup is currently doing. This is shown in the
|
||||
/// always-visible header of the startup UI to unauthenticated clients, so it never contains server specific details.
|
||||
/// </summary>
|
||||
internal static string CurrentActivity => _currentActivity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Startup server is currently running.
|
||||
/// </summary>
|
||||
@@ -87,64 +90,9 @@ public sealed class SetupServer : IDisposable
|
||||
/// <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(
|
||||
(Version version, int arg) =>
|
||||
{
|
||||
// version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
|
||||
return version.ToString(arg);
|
||||
},
|
||||
"ToString")
|
||||
.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();
|
||||
ReportActivity(StartupActivity.Starting);
|
||||
_startupUiRenderer = await StartupUiRenderer.CreateAsync(
|
||||
Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
|
||||
|
||||
ThrowIfDisposed();
|
||||
var retryAfterValue = TimeSpan.FromSeconds(5);
|
||||
@@ -257,13 +205,14 @@ public sealed class SetupServer : IDisposable
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
{ "isInReportingMode", _isUnhealthy },
|
||||
{ "currentActivity", CurrentActivity },
|
||||
{ "retryValue", retryAfterValue },
|
||||
{ "version", 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))
|
||||
context.Response.BodyWriter.AsStream())
|
||||
.ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
@@ -309,6 +258,16 @@ public sealed class SetupServer : IDisposable
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports the current startup activity shown to all clients in the startup UI header.
|
||||
/// Only pass generic, non-identifying text from <see cref="StartupActivity"/>.
|
||||
/// </summary>
|
||||
/// <param name="activity">A generic description such as <see cref="StartupActivity.PreparingMigrations"/>.</param>
|
||||
internal static void ReportActivity(string activity)
|
||||
{
|
||||
_currentActivity = activity;
|
||||
}
|
||||
|
||||
internal void SoftStop()
|
||||
{
|
||||
_isUnhealthy = true;
|
||||
|
||||
41
Jellyfin.Server/ServerSetupApp/StartupActivity.cs
Normal file
41
Jellyfin.Server/ServerSetupApp/StartupActivity.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// A curated vocabulary of generic, non-identifying descriptions of what the server is doing during startup.
|
||||
/// These are shown in the always-visible header of the startup UI to <b>unauthenticated</b> clients, so every
|
||||
/// value must stay generic and must never contain server specific details (paths, names, plugin or migration ids, counts of items, etc.).
|
||||
/// </summary>
|
||||
public static class StartupActivity
|
||||
{
|
||||
/// <summary>The default state before any work has been reported.</summary>
|
||||
public const string Starting = "Starting up";
|
||||
|
||||
/// <summary>Validating that the configured storage locations are usable.</summary>
|
||||
public const string CheckingStorage = "Checking storage";
|
||||
|
||||
/// <summary>Bringing up the migration subsystem and running early startup checks.</summary>
|
||||
public const string Initializing = "Initializing server";
|
||||
|
||||
/// <summary>Preparing the system for migrations (e.g. taking safety backups).</summary>
|
||||
public const string PreparingMigrations = "Preparing migrations";
|
||||
|
||||
/// <summary>Restoring from a backup.</summary>
|
||||
public const string RestoringBackup = "Restoring backup";
|
||||
|
||||
/// <summary>Bringing up core services and plugins.</summary>
|
||||
public const string InitializingServices = "Initializing services";
|
||||
|
||||
/// <summary>Running the final startup tasks.</summary>
|
||||
public const string FinishingStartup = "Finishing startup";
|
||||
|
||||
/// <summary>
|
||||
/// Builds a generic "Running migration X of Y" description. Only the numeric position and total are exposed.
|
||||
/// </summary>
|
||||
/// <param name="current">The 1-based index of the migration currently running.</param>
|
||||
/// <param name="total">The total number of migrations in this batch.</param>
|
||||
/// <returns>A generic progress description.</returns>
|
||||
public static string Migration(int current, int total)
|
||||
=> string.Format(CultureInfo.InvariantCulture, "Running migration {0} of {1}", current, total);
|
||||
}
|
||||
109
Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs
Normal file
109
Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Morestachio;
|
||||
using Morestachio.Framework.IO.SingleStream;
|
||||
using Morestachio.Rendering;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles and renders the startup UI Morestachio template.
|
||||
/// Shared by the live <see cref="SetupServer"/> and the standalone startup UI preview tool so both
|
||||
/// exercise the exact same template and formatters.
|
||||
/// </summary>
|
||||
public sealed class StartupUiRenderer
|
||||
{
|
||||
private readonly IRenderer _renderer;
|
||||
|
||||
private StartupUiRenderer(IRenderer renderer)
|
||||
{
|
||||
_renderer = renderer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the startup UI template located at <paramref name="templatePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="templatePath">The full path to the <c>index.mstemplate.html</c> template.</param>
|
||||
/// <returns>A ready to use <see cref="StartupUiRenderer"/>.</returns>
|
||||
public static async Task<StartupUiRenderer> CreateAsync(string templatePath)
|
||||
{
|
||||
var fileTemplate = await File.ReadAllTextAsync(templatePath).ConfigureAwait(false);
|
||||
var renderer = (await ParserOptionsBuilder.New()
|
||||
.WithTemplate(fileTemplate)
|
||||
.WithFormatter(
|
||||
(Version version, int arg) =>
|
||||
{
|
||||
// version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
|
||||
return version.ToString(arg);
|
||||
},
|
||||
"ToString")
|
||||
.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();
|
||||
|
||||
return new StartupUiRenderer(renderer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the template with the provided model into the target stream.
|
||||
/// </summary>
|
||||
/// <param name="model">The values made available to the template.</param>
|
||||
/// <param name="output">The stream the rendered HTML is written to.</param>
|
||||
/// <returns>A Task.</returns>
|
||||
public Task RenderAsync(IDictionary<string, object> model, Stream output)
|
||||
{
|
||||
return _renderer.RenderAsync(
|
||||
model,
|
||||
new ByteCounterStream(output, IODefaults.FileStreamBufferSize, true, _renderer.ParserOptions));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user