diff options
Diffstat (limited to 'Jellyfin.Server')
35 files changed, 538 insertions, 1362 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 002193baf..40cd5a044 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Reflection; -using Emby.Drawing; using Emby.Server.Implementations; using Emby.Server.Implementations.Session; using Jellyfin.Api.WebSocketListeners; +using Jellyfin.Drawing; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; @@ -107,7 +107,7 @@ namespace Jellyfin.Server yield return typeof(CoreAppHost).Assembly; // Jellyfin.Server.Implementations - yield return typeof(JellyfinDb).Assembly; + yield return typeof(JellyfinDbContext).Assembly; } /// <inheritdoc /> diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index e29167747..463ca7321 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Jellyfin.Api.Middleware; using Jellyfin.Networking.Configuration; -using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e8a51c2aa..5065fbdbb 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -20,13 +20,13 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; -using Jellyfin.Server.Formatters; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs new file mode 100644 index 000000000..58d3e1b2d --- /dev/null +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Net; +using Jellyfin.Server.Helpers; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Extensions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Extensions; + +/// <summary> +/// Extensions for configuring the web host builder. +/// </summary> +public static class WebHostBuilderExtensions +{ + /// <summary> + /// Configure the web host builder. + /// </summary> + /// <param name="builder">The builder to configure.</param> + /// <param name="appHost">The application host.</param> + /// <param name="startupConfig">The application configuration.</param> + /// <param name="appPaths">The application paths.</param> + /// <param name="logger">The logger.</param> + /// <returns>The configured web host builder.</returns> + public static IWebHostBuilder ConfigureWebHostBuilder( + this IWebHostBuilder builder, + CoreAppHost appHost, + IConfiguration startupConfig, + IApplicationPaths appPaths, + ILogger logger) + { + return builder + .UseKestrel((builderContext, options) => + { + var addresses = appHost.NetManager.GetAllBindInterfaces(); + + bool flagged = false; + foreach (IPObject netAdd in addresses) + { + logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd); + options.Listen(netAdd.Address, appHost.HttpPort); + if (appHost.ListenWithHttps) + { + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps(appHost.Certificate)); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + try + { + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps()); + } + catch (InvalidOperationException) + { + if (!flagged) + { + logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); + flagged = true; + } + } + } + } + + // Bind to unix socket (only on unix systems) + if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) + { + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); + + // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + options.ListenUnixSocket(socketPath); + logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + } + }) + .UseStartup(_ => new Startup(appHost)); + } +} diff --git a/Jellyfin.Server/Filters/FileRequestFilter.cs b/Jellyfin.Server/Filters/FileRequestFilter.cs index 69e10994f..bb5d6a412 100644 --- a/Jellyfin.Server/Filters/FileRequestFilter.cs +++ b/Jellyfin.Server/Filters/FileRequestFilter.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters { if (attribute is AcceptsFileAttribute acceptsFileAttribute) { - operation.RequestBody = GetRequestBody(acceptsFileAttribute.GetContentTypes()); + operation.RequestBody = GetRequestBody(acceptsFileAttribute.ContentTypes); break; } } diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs index 544fdbfd6..1a4559d26 100644 --- a/Jellyfin.Server/Filters/FileResponseFilter.cs +++ b/Jellyfin.Server/Filters/FileResponseFilter.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Filters response.Value.Content.Clear(); // Add all content-types as file. - foreach (var contentType in producesFileAttribute.GetContentTypes()) + foreach (var contentType in producesFileAttribute.ContentTypes) { response.Value.Content.Add(contentType, _openApiMediaType); } diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs deleted file mode 100644 index ea8c5ecdb..000000000 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Jellyfin.Extensions.Json; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Net.Http.Headers; - -namespace Jellyfin.Server.Formatters -{ - /// <summary> - /// Camel Case Json Profile Formatter. - /// </summary> - public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter - { - /// <summary> - /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class. - /// </summary> - public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions) - { - SupportedMediaTypes.Clear(); - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType)); - } - } -} diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs deleted file mode 100644 index fdaa48f84..000000000 --- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Formatters; - -namespace Jellyfin.Server.Formatters -{ - /// <summary> - /// Css output formatter. - /// </summary> - public class CssOutputFormatter : TextOutputFormatter - { - /// <summary> - /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class. - /// </summary> - public CssOutputFormatter() - { - SupportedMediaTypes.Add("text/css"); - - SupportedEncodings.Add(Encoding.UTF8); - SupportedEncodings.Add(Encoding.Unicode); - } - - /// <summary> - /// Write context object to stream. - /// </summary> - /// <param name="context">Writer context.</param> - /// <param name="selectedEncoding">Unused. Writer encoding.</param> - /// <returns>Write stream task.</returns> - public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) - { - var stringResponse = context.Object?.ToString(); - return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); - } - } -} diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs deleted file mode 100644 index 03ca7dda7..000000000 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net.Mime; -using Jellyfin.Extensions.Json; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Net.Http.Headers; - -namespace Jellyfin.Server.Formatters -{ - /// <summary> - /// Pascal Case Json Profile Formatter. - /// </summary> - public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter - { - /// <summary> - /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class. - /// </summary> - public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions) - { - SupportedMediaTypes.Clear(); - // Add application/json for default formatter - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json)); - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType)); - } - } -} diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs deleted file mode 100644 index 156368d69..000000000 --- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Net.Mime; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Formatters; - -namespace Jellyfin.Server.Formatters -{ - /// <summary> - /// Xml output formatter. - /// </summary> - public class XmlOutputFormatter : TextOutputFormatter - { - /// <summary> - /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class. - /// </summary> - public XmlOutputFormatter() - { - SupportedMediaTypes.Clear(); - SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); - - SupportedEncodings.Add(Encoding.UTF8); - SupportedEncodings.Add(Encoding.Unicode); - } - - /// <inheritdoc /> - public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) - { - var stringResponse = context.Object?.ToString(); - return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); - } - } -} diff --git a/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs new file mode 100644 index 000000000..bf00dcd53 --- /dev/null +++ b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Jellyfin.Server.HealthChecks; + +/// <summary> +/// Implementation of the <see cref="DbContextHealthCheck{TContext}"/> for a <see cref="IDbContextFactory{TContext}"/>. +/// </summary> +/// <typeparam name="TContext">The type of database context.</typeparam> +public class DbContextFactoryHealthCheck<TContext> : IHealthCheck + where TContext : DbContext +{ + private readonly IDbContextFactory<TContext> _dbContextFactory; + + /// <summary> + /// Initializes a new instance of the <see cref="DbContextFactoryHealthCheck{TContext}"/> class. + /// </summary> + /// <param name="contextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param> + public DbContextFactoryHealthCheck(IDbContextFactory<TContext> contextFactory) + { + _dbContextFactory = contextFactory; + } + + /// <inheritdoc /> + public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + + var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + if (await dbContext.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false)) + { + return HealthCheckResult.Healthy(); + } + } + + return HealthCheckResult.Unhealthy(); + } +} diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs new file mode 100644 index 000000000..f1bb9b283 --- /dev/null +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -0,0 +1,326 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using Emby.Server.Implementations; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Serilog; +using SQLitePCL; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Jellyfin.Server.Helpers; + +/// <summary> +/// A class containing helper methods for server startup. +/// </summary> +public static class StartupHelpers +{ + /// <summary> + /// Create the data, config and log paths from the variety of inputs(command line args, + /// environment variables) or decide on what default to use. For Windows it's %AppPath% + /// for everything else the + /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a> + /// is followed. + /// </summary> + /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param> + /// <returns><see cref="ServerApplicationPaths" />.</returns> + public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) + { + // dataDir + // IF --datadir + // ELSE IF $JELLYFIN_DATA_DIR + // ELSE IF windows, use <%APPDATA%>/jellyfin + // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin + // ELSE use $HOME/.local/share/jellyfin + var dataDir = options.DataDir; + if (string.IsNullOrEmpty(dataDir)) + { + dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); + + if (string.IsNullOrEmpty(dataDir)) + { + // LocalApplicationData follows the XDG spec on unix machines + dataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "jellyfin"); + } + } + + // configDir + // IF --configdir + // ELSE IF $JELLYFIN_CONFIG_DIR + // ELSE IF --datadir, use <datadir>/config (assume portable run) + // ELSE IF <datadir>/config exists, use that + // ELSE IF windows, use <datadir>/config + // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin + // ELSE $HOME/.config/jellyfin + var configDir = options.ConfigDir; + if (string.IsNullOrEmpty(configDir)) + { + configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); + + if (string.IsNullOrEmpty(configDir)) + { + if (options.DataDir is not null + || Directory.Exists(Path.Combine(dataDir, "config")) + || OperatingSystem.IsWindows()) + { + // Hang config folder off already set dataDir + configDir = Path.Combine(dataDir, "config"); + } + else + { + // $XDG_CONFIG_HOME defines the base directory relative to which + // user specific configuration files should be stored. + configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + + // If $XDG_CONFIG_HOME is either not set or empty, + // a default equal to $HOME /.config should be used. + if (string.IsNullOrEmpty(configDir)) + { + configDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config"); + } + + configDir = Path.Combine(configDir, "jellyfin"); + } + } + } + + // cacheDir + // IF --cachedir + // ELSE IF $JELLYFIN_CACHE_DIR + // ELSE IF windows, use <datadir>/cache + // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin + // ELSE HOME/.cache/jellyfin + var cacheDir = options.CacheDir; + if (string.IsNullOrEmpty(cacheDir)) + { + cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); + + if (string.IsNullOrEmpty(cacheDir)) + { + if (OperatingSystem.IsWindows()) + { + // Hang cache folder off already set dataDir + cacheDir = Path.Combine(dataDir, "cache"); + } + else + { + // $XDG_CACHE_HOME defines the base directory relative to which + // user specific non-essential data files should be stored. + cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + + // If $XDG_CACHE_HOME is either not set or empty, + // a default equal to $HOME/.cache should be used. + if (string.IsNullOrEmpty(cacheDir)) + { + cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cache"); + } + + cacheDir = Path.Combine(cacheDir, "jellyfin"); + } + } + } + + // webDir + // IF --webdir + // ELSE IF $JELLYFIN_WEB_DIR + // ELSE <bindir>/jellyfin-web + var webDir = options.WebDir; + if (string.IsNullOrEmpty(webDir)) + { + webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); + + if (string.IsNullOrEmpty(webDir)) + { + // Use default location under ResourcesPath + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); + } + } + + // logDir + // IF --logdir + // ELSE IF $JELLYFIN_LOG_DIR + // ELSE IF --datadir, use <datadir>/log (assume portable run) + // ELSE <datadir>/log + var logDir = options.LogDir; + if (string.IsNullOrEmpty(logDir)) + { + logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); + + if (string.IsNullOrEmpty(logDir)) + { + // Hang log folder off already set dataDir + logDir = Path.Combine(dataDir, "log"); + } + } + + // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162 + dataDir = Path.GetFullPath(dataDir); + logDir = Path.GetFullPath(logDir); + configDir = Path.GetFullPath(configDir); + cacheDir = Path.GetFullPath(cacheDir); + webDir = Path.GetFullPath(webDir); + + // Ensure the main folders exist before we continue + try + { + Directory.CreateDirectory(dataDir); + Directory.CreateDirectory(logDir); + Directory.CreateDirectory(configDir); + Directory.CreateDirectory(cacheDir); + } + catch (IOException ex) + { + Console.Error.WriteLine("Error whilst attempting to create folder"); + Console.Error.WriteLine(ex.ToString()); + Environment.Exit(1); + } + + return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); + } + + /// <summary> + /// Gets the path for the unix socket Kestrel should bind to. + /// </summary> + /// <param name="startupConfig">The startup config.</param> + /// <param name="appPaths">The application paths.</param> + /// <returns>The path for Kestrel to bind to.</returns> + public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths) + { + var socketPath = startupConfig.GetUnixSocketPath(); + + if (string.IsNullOrEmpty(socketPath)) + { + var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + var socketFile = "jellyfin.sock"; + if (xdgRuntimeDir is null) + { + // Fall back to config dir + socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile); + } + else + { + socketPath = Path.Join(xdgRuntimeDir, socketFile); + } + } + + return socketPath; + } + + /// <summary> + /// Sets the unix file permissions for Kestrel's socket file. + /// </summary> + /// <param name="startupConfig">The startup config.</param> + /// <param name="socketPath">The socket path.</param> + /// <param name="logger">The logger.</param> + [UnsupportedOSPlatform("windows")] + public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger) + { + var socketPerms = startupConfig.GetUnixSocketPermissions(); + + if (!string.IsNullOrEmpty(socketPerms)) + { + File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8)); + logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms); + } + } + + /// <summary> + /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist + /// already. + /// </summary> + /// <param name="appPaths">The application paths.</param> + /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns> + public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) + { + // Do nothing if the config file already exists + string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault); + if (File.Exists(configPath)) + { + 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"; + Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) + ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); + await using (resource.ConfigureAwait(false)) + { + Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (dst.ConfigureAwait(false)) + { + // Copy the resource contents to the expected file path for the config file + await resource.CopyToAsync(dst).ConfigureAwait(false); + } + } + } + + /// <summary> + /// Initialize Serilog using configuration and fall back to defaults on failure. + /// </summary> + /// <param name="configuration">The configuration object.</param> + /// <param name="appPaths">The application paths.</param> + public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) + { + try + { + // Serilog.Log is used by SerilogLoggerFactory when no logger is specified + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .CreateLogger(); + } + catch (Exception ex) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}", + formatProvider: CultureInfo.InvariantCulture) + .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}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", + formatProvider: CultureInfo.InvariantCulture, + encoding: Encoding.UTF8)) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .CreateLogger(); + + Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); + } + } + + /// <summary> + /// Call static initialization methods for the application. + /// </summary> + public static void PerformStaticInitialization() + { + // Make sure we have all the code pages we can get + // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // Increase the max http request limit + // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. + ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); + + // Disable the "Expect: 100-Continue" header by default + // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c + ServicePointManager.Expect100Continue = false; + + Batteries_V2.Init(); + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index ac2086935..9ea8508f2 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -24,7 +24,7 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> + <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> @@ -37,24 +37,24 @@ <PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.1" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.1" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.2" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.2" /> <PackageReference Include="prometheus-net" Version="7.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" /> - <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> + <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" /> - <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.3" /> + <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> + <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" /> <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> - <ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" /> + <ProjectReference Include="..\src\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" /> <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" /> </ItemGroup> diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs deleted file mode 100644 index 6ee5bf38a..000000000 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; -using MediaBrowser.Controller.Configuration; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Redirect requests without baseurl prefix to the baseurl prefixed URL. - /// </summary> - public class BaseUrlRedirectionMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger<BaseUrlRedirectionMiddleware> _logger; - private readonly IConfiguration _configuration; - - /// <summary> - /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - /// <param name="logger">The logger.</param> - /// <param name="configuration">The application configuration.</param> - public BaseUrlRedirectionMiddleware( - RequestDelegate next, - ILogger<BaseUrlRedirectionMiddleware> logger, - IConfiguration configuration) - { - _next = next; - _logger = logger; - _configuration = configuration; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <param name="serverConfigurationManager">The server configuration manager.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) - { - var localPath = httpContext.Request.Path.ToString(); - var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; - - if (string.IsNullOrEmpty(localPath) - || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase) - || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - ) - { - // Redirect health endpoint - if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Redirecting /health check"); - httpContext.Response.Redirect(baseUrlPrefix + "/health"); - return; - } - - // Always redirect back to the default path if the base prefix is invalid or missing - _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); - - var port = httpContext.Request.Host.Port ?? -1; - var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri; - var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri; - var target = uri.MakeRelativeUri(redirectUri).ToString(); - _logger.LogDebug("Redirecting to {Target}", target); - - httpContext.Response.Redirect(target); - return; - } - - await _next(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs deleted file mode 100644 index 91dbce19a..000000000 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.IO; -using System.Net.Mime; -using System.Net.Sockets; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Exception Middleware. - /// </summary> - public class ExceptionMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger<ExceptionMiddleware> _logger; - private readonly IServerConfigurationManager _configuration; - private readonly IWebHostEnvironment _hostEnvironment; - - /// <summary> - /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class. - /// </summary> - /// <param name="next">Next request delegate.</param> - /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param> - public ExceptionMiddleware( - RequestDelegate next, - ILogger<ExceptionMiddleware> logger, - IServerConfigurationManager serverConfigurationManager, - IWebHostEnvironment hostEnvironment) - { - _next = next; - _logger = logger; - _configuration = serverConfigurationManager; - _hostEnvironment = hostEnvironment; - } - - /// <summary> - /// Invoke request. - /// </summary> - /// <param name="context">Request context.</param> - /// <returns>Task.</returns> - public async Task Invoke(HttpContext context) - { - try - { - await _next(context).ConfigureAwait(false); - } - catch (Exception ex) - { - if (context.Response.HasStarted) - { - _logger.LogWarning("The response has already started, the exception middleware will not be executed."); - throw; - } - - ex = GetActualException(ex); - - bool ignoreStackTrace = - ex is SocketException - || ex is IOException - || ex is OperationCanceledException - || ex is SecurityException - || ex is AuthenticationException - || ex is FileNotFoundException; - - if (ignoreStackTrace) - { - _logger.LogError( - "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", - ex.Message.TrimEnd('.'), - context.Request.Method, - context.Request.Path); - } - else - { - _logger.LogError( - ex, - "Error processing request. URL {Method} {Url}.", - context.Request.Method, - context.Request.Path); - } - - context.Response.StatusCode = GetStatusCode(ex); - context.Response.ContentType = MediaTypeNames.Text.Plain; - - // Don't send exception unless the server is in a Development environment - var errorContent = _hostEnvironment.IsDevelopment() - ? NormalizeExceptionMessage(ex.Message) - : "Error processing request."; - await context.Response.WriteAsync(errorContent).ConfigureAwait(false); - } - } - - private static Exception GetActualException(Exception ex) - { - if (ex is AggregateException agg) - { - var inner = agg.InnerException; - if (inner is not null) - { - return GetActualException(inner); - } - - var inners = agg.InnerExceptions; - if (inners.Count > 0) - { - return GetActualException(inners[0]); - } - } - - return ex; - } - - private static int GetStatusCode(Exception ex) - { - switch (ex) - { - case ArgumentException _: return StatusCodes.Status400BadRequest; - case AuthenticationException _: return StatusCodes.Status401Unauthorized; - case SecurityException _: return StatusCodes.Status403Forbidden; - case DirectoryNotFoundException _: - case FileNotFoundException _: - case ResourceNotFoundException _: return StatusCodes.Status404NotFound; - case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; - default: return StatusCodes.Status500InternalServerError; - } - } - - private string NormalizeExceptionMessage(string msg) - { - // Strip any information we don't want to reveal - return msg.Replace( - _configuration.ApplicationPaths.ProgramSystemPath, - string.Empty, - StringComparison.OrdinalIgnoreCase) - .Replace( - _configuration.ApplicationPaths.ProgramDataPath, - string.Empty, - StringComparison.OrdinalIgnoreCase); - } - } -} diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs deleted file mode 100644 index 0afcd61a0..000000000 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Validates the IP of requests coming from local networks wrt. remote access. - /// </summary> - public class IpBasedAccessValidationMiddleware - { - private readonly RequestDelegate _next; - - /// <summary> - /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - public IpBasedAccessValidationMiddleware(RequestDelegate next) - { - _next = next; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <param name="networkManager">The network manager.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext, INetworkManager networkManager) - { - if (httpContext.IsLocal()) - { - // Running locally. - await _next(httpContext).ConfigureAwait(false); - return; - } - - var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - - if (!networkManager.HasRemoteAccess(remoteIp)) - { - return; - } - - await _next(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs deleted file mode 100644 index 67bf24d2a..000000000 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Validates the LAN host IP based on application configuration. - /// </summary> - public class LanFilteringMiddleware - { - private readonly RequestDelegate _next; - - /// <summary> - /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - public LanFilteringMiddleware(RequestDelegate next) - { - _next = next; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <param name="networkManager">The network manager.</param> - /// <param name="serverConfigurationManager">The server configuration manager.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) - { - var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - - if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) - { - return; - } - - await _next(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs deleted file mode 100644 index b214299df..000000000 --- a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Removes /emby and /mediabrowser from requested route. - /// </summary> - public class LegacyEmbyRouteRewriteMiddleware - { - private const string EmbyPath = "/emby"; - private const string MediabrowserPath = "/mediabrowser"; - - private readonly RequestDelegate _next; - private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - /// <param name="logger">The logger.</param> - public LegacyEmbyRouteRewriteMiddleware( - RequestDelegate next, - ILogger<LegacyEmbyRouteRewriteMiddleware> logger) - { - _next = next; - _logger = logger; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext) - { - var localPath = httpContext.Request.Path.ToString(); - if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) - { - httpContext.Request.Path = localPath[EmbyPath.Length..]; - _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); - } - else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) - { - httpContext.Request.Path = localPath[MediabrowserPath.Length..]; - _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); - } - - await _next(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs deleted file mode 100644 index 24807ce38..000000000 --- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// URL decodes the querystring before binding. - /// </summary> - public class QueryStringDecodingMiddleware - { - private readonly RequestDelegate _next; - - /// <summary> - /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - public QueryStringDecodingMiddleware(RequestDelegate next) - { - _next = next; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext) - { - var feature = httpContext.Features.Get<IQueryFeature>(); - if (feature is not null) - { - httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature)); - } - - await _next(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs deleted file mode 100644 index 531897cd4..000000000 --- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Response time middleware. - /// </summary> - public class ResponseTimeMiddleware - { - private const string ResponseHeaderResponseTime = "X-Response-Time-ms"; - - private readonly RequestDelegate _next; - private readonly ILogger<ResponseTimeMiddleware> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class. - /// </summary> - /// <param name="next">Next request delegate.</param> - /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> - public ResponseTimeMiddleware( - RequestDelegate next, - ILogger<ResponseTimeMiddleware> logger) - { - _next = next; - _logger = logger; - } - - /// <summary> - /// Invoke request. - /// </summary> - /// <param name="context">Request context.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <returns>Task.</returns> - public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager) - { - var startTimestamp = Stopwatch.GetTimestamp(); - - var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning; - var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs; - context.Response.OnStarting(() => - { - var responseTime = Stopwatch.GetElapsedTime(startTimestamp); - var responseTimeMs = responseTime.TotalMilliseconds; - if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug( - "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}", - context.Request.GetDisplayUrl(), - context.GetNormalizedRemoteIp(), - responseTime, - context.Response.StatusCode); - } - - context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture); - return Task.CompletedTask; - }); - - // Call the next delegate/middleware in the pipeline - await this._next(context).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs deleted file mode 100644 index fabcd2da7..000000000 --- a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Redirect requests to robots.txt to web/robots.txt. - /// </summary> - public class RobotsRedirectionMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger<RobotsRedirectionMiddleware> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - /// <param name="logger">The logger.</param> - public RobotsRedirectionMiddleware( - RequestDelegate next, - ILogger<RobotsRedirectionMiddleware> logger) - { - _next = next; - _logger = logger; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext) - { - var localPath = httpContext.Request.Path.ToString(); - if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); - httpContext.Response.Redirect("web/robots.txt"); - return; - } - - await _next(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs deleted file mode 100644 index 2ec063392..000000000 --- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Net.Mime; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Model.Globalization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Shows a custom message during server startup. - /// </summary> - public class ServerStartupMessageMiddleware - { - private readonly RequestDelegate _next; - - /// <summary> - /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - public ServerStartupMessageMiddleware(RequestDelegate next) - { - _next = next; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <param name="serverApplicationHost">The server application host.</param> - /// <param name="localizationManager">The localization manager.</param> - /// <returns>The async task.</returns> - public async Task Invoke( - HttpContext httpContext, - IServerApplicationHost serverApplicationHost, - ILocalizationManager localizationManager) - { - if (serverApplicationHost.CoreStartupHasCompleted - || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase)) - { - await _next(httpContext).ConfigureAwait(false); - return; - } - - var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); - httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - httpContext.Response.ContentType = MediaTypeNames.Text.Html; - await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs deleted file mode 100644 index 2f1d79157..000000000 --- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Jellyfin.Extensions; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Primitives; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Defines the <see cref="UrlDecodeQueryFeature"/>. - /// </summary> - public class UrlDecodeQueryFeature : IQueryFeature - { - private IQueryCollection? _store; - - /// <summary> - /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class. - /// </summary> - /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param> - public UrlDecodeQueryFeature(IQueryFeature feature) - { - Query = feature.Query; - } - - /// <summary> - /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>. - /// </summary> - public IQueryCollection Query - { - get - { - return _store ?? QueryCollection.Empty; - } - - set - { - // Only interested in where the querystring is encoded which shows up as one key with nothing in the value. - if (value.Count != 1) - { - _store = value; - return; - } - - // Encoded querystrings have no value, so don't process anything if a value is present. - var (key, stringValues) = value.First(); - if (!string.IsNullOrEmpty(stringValues)) - { - _store = value; - return; - } - - if (!key.Contains('=', StringComparison.Ordinal)) - { - _store = value; - return; - } - - var pairs = new Dictionary<string, StringValues>(); - foreach (var pair in key.SpanSplit('&')) - { - var i = pair.IndexOf('='); - if (i == -1) - { - // encoded is an equals. - // We use TryAdd so duplicate keys get ignored - pairs.TryAdd(pair.ToString(), StringValues.Empty); - continue; - } - - var k = pair[..i].ToString(); - var v = pair[(i + 1)..].ToString(); - if (!pairs.TryAdd(k, new StringValues(v))) - { - pairs[k] = StringValues.Concat(pairs[k], v); - } - } - - _store = new QueryCollection(pairs); - } - } - } -} diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs deleted file mode 100644 index b7a5d2b34..000000000 --- a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Net; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Server.Middleware -{ - /// <summary> - /// Handles WebSocket requests. - /// </summary> - public class WebSocketHandlerMiddleware - { - private readonly RequestDelegate _next; - - /// <summary> - /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - public WebSocketHandlerMiddleware(RequestDelegate next) - { - _next = next; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <param name="webSocketManager">The WebSocket connection manager.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) - { - if (!httpContext.WebSockets.IsWebSocketRequest) - { - await _next(httpContext).ConfigureAwait(false); - return; - } - - await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index e9a45c140..23fb9e370 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -38,7 +38,6 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.AddPeopleQueryIndex), typeof(Routines.MigrateAuthenticationDb) }; diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs index f6d8c9cc0..9e12c2e6b 100644 --- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Server.Migrations.Routines /// <inheritdoc/> public void Perform() { - _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); + _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo }; _serverConfigurationManager.SaveConfiguration(); } } diff --git a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs deleted file mode 100644 index 6343c422d..000000000 --- a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.IO; -using MediaBrowser.Controller; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Jellyfin.Server.Migrations.Routines -{ - /// <summary> - /// Migration to add table indexes to optimize the Persons query. - /// </summary> - public class AddPeopleQueryIndex : IMigrationRoutine - { - private const string DbFilename = "library.db"; - private readonly ILogger<AddPeopleQueryIndex> _logger; - private readonly IServerApplicationPaths _serverApplicationPaths; - - /// <summary> - /// Initializes a new instance of the <see cref="AddPeopleQueryIndex"/> class. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger{AddPeopleQueryIndex}"/> interface.</param> - /// <param name="serverApplicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> - public AddPeopleQueryIndex(ILogger<AddPeopleQueryIndex> logger, IServerApplicationPaths serverApplicationPaths) - { - _logger = logger; - _serverApplicationPaths = serverApplicationPaths; - } - - /// <inheritdoc /> - public Guid Id => new Guid("DE009B59-BAAE-428D-A810-F67762DC05B8"); - - /// <inheritdoc /> - public string Name => "AddPeopleQueryIndex"; - - /// <inheritdoc /> - public bool PerformOnNewInstall => true; - - /// <inheritdoc /> - public void Perform() - { - var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename); - using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null); - _logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType"); - connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);"); - _logger.LogInformation("Creating index idx_PeopleNameListOrder"); - connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);"); - } - } -} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index bf66f75ff..e8a0af9f8 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines private const string DbFilename = "activitylog.db"; private readonly ILogger<MigrateActivityLogDb> _logger; - private readonly IDbContextFactory<JellyfinDb> _provider; + private readonly IDbContextFactory<JellyfinDbContext> _provider; private readonly IServerApplicationPaths _paths; /// <summary> @@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines /// <param name="logger">The logger.</param> /// <param name="paths">The server application paths.</param> /// <param name="provider">The database provider.</param> - public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDb> provider) + public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDbContext> provider) { _logger = logger; _provider = provider; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index bf1ea8233..09daae0ff 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines private const string DbFilename = "authentication.db"; private readonly ILogger<MigrateAuthenticationDb> _logger; - private readonly IDbContextFactory<JellyfinDb> _dbProvider; + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IServerApplicationPaths _appPaths; private readonly IUserManager _userManager; @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines /// <param name="userManager">The user manager.</param> public MigrateAuthenticationDb( ILogger<MigrateAuthenticationDb> logger, - IDbContextFactory<JellyfinDb> dbProvider, + IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationPaths appPaths, IUserManager userManager) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 0fad77cfe..4b692d14f 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateDisplayPreferencesDb> _logger; private readonly IServerApplicationPaths _paths; - private readonly IDbContextFactory<JellyfinDb> _provider; + private readonly IDbContextFactory<JellyfinDbContext> _provider; private readonly JsonSerializerOptions _jsonOptions; private readonly IUserManager _userManager; @@ -39,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines public MigrateDisplayPreferencesDb( ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, - IDbContextFactory<JellyfinDb> provider, + IDbContextFactory<JellyfinDbContext> provider, IUserManager userManager) { _logger = logger; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 2dbd82e8f..ea2f03302 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateUserDb> _logger; private readonly IServerApplicationPaths _paths; - private readonly IDbContextFactory<JellyfinDb> _provider; + private readonly IDbContextFactory<JellyfinDbContext> _provider; private readonly IXmlSerializer _xmlSerializer; /// <summary> @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines public MigrateUserDb( ILogger<MigrateUserDb> logger, IServerApplicationPaths paths, - IDbContextFactory<JellyfinDb> provider, + IDbContextFactory<JellyfinDbContext> provider, IXmlSerializer xmlSerializer) { _logger = logger; diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs index 394f14d63..9cfaec46f 100644 --- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs @@ -39,9 +39,9 @@ namespace Jellyfin.Server.Migrations.Routines public void Perform() { // Only add if repository list is empty - if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0) + if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0) { - _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); + _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo }; _serverConfigurationManager.SaveConfiguration(); } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 125a09478..25fe30a39 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,33 +1,26 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text; using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Server.Extensions; +using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.IO; -using Microsoft.AspNetCore.Hosting; +using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; -using SQLitePCL; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -48,8 +41,9 @@ namespace Jellyfin.Server /// </summary> public const string LoggingConfigFileSystem = "logging.json"; - private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static CancellationTokenSource _tokenSource = new(); + private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -94,14 +88,14 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { - var startTimestamp = Stopwatch.GetTimestamp(); + _startTimestamp = Stopwatch.GetTimestamp(); // Log all uncaught exceptions to std error static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => - Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString()); + Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject); AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; - ServerApplicationPaths appPaths = CreateApplicationPaths(options); + ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -110,13 +104,12 @@ namespace Jellyfin.Server Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1"); Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1"); - await InitLoggingConfigFile(appPaths).ConfigureAwait(false); + await StartupHelpers.InitLoggingConfigFile(appPaths).ConfigureAwait(false); // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); - // Initialize logging framework - InitializeLoggingFramework(startupConfig, appPaths); + StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Log uncaught exceptions to the logging instead of std error @@ -160,14 +153,14 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string? webContentPath = appPaths.WebPath; + var webContentPath = appPaths.WebPath; if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any()) { _logger.LogError( "The server is expected to host the web client, but the provided content directory is either " + "invalid or empty: {WebContentPath}. If you do not want to host the web client with the " + "server, you may set the '--nowebclient' command line flag, or set" + - "'{ConfigKey}=false' in your config settings.", + "'{ConfigKey}=false' in your config settings", webContentPath, HostWebClientKey); Environment.ExitCode = 1; @@ -175,48 +168,66 @@ namespace Jellyfin.Server } } - PerformStaticInitialization(); + StartupHelpers.PerformStaticInitialization(); Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory); + do + { + _restartOnShutdown = false; + await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); + + if (_restartOnShutdown) + { + _tokenSource = new CancellationTokenSource(); + _startTimestamp = Stopwatch.GetTimestamp(); + } + } while (_restartOnShutdown); + } + + private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) + { var appHost = new CoreAppHost( appPaths, _loggerFactory, options, startupConfig); + IHost? host = null; try { - var serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); - - var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); + host = Host.CreateDefaultBuilder() + .ConfigureServices(services => appHost.Init(services)) + .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) + .UseSerilog() + .Build(); - // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = webHost.Services; + // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. + appHost.ServiceProvider = host.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false); + await host.StartAsync(_tokenSource.Token).ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { - var socketPath = GetUnixSocketPath(startupConfig, appPaths); + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); - SetUnixSocketPermissions(startupConfig, socketPath); + StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); } } catch (Exception ex) when (ex is not TaskCanceledException) { - _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again."); + _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again"); throw; } await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); - _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(startTimestamp)); + _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); // Block main thread until shutdown await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); @@ -227,7 +238,7 @@ namespace Jellyfin.Server } catch (Exception ex) { - _logger.LogCritical(ex, "Error while starting server."); + _logger.LogCritical(ex, "Error while starting server"); } finally { @@ -236,7 +247,7 @@ namespace Jellyfin.Server { _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); // Run before disposing the application - var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false); + var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false); await using (context.ConfigureAwait(false)) { if (context.Database.IsSqlite()) @@ -247,308 +258,7 @@ namespace Jellyfin.Server } await appHost.DisposeAsync().ConfigureAwait(false); - } - - if (_restartOnShutdown) - { - StartNewInstance(options); - } - } - - /// <summary> - /// Call static initialization methods for the application. - /// </summary> - public static void PerformStaticInitialization() - { - // Make sure we have all the code pages we can get - // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - - // Increase the max http request limit - // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - - // Disable the "Expect: 100-Continue" header by default - // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c - ServicePointManager.Expect100Continue = false; - - Batteries_V2.Init(); - if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK) - { - _logger.LogWarning("Failed to enable shared cache for SQLite"); - } - } - - /// <summary> - /// Configure the web host builder. - /// </summary> - /// <param name="builder">The builder to configure.</param> - /// <param name="appHost">The application host.</param> - /// <param name="serviceCollection">The application service collection.</param> - /// <param name="commandLineOpts">The command line options passed to the application.</param> - /// <param name="startupConfig">The application configuration.</param> - /// <param name="appPaths">The application paths.</param> - /// <returns>The configured web host builder.</returns> - public static IWebHostBuilder ConfigureWebHostBuilder( - this IWebHostBuilder builder, - ApplicationHost appHost, - IServiceCollection serviceCollection, - StartupOptions commandLineOpts, - IConfiguration startupConfig, - IApplicationPaths appPaths) - { - return builder - .UseKestrel((builderContext, options) => - { - var addresses = appHost.NetManager.GetAllBindInterfaces(); - - bool flagged = false; - foreach (IPData netAdd in addresses) - { - _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd.Address); - options.Listen(netAdd.Address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen( - netAdd.Address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen( - netAdd.Address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps()); - } - catch (InvalidOperationException) - { - if (!flagged) - { - _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); - flagged = true; - } - } - } - } - - // Bind to unix socket (only on unix systems) - if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) - { - var socketPath = GetUnixSocketPath(startupConfig, appPaths); - options.ListenUnixSocket(socketPath); - _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); - } - }) - .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) - .UseSerilog() - .ConfigureServices(services => - { - // Merge the external ServiceCollection into ASP.NET DI - services.Add(serviceCollection); - }) - .UseStartup<Startup>(); - } - - /// <summary> - /// Create the data, config and log paths from the variety of inputs(command line args, - /// environment variables) or decide on what default to use. For Windows it's %AppPath% - /// for everything else the - /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a> - /// is followed. - /// </summary> - /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param> - /// <returns><see cref="ServerApplicationPaths" />.</returns> - private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) - { - // dataDir - // IF --datadir - // ELSE IF $JELLYFIN_DATA_DIR - // ELSE IF windows, use <%APPDATA%>/jellyfin - // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin - // ELSE use $HOME/.local/share/jellyfin - var dataDir = options.DataDir; - if (string.IsNullOrEmpty(dataDir)) - { - dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); - - if (string.IsNullOrEmpty(dataDir)) - { - // LocalApplicationData follows the XDG spec on unix machines - dataDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "jellyfin"); - } - } - - // configDir - // IF --configdir - // ELSE IF $JELLYFIN_CONFIG_DIR - // ELSE IF --datadir, use <datadir>/config (assume portable run) - // ELSE IF <datadir>/config exists, use that - // ELSE IF windows, use <datadir>/config - // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin - // ELSE $HOME/.config/jellyfin - var configDir = options.ConfigDir; - if (string.IsNullOrEmpty(configDir)) - { - configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); - - if (string.IsNullOrEmpty(configDir)) - { - if (options.DataDir is not null - || Directory.Exists(Path.Combine(dataDir, "config")) - || OperatingSystem.IsWindows()) - { - // Hang config folder off already set dataDir - configDir = Path.Combine(dataDir, "config"); - } - else - { - // $XDG_CONFIG_HOME defines the base directory relative to which - // user specific configuration files should be stored. - configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); - - // If $XDG_CONFIG_HOME is either not set or empty, - // a default equal to $HOME /.config should be used. - if (string.IsNullOrEmpty(configDir)) - { - configDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".config"); - } - - configDir = Path.Combine(configDir, "jellyfin"); - } - } - } - - // cacheDir - // IF --cachedir - // ELSE IF $JELLYFIN_CACHE_DIR - // ELSE IF windows, use <datadir>/cache - // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin - // ELSE HOME/.cache/jellyfin - var cacheDir = options.CacheDir; - if (string.IsNullOrEmpty(cacheDir)) - { - cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); - - if (string.IsNullOrEmpty(cacheDir)) - { - if (OperatingSystem.IsWindows()) - { - // Hang cache folder off already set dataDir - cacheDir = Path.Combine(dataDir, "cache"); - } - else - { - // $XDG_CACHE_HOME defines the base directory relative to which - // user specific non-essential data files should be stored. - cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); - - // If $XDG_CACHE_HOME is either not set or empty, - // a default equal to $HOME/.cache should be used. - if (string.IsNullOrEmpty(cacheDir)) - { - cacheDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cache"); - } - - cacheDir = Path.Combine(cacheDir, "jellyfin"); - } - } - } - - // webDir - // IF --webdir - // ELSE IF $JELLYFIN_WEB_DIR - // ELSE <bindir>/jellyfin-web - var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) - { - webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); - - if (string.IsNullOrEmpty(webDir)) - { - // Use default location under ResourcesPath - webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); - } - } - - // logDir - // IF --logdir - // ELSE IF $JELLYFIN_LOG_DIR - // ELSE IF --datadir, use <datadir>/log (assume portable run) - // ELSE <datadir>/log - var logDir = options.LogDir; - if (string.IsNullOrEmpty(logDir)) - { - logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); - - if (string.IsNullOrEmpty(logDir)) - { - // Hang log folder off already set dataDir - logDir = Path.Combine(dataDir, "log"); - } - } - - // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162 - dataDir = Path.GetFullPath(dataDir); - logDir = Path.GetFullPath(logDir); - configDir = Path.GetFullPath(configDir); - cacheDir = Path.GetFullPath(cacheDir); - webDir = Path.GetFullPath(webDir); - - // Ensure the main folders exist before we continue - try - { - Directory.CreateDirectory(dataDir); - Directory.CreateDirectory(logDir); - Directory.CreateDirectory(configDir); - Directory.CreateDirectory(cacheDir); - } - catch (IOException ex) - { - Console.Error.WriteLine("Error whilst attempting to create folder"); - Console.Error.WriteLine(ex.ToString()); - Environment.Exit(1); - } - - return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); - } - - /// <summary> - /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist - /// already. - /// </summary> - /// <param name="appPaths">The application paths.</param> - /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns> - public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) - { - // Do nothing if the config file already exists - string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); - if (File.Exists(configPath)) - { - 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"; - Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) - ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); - await using (resource.ConfigureAwait(false)) - { - Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await using (dst.ConfigureAwait(false)) - { - // Copy the resource contents to the expected file path for the config file - await resource.CopyToAsync(dst).ConfigureAwait(false); - } + host?.Dispose(); } } @@ -586,112 +296,5 @@ namespace Jellyfin.Server .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } - - /// <summary> - /// Initialize Serilog using configuration and fall back to defaults on failure. - /// </summary> - private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) - { - try - { - // Serilog.Log is used by SerilogLoggerFactory when no logger is specified - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .CreateLogger(); - } - catch (Exception ex) - { - Log.Logger = new LoggerConfiguration() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}", - formatProvider: CultureInfo.InvariantCulture) - .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}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", - formatProvider: CultureInfo.InvariantCulture, - encoding: Encoding.UTF8)) - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .CreateLogger(); - - Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); - } - } - - private static void StartNewInstance(StartupOptions options) - { - _logger.LogInformation("Starting new instance"); - - var module = options.RestartPath; - - if (string.IsNullOrWhiteSpace(module)) - { - module = Environment.GetCommandLineArgs()[0]; - } - - string commandLineArgsString; - if (options.RestartArgs is not null) - { - commandLineArgsString = options.RestartArgs; - } - else - { - commandLineArgsString = string.Join( - ' ', - Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument)); - } - - _logger.LogInformation("Executable: {0}", module); - _logger.LogInformation("Arguments: {0}", commandLineArgsString); - - Process.Start(module, commandLineArgsString); - } - - private static string NormalizeCommandLineArgument(string arg) - { - if (!arg.Contains(' ', StringComparison.Ordinal)) - { - return arg; - } - - return "\"" + arg + "\""; - } - - private static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths) - { - var socketPath = startupConfig.GetUnixSocketPath(); - - if (string.IsNullOrEmpty(socketPath)) - { - var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); - var socketFile = "jellyfin.sock"; - if (xdgRuntimeDir is null) - { - // Fall back to config dir - socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile); - } - else - { - socketPath = Path.Join(xdgRuntimeDir, socketFile); - } - } - - return socketPath; - } - - [UnsupportedOSPlatform("windows")] - private static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath) - { - var socketPerms = startupConfig.GetUnixSocketPermissions(); - - if (!string.IsNullOrEmpty(socketPerms)) - { - File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8)); - _logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms); - } - } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 49a57aa68..7abd2fbef 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,13 +5,14 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; +using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; +using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; -using Jellyfin.Server.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -34,20 +35,17 @@ namespace Jellyfin.Server /// </summary> public class Startup { - private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerApplicationHost _serverApplicationHost; + private readonly IServerConfigurationManager _serverConfigurationManager; /// <summary> /// Initializes a new instance of the <see cref="Startup" /> class. /// </summary> - /// <param name="serverConfigurationManager">The server configuration manager.</param> - /// <param name="serverApplicationHost">The server application host.</param> - public Startup( - IServerConfigurationManager serverConfigurationManager, - IServerApplicationHost serverApplicationHost) + /// <param name="appHost">The server application host.</param> + public Startup(CoreAppHost appHost) { - _serverConfigurationManager = serverConfigurationManager; - _serverApplicationHost = serverApplicationHost; + _serverApplicationHost = appHost; + _serverConfigurationManager = appHost.ConfigurationManager; } /// <summary> @@ -86,8 +84,7 @@ namespace Jellyfin.Server RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 }; - services - .AddHttpClient(NamedClient.Default, c => + services.AddHttpClient(NamedClient.Default, c => { c.DefaultRequestHeaders.UserAgent.Add(productHeader); c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); @@ -122,7 +119,7 @@ namespace Jellyfin.Server .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHealthChecks() - .AddDbContextCheck<JellyfinDb>(); + .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); services.AddHlsPlaylistGenerator(); } @@ -207,7 +204,7 @@ namespace Jellyfin.Server endpoints.MapControllers(); if (_serverConfigurationManager.Configuration.EnableMetrics) { - endpoints.MapMetrics("/metrics"); + endpoints.MapMetrics(); } endpoints.MapHealthChecks("/health"); diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 0d9f379e0..c3989751c 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -64,14 +64,6 @@ namespace Jellyfin.Server public string? PackageName { get; set; } /// <inheritdoc /> - [Option("restartpath", Required = false, HelpText = "Path to restart script.")] - public string? RestartPath { get; set; } - - /// <inheritdoc /> - [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] - public string? RestartArgs { get; set; } - - /// <inheritdoc /> [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")] public string? PublishedServerUrl { get; set; } |
