You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/Jellyfin.Server/Program.cs

432 lines
17 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
using static 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 = NullLogger.Instance;
private static bool _restartOnShutdown;
/// <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)
{
static Task ErrorParsingArguments(IEnumerable<Error> errors)
{
Environment.ExitCode = 1;
return Task.CompletedTask;
}
// Parse the command line arguments and either start the app or exit indicating error
return Parser.Default.ParseArguments<StartupOptions>(args)
.MapResult(StartApp, ErrorParsingArguments);
}
/// <summary>
/// Shuts down the application.
/// </summary>
internal static void Shutdown()
{
if (!_tokenSource.IsCancellationRequested)
{
_tokenSource.Cancel();
}
}
/// <summary>
/// Restarts the application.
/// </summary>
internal static void Restart()
{
_restartOnShutdown = true;
Shutdown();
}
private static async Task StartApp(StartupOptions options)
{
var 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());
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
// Enable cl-va P010 interop for tonemapping on Intel VAAPI
Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1");
Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1");
await StartupHelpers.InitLoggingConfigFile(appPaths).ConfigureAwait(false);
// Create an instance of the application configuration to use for application startup
IConfiguration startupConfig = CreateAppConfiguration(options, appPaths);
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
// 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 += (_, e) =>
{
if (_tokenSource.IsCancellationRequested)
{
return; // Already shutting down
}
e.Cancel = true;
_logger.LogInformation("Ctrl+C, shutting down");
Environment.ExitCode = 128 + 2;
Shutdown();
};
// Register a SIGTERM handler
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
{
if (_tokenSource.IsCancellationRequested)
{
return; // Already shutting down
}
_logger.LogInformation("Received a SIGTERM signal, shutting down");
Environment.ExitCode = 128 + 15;
Shutdown();
};
_logger.LogInformation(
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
string? 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.",
webContentPath,
HostWebClientKey);
Environment.ExitCode = 1;
return;
}
}
StartupHelpers.PerformStaticInitialization();
Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory);
var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
startupConfig);
try
{
var host = Host.CreateDefaultBuilder()
1 year ago
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
.UseSerilog()
.Build();
// 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 host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
var socketPath = GetUnixSocketPath(startupConfig, appPaths);
SetUnixSocketPermissions(startupConfig, socketPath);
}
}
catch (Exception ex) when (ex is not TaskCanceledException)
{
3 years ago
_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));
5 years ago
// 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
{
// Don't throw additional exception if startup failed.
if (appHost.ServiceProvider is not null)
{
_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);
await using (context.ConfigureAwait(false))
{
if (context.Database.IsSqlite())
{
await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
}
}
}
await appHost.DisposeAsync().ConfigureAwait(false);
}
if (_restartOnShutdown)
{
StartNewInstance(options);
}
}
/// <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>
/// <returns>The configured web host builder.</returns>
public static IWebHostBuilder ConfigureWebHostBuilder(
this IWebHostBuilder builder,
CoreAppHost appHost,
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 = 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));
}
/// <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 is not null && !startupConfig.HostWebClient())
{
inMemoryDefaultConfig[DefaultRedirectKey] = "api-docs/swagger";
}
return config
.SetBasePath(appPaths.ConfigurationDirectoryPath)
.AddInMemoryCollection(inMemoryDefaultConfig)
.AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
.AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
.AddEnvironmentVariables("JELLYFIN_")
.AddInMemoryCollection(commandLineOpts.ConvertToConfig());
}
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);
}
}
}
}