diff options
Diffstat (limited to 'Jellyfin.Server')
| -rw-r--r-- | Jellyfin.Server/CoreAppHost.cs | 35 | ||||
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 5 | ||||
| -rw-r--r-- | Jellyfin.Server/Jellyfin.Server.csproj | 11 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Server/Program.cs | 187 | ||||
| -rw-r--r-- | Jellyfin.Server/Properties/launchSettings.json | 17 | ||||
| -rw-r--r-- | Jellyfin.Server/Startup.cs | 11 | ||||
| -rw-r--r-- | Jellyfin.Server/StartupOptions.cs | 34 |
8 files changed, 220 insertions, 82 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 8b4b61e29..f678e714c 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,10 +1,13 @@ +using System; using System.Collections.Generic; using System.Reflection; +using Emby.Drawing; using Emby.Server.Implementations; +using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.IO; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Jellyfin.Server @@ -21,30 +24,40 @@ namespace Jellyfin.Server /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> /// <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) { } - /// <inheritdoc /> - public override bool CanSelfRestart => StartupOptions.RestartPath != null; + /// <inheritdoc/> + protected override void RegisterServices(IServiceCollection serviceCollection) + { + // Register an image encoder + bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable(); + Type imageEncoderType = useSkiaEncoder + ? typeof(SkiaEncoder) + : typeof(NullImageEncoder); + serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType); + + // Log a warning if the Skia encoder could not be used + if (!useSkiaEncoder) + { + Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); + } + + base.RegisterServices(serviceCollection); + } /// <inheritdoc /> protected override void RestartInternal() => Program.Restart(); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dd4f9cd23..71ef9a69a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,6 +71,11 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) + .AddJsonOptions(options => + { + // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. + options.JsonSerializerOptions.PropertyNamingPolicy = null; + }) .AddControllersAsServices(); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bc18f11fd..88114d999 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{07E39F42-A2C6-4B32-AF8C-725F957A73FF}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> @@ -36,8 +41,10 @@ <ItemGroup> <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="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" /> + <PackageReference Include="prometheus-net" Version="3.5.0" /> + <PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" /> <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" /> diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index 673f0e415..6f8e4a8ff 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -1,8 +1,6 @@ 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 diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7c3d0f277..9635cc6ec 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -13,20 +12,23 @@ using System.Threading.Tasks; using CommandLine; using Emby.Drawing; using Emby.Server.Implementations; +using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Globalization; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; 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.Events; using Serilog.Extensions.Logging; using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -112,10 +114,13 @@ 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); + await InitLoggingConfigFile(appPaths).ConfigureAwait(false); - CreateLogger(appConfig, appPaths); + // Create an instance of the application configuration to use for application startup + IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); + // Initialize logging framework + InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Log uncaught exceptions to the logging instead of std error @@ -179,24 +184,37 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), - GetImageEncoder(appPaths), - new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()), - appConfig); + new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>())); + try { + // If hosting the web client, validate the client content path + if (startupConfig.HostWebClient()) + { + string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager); + if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) + { + throw new InvalidOperationException( + "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" + + $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); + } + } + ServiceCollection serviceCollection = new ServiceCollection(); - await appHost.InitAsync(serviceCollection).ConfigureAwait(false); + appHost.Init(serviceCollection); - var host = CreateWebHostBuilder(appHost, serviceCollection).Build(); + var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, 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.FindParts(); + // 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; + await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await host.StartAsync().ConfigureAwait(false); + await webHost.StartAsync().ConfigureAwait(false); } catch { @@ -232,19 +250,30 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection) + private static IWebHostBuilder CreateWebHostBuilder( + ApplicationHost appHost, + IServiceCollection serviceCollection, + StartupOptions commandLineOpts, + IConfiguration startupConfig, + IApplicationPaths appPaths) { return new WebHostBuilder() - .UseKestrel(options => + .UseKestrel((builderContext, options) => { var addresses = appHost.ServerConfigurationManager .Configuration .LocalNetworkAddresses .Select(appHost.NormalizeConfiguredLocalAddress) .Where(i => i != null) - .ToList(); - if (addresses.Any()) + .ToHashSet(); + if (addresses.Any() && !addresses.Contains(IPAddress.Any)) { + if (!addresses.Contains(IPAddress.Loopback)) + { + // we must listen on loopback for LiveTV to function regardless of the settings + addresses.Add(IPAddress.Loopback); + } + foreach (var address in addresses) { _logger.LogInformation("Kestrel listening on {IpAddress}", address); @@ -252,10 +281,19 @@ namespace Jellyfin.Server if (appHost.EnableHttps && appHost.Certificate != null) { - options.Listen( - address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); + options.Listen(address, appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(appHost.Certificate); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + options.Listen(address, appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); } } } @@ -266,14 +304,24 @@ namespace Jellyfin.Server if (appHost.EnableHttps && appHost.Certificate != null) { - options.ListenAnyIP( - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); + options.ListenAnyIP(appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(appHost.Certificate); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + options.ListenAnyIP(appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); } } }) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) .UseSerilog() - .UseContentRoot(appHost.ContentRoot) .ConfigureServices(services => { // Merge the external ServiceCollection into ASP.NET DI @@ -396,9 +444,8 @@ namespace Jellyfin.Server // webDir // IF --webdir // ELSE IF $JELLYFIN_WEB_DIR - // ELSE use <bindir>/jellyfin-web + // ELSE <bindir>/jellyfin-web var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) { webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); @@ -445,38 +492,63 @@ 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"; + // Do nothing if the config file already exists string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); - - if (!File.Exists(configPath)) + if (File.Exists(configPath)) { - // For some reason the csproj name is used instead of the assembly name - await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath); - if (resource == null) - { - throw new InvalidOperationException( - string.Format( - CultureInfo.InvariantCulture, - "Invalid resource path: '{0}'", - ResourcePath)); - } - - await 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(StartupOptions commandLineOpts, IApplicationPaths appPaths) + { return new ConfigurationBuilder() + .ConfigureAppConfiguration(commandLineOpts, appPaths) + .Build(); + } + + private static IConfigurationBuilder ConfigureAppConfiguration( + this IConfigurationBuilder config, + StartupOptions commandLineOpts, + IApplicationPaths appPaths, + IConfiguration? startupConfig = null) + { + // Use the swagger API page as the default redirect path if not hosting the web client + var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; + if (startupConfig != null && !startupConfig.HostWebClient()) + { + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; + } + + return config .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddInMemoryCollection(ConfigurationOptions.Configuration) + .AddInMemoryCollection(inMemoryDefaultConfig) .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) .AddEnvironmentVariables("JELLYFIN_") - .Build(); + .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } - 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 { @@ -503,25 +575,6 @@ namespace Jellyfin.Server } } - private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths) - { - try - { - // Test if the native lib is available - SkiaEncoder.TestSkia(); - - return new SkiaEncoder( - _loggerFactory.CreateLogger<SkiaEncoder>(), - appPaths); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Skia not available. Will fallback to NullIMageEncoder."); - } - - return new NullImageEncoder(); - } - private static void StartNewInstance(StartupOptions options) { _logger.LogInformation("Starting new instance"); diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json new file mode 100644 index 000000000..b6e2bcf97 --- /dev/null +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "profiles": { + "Jellyfin.Server": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Jellyfin.Server (nowebclient)": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "commandLineArgs": "--nowebclient" + } + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 4d7d56e9d..8bcfd1350 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Prometheus; namespace Jellyfin.Server { @@ -69,9 +70,19 @@ namespace Jellyfin.Server app.UseJellyfinApiSwagger(); app.UseRouting(); app.UseAuthorization(); + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + app.UseHttpMetrics(); + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics"); + } }); app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 1fb1c5af8..6e15d058f 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -1,5 +1,8 @@ +using System.Collections.Generic; using CommandLine; using Emby.Server.Implementations; +using Emby.Server.Implementations.Updates; +using MediaBrowser.Controller.Extensions; namespace Jellyfin.Server { @@ -16,6 +19,12 @@ namespace Jellyfin.Server public string? DataDir { get; set; } /// <summary> + /// Gets or sets a value indicating whether the server should not host the web client. + /// </summary> + [Option("nowebclient", Required = false, HelpText = "Indicates that the web server should not host the web client.")] + public bool NoWebClient { get; set; } + + /// <summary> /// Gets or sets the path to the web directory. /// </summary> /// <value>The path to the web directory.</value> @@ -66,5 +75,30 @@ namespace Jellyfin.Server /// <inheritdoc /> [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] public string? RestartArgs { get; set; } + + /// <inheritdoc /> + [Option("plugin-manifest-url", Required = false, HelpText = "A custom URL for the plugin repository JSON manifest")] + public string? PluginManifestUrl { get; set; } + + /// <summary> + /// Gets the command line options as a dictionary that can be used in the .NET configuration system. + /// </summary> + /// <returns>The configuration dictionary.</returns> + public Dictionary<string, string> ConvertToConfig() + { + var config = new Dictionary<string, string>(); + + if (PluginManifestUrl != null) + { + config.Add(InstallationManager.PluginManifestUrlKey, PluginManifestUrl); + } + + if (NoWebClient) + { + config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString); + } + + return config; + } } } |
