diff options
Diffstat (limited to 'Jellyfin.Server/Program.cs')
| -rw-r--r-- | Jellyfin.Server/Program.cs | 521 |
1 files changed, 371 insertions, 150 deletions
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 41ee73a56..45699f3af 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,64 +1,79 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; -using System.Net.Security; using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; +using System.Text; using System.Threading; using System.Threading.Tasks; using CommandLine; -using Emby.Drawing; using Emby.Server.Implementations; -using Emby.Server.Implementations.EnvironmentInfo; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; +using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Globalization; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Hosting; +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.AspNetCore; +using Serilog.Extensions.Logging; +using SQLitePCL; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server { + /// <summary> + /// Class containing the entry point of the application. + /// </summary> public static class Program { + /// <summary> + /// The name of logging configuration file containing application defaults. + /// </summary> + public const string LoggingConfigFileDefault = "logging.default.json"; + + /// <summary> + /// The name of the logging configuration file containing the system-specific override settings. + /// </summary> + public const string LoggingConfigFileSystem = "logging.json"; + private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static ILogger _logger; + private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; - private static IConfiguration appConfig; - public static async Task Main(string[] args) + /// <summary> + /// The entry point of the application. + /// </summary> + /// <param name="args">The command line arguments passed.</param> + /// <returns><see cref="Task" />.</returns> + public static Task Main(string[] args) { - // For backwards compatibility. - // Modify any input arguments now which start with single-hyphen to POSIX standard - // double-hyphen to allow parsing by CommandLineParser package. - const string pattern = @"^(-[^-\s]{2})"; // Match -xx, not -x, not --xx, not xx - const string substitution = @"-$1"; // Prepend with additional single-hyphen - var regex = new Regex(pattern); - - for (var i = 0; i < args.Length; i++) + static Task ErrorParsingArguments(IEnumerable<Error> errors) { - args[i] = regex.Replace(args[i], substitution); + Environment.ExitCode = 1; + return Task.CompletedTask; } // Parse the command line arguments and either start the app or exit indicating error - await Parser.Default.ParseArguments<StartupOptions>(args) - .MapResult( - options => StartApp(options), - errs => Task.FromResult(0)).ConfigureAwait(false); + return Parser.Default.ParseArguments<StartupOptions>(args) + .MapResult(StartApp, ErrorParsingArguments); } - public static void Shutdown() + /// <summary> + /// Shuts down the application. + /// </summary> + internal static void Shutdown() { if (!_tokenSource.IsCancellationRequested) { @@ -66,7 +81,10 @@ namespace Jellyfin.Server } } - public static void Restart() + /// <summary> + /// Restarts the application. + /// </summary> + internal static void Restart() { _restartOnShutdown = true; @@ -75,22 +93,39 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + // Log all uncaught exceptions to std error + static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => + Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString()); + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; + ServerApplicationPaths appPaths = CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false); + // Enable cl-va P010 interop for tonemapping on Intel VAAPI + Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1"); + Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1"); + + 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"); - AppDomain.CurrentDomain.UnhandledException += (sender, e) + // Log uncaught exceptions to the logging instead of std error + AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole; + AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); // Intercept Ctrl+C and Ctrl+Break - Console.CancelKeyPress += (sender, e) => + Console.CancelKeyPress += (_, e) => { if (_tokenSource.IsCancellationRequested) { @@ -104,7 +139,7 @@ namespace Jellyfin.Server }; // Register a SIGTERM handler - AppDomain.CurrentDomain.ProcessExit += (sender, e) => + AppDomain.CurrentDomain.ProcessExit += (_, _) => { if (_tokenSource.IsCancellationRequested) { @@ -116,45 +151,86 @@ namespace Jellyfin.Server Shutdown(); }; - _logger.LogInformation("Jellyfin version: {Version}", Assembly.GetEntryAssembly().GetName().Version); - - EnvironmentInfo environmentInfo = new EnvironmentInfo(GetOperatingSystem()); - ApplicationHost.LogEnvironmentInfo(_logger, appPaths, environmentInfo); - - SQLitePCL.Batteries_V2.Init(); + _logger.LogInformation( + "Jellyfin version: {Version}", + Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3)); - // Allow all https requests - ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; } ); + ApplicationHost.LogEnvironmentInfo(_logger, appPaths); - var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths); + PerformStaticInitialization(); + var serviceCollection = new ServiceCollection(); - using (var appHost = new CoreAppHost( + var appHost = new CoreAppHost( appPaths, _loggerFactory, options, - fileSystem, - environmentInfo, - new NullImageEncoder(), - new NetworkManager(_loggerFactory, environmentInfo), - appConfig)) + startupConfig, + new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), + serviceCollection); + + try { - await appHost.Init(new ServiceCollection()).ConfigureAwait(false); + // If hosting the web client, validate the client content path + if (startupConfig.HostWebClient()) + { + string? webContentPath = appHost.ConfigurationManager.ApplicationPaths.WebPath; + 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" + + $"'{ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); + } + } - appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); + appHost.Init(); - await appHost.RunStartupTasks().ConfigureAwait(false); + var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); - // TODO: read input for a stop command + // 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 { - // Block main thread until shutdown - await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); + await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false); } - catch (TaskCanceledException) + catch (Exception ex) when (ex is not TaskCanceledException) { - // Don't throw on cancellation + _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); + + stopWatch.Stop(); + + _logger.LogInformation("Startup complete {Time:g}", stopWatch.Elapsed); + + // Block main thread until shutdown + await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + // Don't throw on cancellation + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Error while starting server."); + } + finally + { + _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); + // Run before disposing the application + using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext(); + if (context.Database.IsSqlite()) + { + context.Database.ExecuteSqlRaw("PRAGMA optimize"); + } + + appHost.Dispose(); } if (_restartOnShutdown) @@ -164,36 +240,153 @@ namespace Jellyfin.Server } /// <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 (IPObject netAdd in addresses) + { + _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "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 = startupConfig.GetUnixSocketPath(); + if (string.IsNullOrEmpty(socketPath)) + { + var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + if (xdgRuntimeDir == null) + { + // Fall back to config dir + socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, "socket.sock"); + } + else + { + socketPath = Path.Join(xdgRuntimeDir, "jellyfin-socket"); + } + } + + // 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); + } + }) + .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 XDG approach is followed: - /// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + /// 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">StartupOptions</param> - /// <returns>ServerApplicationPaths</returns> + /// <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_PATH + // 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_PATH"); + 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"); + dataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "jellyfin"); } } - Directory.CreateDirectory(dataDir); - // configDir // IF --configdir // ELSE IF $JELLYFIN_CONFIG_DIR @@ -209,20 +402,26 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(configDir)) { - if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (options.DataDir != 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. + // $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 $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( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config"); } configDir = Path.Combine(configDir, "jellyfin"); @@ -237,27 +436,30 @@ namespace Jellyfin.Server // 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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + 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. + // $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 $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( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cache"); } cacheDir = Path.Combine(cacheDir, "jellyfin"); @@ -265,13 +467,28 @@ namespace Jellyfin.Server } } + // 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"); @@ -283,9 +500,17 @@ namespace Jellyfin.Server } } + // 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); @@ -297,101 +522,98 @@ namespace Jellyfin.Server Environment.Exit(1); } - return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir); + 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> + /// <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) { - string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json"); - - if (!File.Exists(configPath)) + // Do nothing if the config file already exists + string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); + if (File.Exists(configPath)) { - // For some reason the csproj name is used instead of the assembly name - using (Stream rscstr = typeof(Program).Assembly - .GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json")) - using (Stream fstr = File.Open(configPath, FileMode.CreateNew)) - { - await rscstr.CopyToAsync(fstr).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 = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await resource.CopyToAsync(dst).ConfigureAwait(false); + } + + /// <summary> + /// Create the application configuration. + /// </summary> + /// <param name="commandLineOpts">The command line options passed to the program.</param> + /// <param name="appPaths">The application paths.</param> + /// <returns>The application configuration.</returns> + public 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[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger"; + } + + return config .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddJsonFile("logging.json") + .AddInMemoryCollection(inMemoryDefaultConfig) + .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) + .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) .AddEnvironmentVariables("JELLYFIN_") - .AddInMemoryCollection(ConfigurationOptions.Configuration) - .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 { // Serilog.Log is used by SerilogLoggerFactory when no logger is specified - Serilog.Log.Logger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .Enrich.FromLogContext() + .Enrich.WithThreadId() .CreateLogger(); } catch (Exception ex) { - Serilog.Log.Logger = new LoggerConfiguration() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}") + Log.Logger = new LoggerConfiguration() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") .WriteTo.Async(x => x.File( Path.Combine(appPaths.LogDirectoryPath, "log_.log"), rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}")) + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", + encoding: Encoding.UTF8)) .Enrich.FromLogContext() + .Enrich.WithThreadId() .CreateLogger(); - Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); - } - } - - private static IImageEncoder GetImageEncoder( - IFileSystem fileSystem, - IApplicationPaths appPaths, - ILocalizationManager localizationManager) - { - try - { - return new SkiaEncoder(_loggerFactory, appPaths, fileSystem, localizationManager); - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}"); - } - - return new NullImageEncoder(); - } - - private static MediaBrowser.Model.System.OperatingSystem GetOperatingSystem() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - return MediaBrowser.Model.System.OperatingSystem.OSX; - case PlatformID.Win32NT: - return MediaBrowser.Model.System.OperatingSystem.Windows; - case PlatformID.Unix: - default: - { - string osDescription = RuntimeInformation.OSDescription; - if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase)) - { - return MediaBrowser.Model.System.OperatingSystem.Linux; - } - else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase)) - { - return MediaBrowser.Model.System.OperatingSystem.OSX; - } - else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase)) - { - return MediaBrowser.Model.System.OperatingSystem.BSD; - } - - throw new Exception($"Can't resolve OS with description: '{osDescription}'"); - } + Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); } } @@ -399,23 +621,22 @@ namespace Jellyfin.Server { _logger.LogInformation("Starting new instance"); - string module = options.RestartPath; + var module = options.RestartPath; if (string.IsNullOrWhiteSpace(module)) { - module = Environment.GetCommandLineArgs().First(); + module = Environment.GetCommandLineArgs()[0]; } string commandLineArgsString; - if (options.RestartArgs != null) { - commandLineArgsString = options.RestartArgs ?? string.Empty; + commandLineArgsString = options.RestartArgs; } else { commandLineArgsString = string.Join( - " ", + ' ', Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument)); } |
