aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/CoreAppHost.cs7
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj20
-rw-r--r--Jellyfin.Server/Migrations/IMigrationRoutine.cs28
-rw-r--r--Jellyfin.Server/Migrations/MigrationOptions.cs24
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs73
-rw-r--r--Jellyfin.Server/Migrations/MigrationsFactory.cs20
-rw-r--r--Jellyfin.Server/Migrations/MigrationsListStore.cs24
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs73
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs35
-rw-r--r--Jellyfin.Server/Program.cs115
-rw-r--r--Jellyfin.Server/Resources/Configuration/logging.json15
-rw-r--r--Jellyfin.Server/Startup.cs1
12 files changed, 368 insertions, 67 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 8b4b61e290..ed5968ad6f 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -23,23 +23,20 @@ namespace Jellyfin.Server
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="configuration">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost(
ServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
StartupOptions options,
IFileSystem fileSystem,
IImageEncoder imageEncoder,
- INetworkManager networkManager,
- IConfiguration configuration)
+ INetworkManager networkManager)
: base(
applicationPaths,
loggerFactory,
options,
fileSystem,
imageEncoder,
- networkManager,
- configuration)
+ networkManager)
{
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index a411121916..bc18f11fde 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -6,9 +6,6 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- </PropertyGroup>
-
- <PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -27,10 +24,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" />
- <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -38,15 +35,16 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="CommandLineParser" Version="2.6.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.1" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.1" />
+ <PackageReference Include="CommandLineParser" Version="2.7.82" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.1" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
+ <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
- <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.1" />
+ <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.2" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.2" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
new file mode 100644
index 0000000000..eab995d67e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations
+{
+ /// <summary>
+ /// Interface that describes a migration routine.
+ /// </summary>
+ internal interface IMigrationRoutine
+ {
+ /// <summary>
+ /// Gets the unique id for this migration. This should never be modified after the migration has been created.
+ /// </summary>
+ public Guid Id { get; }
+
+ /// <summary>
+ /// Gets the display name of the migration.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// Execute the migration routine.
+ /// </summary>
+ /// <param name="host">Host that hosts current version.</param>
+ /// <param name="logger">Host logger.</param>
+ public void Perform(CoreAppHost host, ILogger logger);
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs
new file mode 100644
index 0000000000..816dd9ee74
--- /dev/null
+++ b/Jellyfin.Server/Migrations/MigrationOptions.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Server.Migrations
+{
+ /// <summary>
+ /// Configuration part that holds all migrations that were applied.
+ /// </summary>
+ public class MigrationOptions
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrationOptions"/> class.
+ /// </summary>
+ public MigrationOptions()
+ {
+ Applied = new List<(Guid Id, string Name)>();
+ }
+
+ /// <summary>
+ /// Gets the list of applied migration routine names.
+ /// </summary>
+ public List<(Guid Id, string Name)> Applied { get; }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
new file mode 100644
index 0000000000..b5ea04dcac
--- /dev/null
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations
+{
+ /// <summary>
+ /// The class that knows which migrations to apply and how to apply them.
+ /// </summary>
+ public sealed class MigrationRunner
+ {
+ /// <summary>
+ /// The list of known migrations, in order of applicability.
+ /// </summary>
+ internal static readonly IMigrationRoutine[] Migrations =
+ {
+ new Routines.DisableTranscodingThrottling(),
+ new Routines.CreateUserLoggingConfigFile()
+ };
+
+ /// <summary>
+ /// Run all needed migrations.
+ /// </summary>
+ /// <param name="host">CoreAppHost that hosts current version.</param>
+ /// <param name="loggerFactory">Factory for making the logger.</param>
+ public static void Run(CoreAppHost host, ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger<MigrationRunner>();
+ var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
+
+ if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
+ {
+ // If startup wizard is not finished, this is a fresh install.
+ // Don't run any migrations, just mark all of them as applied.
+ logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
+ migrationOptions.Applied.AddRange(Migrations.Select(m => (m.Id, m.Name)));
+ host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+ return;
+ }
+
+ var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
+
+ for (var i = 0; i < Migrations.Length; i++)
+ {
+ var migrationRoutine = Migrations[i];
+ if (appliedMigrationIds.Contains(migrationRoutine.Id))
+ {
+ logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name);
+ continue;
+ }
+
+ logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name);
+
+ try
+ {
+ migrationRoutine.Perform(host, logger);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name);
+ throw;
+ }
+
+ // Mark the migration as completed
+ logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
+ migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
+ host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+ logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationsFactory.cs b/Jellyfin.Server/Migrations/MigrationsFactory.cs
new file mode 100644
index 0000000000..23c1b1ee6f
--- /dev/null
+++ b/Jellyfin.Server/Migrations/MigrationsFactory.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Server.Migrations
+{
+ /// <summary>
+ /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations.
+ /// </summary>
+ public class MigrationsFactory : IConfigurationFactory
+ {
+ /// <inheritdoc/>
+ public IEnumerable<ConfigurationStore> GetConfigurations()
+ {
+ return new[]
+ {
+ new MigrationsListStore()
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationsListStore.cs b/Jellyfin.Server/Migrations/MigrationsListStore.cs
new file mode 100644
index 0000000000..7a1ca66714
--- /dev/null
+++ b/Jellyfin.Server/Migrations/MigrationsListStore.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Server.Migrations
+{
+ /// <summary>
+ /// A configuration that lists all the migration routines that were applied.
+ /// </summary>
+ public class MigrationsListStore : ConfigurationStore
+ {
+ /// <summary>
+ /// The name of the configuration in the storage.
+ /// </summary>
+ public static readonly string StoreKey = "migrations";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrationsListStore"/> class.
+ /// </summary>
+ public MigrationsListStore()
+ {
+ ConfigurationType = typeof(MigrationOptions);
+ Key = StoreKey;
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
new file mode 100644
index 0000000000..3bc32c0478
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// Migration to initialize the user logging configuration file "logging.user.json".
+ /// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json,
+ /// otherwise a blank file will be created.
+ /// </summary>
+ internal class CreateUserLoggingConfigFile : IMigrationRoutine
+ {
+ /// <summary>
+ /// File history for logging.json as existed during this migration creation. The contents for each has been minified.
+ /// </summary>
+ private readonly List<string> _defaultConfigHistory = new List<string>
+ {
+ // 9a6c27947353585391e211aa88b925f81e8cd7b9
+ @"{""Serilog"":{""MinimumLevel"":{""Default"":""Information"",""Override"":{""Microsoft"":""Warning"",""System"":""Warning""}},""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+ // 71bdcd730705a714ee208eaad7290b7c68df3885
+ @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+ // a44936f97f8afc2817d3491615a7cfe1e31c251c
+ @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}",
+ // 7af3754a11ad5a4284f107997fb5419a010ce6f3
+ @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}",
+ // 60691349a11f541958e0b2247c9abc13cb40c9fb
+ @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}",
+ // 65fe243afbcc4b596cf8726708c1965cd34b5f68
+ @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {ThreadId} {SourceContext}: {Message:lj} {NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {ThreadId} {SourceContext}:{Message} {NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+ // 96c9af590494aa8137d5a061aaf1e68feee60b67
+ @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+ };
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
+
+ /// <inheritdoc/>
+ public string Name => "CreateLoggingConfigHeirarchy";
+
+ /// <inheritdoc/>
+ public void Perform(CoreAppHost host, ILogger logger)
+ {
+ var logDirectory = host.Resolve<IApplicationPaths>().ConfigurationDirectoryPath;
+ var existingConfigPath = Path.Combine(logDirectory, "logging.json");
+
+ // If the existing logging.json config file is unmodified, then 'reset' it by moving it to 'logging.old.json'
+ // NOTE: This config file has 'reloadOnChange: true', so this change will take effect immediately even though it has already been loaded
+ if (File.Exists(existingConfigPath) && ExistingConfigUnmodified(existingConfigPath))
+ {
+ File.Move(existingConfigPath, Path.Combine(logDirectory, "logging.old.json"));
+ }
+ }
+
+ /// <summary>
+ /// Check if the existing logging.json file has not been modified by the user by comparing it to all the
+ /// versions in our git history. Until now, the file has never been migrated after first creation so users
+ /// could have any version from the git history.
+ /// </summary>
+ /// <exception cref="IOException"><paramref name="oldConfigPath"/> does not exist or could not be read.</exception>
+ private bool ExistingConfigUnmodified(string oldConfigPath)
+ {
+ var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath));
+ return _defaultConfigHistory
+ .Select(historicalConfigText => JToken.Parse(historicalConfigText))
+ .Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson));
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
new file mode 100644
index 0000000000..673f0e4155
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -0,0 +1,35 @@
+using System;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// Disable transcode throttling for all installations since it is currently broken for certain video formats.
+ /// </summary>
+ internal class DisableTranscodingThrottling : IMigrationRoutine
+ {
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}");
+
+ /// <inheritdoc/>
+ public string Name => "DisableTranscodingThrottling";
+
+ /// <inheritdoc/>
+ public void Perform(CoreAppHost host, ILogger logger)
+ {
+ // Set EnableThrottling to false since it wasn't used before and may introduce issues
+ var encoding = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<EncodingOptions>("encoding");
+ if (encoding.EnableThrottling)
+ {
+ logger.LogInformation("Disabling transcoding throttling during migration");
+ encoding.EnableThrottling = false;
+
+ host.ServerConfigurationManager.SaveConfiguration("encoding", encoding);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 712990a1ee..e9e852349c 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -4,7 +4,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
-using System.Net.Security;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
@@ -27,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
+using Serilog.Events;
using Serilog.Extensions.Logging;
using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -38,6 +38,16 @@ namespace Jellyfin.Server
/// </summary>
public static class Program
{
+ /// <summary>
+ /// The name of logging configuration file containing application defaults.
+ /// </summary>
+ public static readonly string LoggingConfigFileDefault = "logging.default.json";
+
+ /// <summary>
+ /// The name of the logging configuration file containing the system-specific override settings.
+ /// </summary>
+ public static readonly string LoggingConfigFileSystem = "logging.json";
+
private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static ILogger _logger = NullLogger.Instance;
@@ -102,10 +112,12 @@ namespace Jellyfin.Server
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
- IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false);
-
- CreateLogger(appConfig, appPaths);
+ // Create an instance of the application configuration to use for application startup
+ await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
+ IConfiguration startupConfig = CreateAppConfiguration(appPaths);
+ // Initialize logging framework
+ InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error
@@ -169,23 +181,23 @@ namespace Jellyfin.Server
_loggerFactory,
options,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- new NullImageEncoder(),
- new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
- appConfig);
+ GetImageEncoder(appPaths),
+ new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
try
{
ServiceCollection serviceCollection = new ServiceCollection();
- await appHost.InitAsync(serviceCollection).ConfigureAwait(false);
+ await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false);
- var host = CreateWebHostBuilder(appHost, serviceCollection).Build();
+ var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build();
// A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection.
- appHost.ServiceProvider = host.Services;
+ appHost.ServiceProvider = webHost.Services;
appHost.FindParts();
+ Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
- await host.StartAsync().ConfigureAwait(false);
+ await webHost.StartAsync().ConfigureAwait(false);
}
catch
{
@@ -193,8 +205,6 @@ namespace Jellyfin.Server
throw;
}
- appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager);
-
await appHost.RunStartupTasksAsync().ConfigureAwait(false);
stopWatch.Stop();
@@ -223,7 +233,7 @@ namespace Jellyfin.Server
}
}
- private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection)
+ private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths)
{
return new WebHostBuilder()
.UseKestrel(options =>
@@ -238,7 +248,7 @@ namespace Jellyfin.Server
{
foreach (var address in addresses)
{
- _logger.LogInformation("Kestrel listening on {ipaddr}", address);
+ _logger.LogInformation("Kestrel listening on {IpAddress}", address);
options.Listen(address, appHost.HttpPort);
if (appHost.EnableHttps && appHost.Certificate != null)
@@ -263,6 +273,8 @@ namespace Jellyfin.Server
}
}
})
+ .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths))
+ .UseSerilog()
.UseContentRoot(appHost.ContentRoot)
.ConfigureServices(services =>
{
@@ -435,39 +447,51 @@ namespace Jellyfin.Server
return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
}
- private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths)
+ /// <summary>
+ /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
+ /// already.
+ /// </summary>
+ private static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
{
- const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
- string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
-
- if (!File.Exists(configPath))
+ // Do nothing if the config file already exists
+ string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
+ if (File.Exists(configPath))
{
- // For some reason the csproj name is used instead of the assembly name
- using (Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath))
- {
- if (resource == null)
- {
- throw new InvalidOperationException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Invalid resource path: '{0}'",
- ResourcePath));
- }
-
- using Stream dst = File.Open(configPath, FileMode.CreateNew);
- await resource.CopyToAsync(dst).ConfigureAwait(false);
- }
+ return;
}
+ // Get a stream of the resource contents
+ // NOTE: The .csproj name is used instead of the assembly name in the resource path
+ const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
+ await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
+
+ // Copy the resource contents to the expected file path for the config file
+ await using Stream dst = File.Open(configPath, FileMode.CreateNew);
+ await resource.CopyToAsync(dst).ConfigureAwait(false);
+ }
+
+ private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths)
+ {
return new ConfigurationBuilder()
+ .ConfigureAppConfiguration(appPaths)
+ .Build();
+ }
+
+ private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths)
+ {
+ return config
.SetBasePath(appPaths.ConfigurationDirectoryPath)
.AddInMemoryCollection(ConfigurationOptions.Configuration)
- .AddJsonFile("logging.json", false, true)
- .AddEnvironmentVariables("JELLYFIN_")
- .Build();
+ .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
+ .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables("JELLYFIN_");
}
- private static void CreateLogger(IConfiguration configuration, IApplicationPaths appPaths)
+ /// <summary>
+ /// Initialize Serilog using configuration and fall back to defaults on failure.
+ /// </summary>
+ private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
{
try
{
@@ -475,26 +499,26 @@ namespace Jellyfin.Server
Serilog.Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
}
catch (Exception ex)
{
Serilog.Log.Logger = new LoggerConfiguration()
- .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"))
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}"))
.Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
}
}
- private static IImageEncoder GetImageEncoder(
- IApplicationPaths appPaths,
- ILocalizationManager localizationManager)
+ private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths)
{
try
{
@@ -503,8 +527,7 @@ namespace Jellyfin.Server
return new SkiaEncoder(
_loggerFactory.CreateLogger<SkiaEncoder>(),
- appPaths,
- localizationManager);
+ appPaths);
}
catch (Exception ex)
{
diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json
index e85ef05afd..f64a85219d 100644
--- a/Jellyfin.Server/Resources/Configuration/logging.json
+++ b/Jellyfin.Server/Resources/Configuration/logging.json
@@ -1,11 +1,17 @@
{
"Serilog": {
- "MinimumLevel": "Information",
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "System": "Warning"
+ }
+ },
"WriteTo": [
{
"Name": "Console",
"Args": {
- "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
+ "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
},
{
@@ -20,12 +26,13 @@
"retainedFileCountLimit": 3,
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 100000000,
- "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}"
}
}
]
}
}
- ]
+ ],
+ "Enrich": [ "FromLogContext", "WithThreadId" ]
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 3ee5fb8b50..4d7d56e9d4 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -3,7 +3,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;