Merge pull request #567 from Bond-009/term

Shutdown gracefully when recieving a termination signal
This commit is contained in:
Joshua M. Boniface 2019-01-16 11:49:53 -05:00 committed by GitHub
commit 2b1e3aa45f

View file

@ -1,313 +1,346 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Security; using System.Net.Security;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading;
using Emby.Drawing; using System.Threading.Tasks;
using Emby.Drawing.Skia; using Emby.Drawing;
using Emby.Server.Implementations; using Emby.Drawing.Skia;
using Emby.Server.Implementations.EnvironmentInfo; using Emby.Server.Implementations;
using Emby.Server.Implementations.IO; using Emby.Server.Implementations.EnvironmentInfo;
using Emby.Server.Implementations.Networking; using Emby.Server.Implementations.IO;
using MediaBrowser.Common.Configuration; using Emby.Server.Implementations.Networking;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Common.Net;
using MediaBrowser.Model.IO; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System; using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Configuration; using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Configuration;
using Serilog; using Microsoft.Extensions.Logging;
using Serilog.AspNetCore; using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger; using Serilog.AspNetCore;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server
{ namespace Jellyfin.Server
public static class Program {
{ public static class Program
private static readonly TaskCompletionSource<bool> ApplicationTaskCompletionSource = new TaskCompletionSource<bool>(); {
private static ILoggerFactory _loggerFactory; private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private static ILogger _logger; private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static bool _restartOnShutdown; private static ILogger _logger;
private static bool _restartOnShutdown;
public static async Task<int> Main(string[] args)
{ public static async Task Main(string[] args)
StartupOptions options = new StartupOptions(args); {
Version version = Assembly.GetEntryAssembly().GetName().Version; StartupOptions options = new StartupOptions(args);
Version version = Assembly.GetEntryAssembly().GetName().Version;
if (options.ContainsOption("-v") || options.ContainsOption("--version"))
{ if (options.ContainsOption("-v") || options.ContainsOption("--version"))
Console.WriteLine(version.ToString()); {
return 0; Console.WriteLine(version.ToString());
} }
ServerApplicationPaths appPaths = createApplicationPaths(options); ServerApplicationPaths appPaths = createApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
await createLogger(appPaths); await createLogger(appPaths);
_loggerFactory = new SerilogLoggerFactory(); _logger = _loggerFactory.CreateLogger("Main");
_logger = _loggerFactory.CreateLogger("Main");
AppDomain.CurrentDomain.UnhandledException += (sender, e)
AppDomain.CurrentDomain.UnhandledException += (sender, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
// Intercept Ctrl+C and Ctrl+Break
_logger.LogInformation("Jellyfin version: {Version}", version); Console.CancelKeyPress += (sender, e) =>
{
EnvironmentInfo environmentInfo = new EnvironmentInfo(getOperatingSystem()); if (_tokenSource.IsCancellationRequested)
ApplicationHost.LogEnvironmentInfo(_logger, appPaths, environmentInfo); {
return; // Already shutting down
SQLitePCL.Batteries_V2.Init(); }
e.Cancel = true;
// Allow all https requests _logger.LogInformation("Ctrl+C, shutting down");
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); Environment.ExitCode = 128 + 2;
Shutdown();
var fileSystem = new ManagedFileSystem(_loggerFactory.CreateLogger("FileSystem"), environmentInfo, null, appPaths.TempDirectory, true); };
using (var appHost = new CoreAppHost( // Register a SIGTERM handler
appPaths, AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
_loggerFactory, {
options, if (_tokenSource.IsCancellationRequested)
fileSystem, {
environmentInfo, return; // Already shutting down
new NullImageEncoder(), }
new SystemEvents(_loggerFactory.CreateLogger("SystemEvents")), _logger.LogInformation("Received a SIGTERM signal, shutting down");
new NetworkManager(_loggerFactory.CreateLogger("NetworkManager"), environmentInfo))) Environment.ExitCode = 128 + 15;
{ Shutdown();
appHost.Init(); };
appHost.ImageProcessor.ImageEncoder = getImageEncoder(_logger, fileSystem, options, () => appHost.HttpClient, appPaths, environmentInfo, appHost.LocalizationManager); _logger.LogInformation("Jellyfin version: {Version}", version);
_logger.LogInformation("Running startup tasks"); EnvironmentInfo environmentInfo = new EnvironmentInfo(getOperatingSystem());
ApplicationHost.LogEnvironmentInfo(_logger, appPaths, environmentInfo);
await appHost.RunStartupTasks();
SQLitePCL.Batteries_V2.Init();
// TODO: read input for a stop command
// Block main thread until shutdown // Allow all https requests
await ApplicationTaskCompletionSource.Task; ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
_logger.LogInformation("Disposing app host"); var fileSystem = new ManagedFileSystem(_loggerFactory.CreateLogger("FileSystem"), environmentInfo, null, appPaths.TempDirectory, true);
}
using (var appHost = new CoreAppHost(
if (_restartOnShutdown) appPaths,
{ _loggerFactory,
StartNewInstance(options); options,
} fileSystem,
environmentInfo,
return 0; new NullImageEncoder(),
} new SystemEvents(_loggerFactory.CreateLogger("SystemEvents")),
new NetworkManager(_loggerFactory.CreateLogger("NetworkManager"), environmentInfo)))
private static ServerApplicationPaths createApplicationPaths(StartupOptions options) {
{ appHost.Init();
string programDataPath = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
if (string.IsNullOrEmpty(programDataPath)) appHost.ImageProcessor.ImageEncoder = getImageEncoder(_logger, fileSystem, options, () => appHost.HttpClient, appPaths, environmentInfo, appHost.LocalizationManager);
{
if (options.ContainsOption("-programdata")) _logger.LogInformation("Running startup tasks");
{
programDataPath = options.GetOption("-programdata"); await appHost.RunStartupTasks();
}
else // TODO: read input for a stop command
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) try
{ {
programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); // Block main thread until shutdown
} await Task.Delay(-1, _tokenSource.Token);
else }
{ catch (TaskCanceledException)
// $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. {
programDataPath = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); // Don't throw on cancellation
// If $XDG_DATA_HOME is either not set or empty, $HOME/.local/share should be used. }
if (string.IsNullOrEmpty(programDataPath))
{ _logger.LogInformation("Disposing app host");
programDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share"); }
}
} if (_restartOnShutdown)
programDataPath = Path.Combine(programDataPath, "jellyfin"); {
// Ensure the dir exists StartNewInstance(options);
Directory.CreateDirectory(programDataPath); }
} }
}
private static ServerApplicationPaths createApplicationPaths(StartupOptions options)
string configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); {
if (string.IsNullOrEmpty(configDir)) string programDataPath = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
{ if (string.IsNullOrEmpty(programDataPath))
if (options.ContainsOption("-configdir")) {
{ if (options.ContainsOption("-programdata"))
configDir = options.GetOption("-configdir"); {
} programDataPath = options.GetOption("-programdata");
else }
{ else
// Let BaseApplicationPaths set up the default value {
configDir = null; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
} {
} programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
}
string logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); else
if (string.IsNullOrEmpty(logDir)) {
{ // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored.
if (options.ContainsOption("-logdir")) programDataPath = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
{ // If $XDG_DATA_HOME is either not set or empty, $HOME/.local/share should be used.
logDir = options.GetOption("-logdir"); if (string.IsNullOrEmpty(programDataPath))
} {
else programDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");
{ }
// Let BaseApplicationPaths set up the default value }
logDir = null; programDataPath = Path.Combine(programDataPath, "jellyfin");
} // Ensure the dir exists
} Directory.CreateDirectory(programDataPath);
}
string appPath = AppContext.BaseDirectory; }
return new ServerApplicationPaths(programDataPath, appPath, appPath, logDir, configDir); string configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
} if (string.IsNullOrEmpty(configDir))
{
private static async Task createLogger(IApplicationPaths appPaths) if (options.ContainsOption("-configdir"))
{ {
try configDir = options.GetOption("-configdir");
{ }
string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json"); else
{
if (!File.Exists(configPath)) // Let BaseApplicationPaths set up the default value
{ configDir = null;
// 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)) string logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
{ if (string.IsNullOrEmpty(logDir))
await rscstr.CopyToAsync(fstr); {
} if (options.ContainsOption("-logdir"))
} {
var configuration = new ConfigurationBuilder() logDir = options.GetOption("-logdir");
.SetBasePath(appPaths.ConfigurationDirectoryPath) }
.AddJsonFile("logging.json") else
.AddEnvironmentVariables("JELLYFIN_") {
.Build(); // Let BaseApplicationPaths set up the default value
logDir = null;
// Serilog.Log is used by SerilogLoggerFactory when no logger is specified }
Serilog.Log.Logger = new LoggerConfiguration() }
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext() string appPath = AppContext.BaseDirectory;
.CreateLogger();
} return new ServerApplicationPaths(programDataPath, appPath, appPath, logDir, configDir);
catch (Exception ex) }
{
Serilog.Log.Logger = new LoggerConfiguration() private static async Task createLogger(IApplicationPaths appPaths)
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}") {
.WriteTo.Async(x => x.File( try
Path.Combine(appPaths.LogDirectoryPath, "log_.log"), {
rollingInterval: RollingInterval.Day, string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"))
.Enrich.FromLogContext() if (!File.Exists(configPath))
.CreateLogger(); {
// For some reason the csproj name is used instead of the assembly name
Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); using (Stream rscstr = typeof(Program).Assembly
} .GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json"))
} using (Stream fstr = File.Open(configPath, FileMode.CreateNew))
{
public static IImageEncoder getImageEncoder( await rscstr.CopyToAsync(fstr);
ILogger logger, }
IFileSystem fileSystem, }
StartupOptions startupOptions, var configuration = new ConfigurationBuilder()
Func<IHttpClient> httpClient, .SetBasePath(appPaths.ConfigurationDirectoryPath)
IApplicationPaths appPaths, .AddJsonFile("logging.json")
IEnvironmentInfo environment, .AddEnvironmentVariables("JELLYFIN_")
ILocalizationManager localizationManager) .Build();
{
try // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
{ Serilog.Log.Logger = new LoggerConfiguration()
return new SkiaEncoder(logger, appPaths, httpClient, fileSystem, localizationManager); .ReadFrom.Configuration(configuration)
} .Enrich.FromLogContext()
catch (Exception ex) .CreateLogger();
{ }
logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}"); catch (Exception ex)
} {
Serilog.Log.Logger = new LoggerConfiguration()
return new NullImageEncoder(); .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}")
} .WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
private static MediaBrowser.Model.System.OperatingSystem getOperatingSystem() { rollingInterval: RollingInterval.Day,
switch (Environment.OSVersion.Platform) outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"))
{ .Enrich.FromLogContext()
case PlatformID.MacOSX: .CreateLogger();
return MediaBrowser.Model.System.OperatingSystem.OSX;
case PlatformID.Win32NT: Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
return MediaBrowser.Model.System.OperatingSystem.Windows; }
case PlatformID.Unix: }
default:
{ public static IImageEncoder getImageEncoder(
string osDescription = RuntimeInformation.OSDescription; ILogger logger,
if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase)) IFileSystem fileSystem,
{ StartupOptions startupOptions,
return MediaBrowser.Model.System.OperatingSystem.Linux; Func<IHttpClient> httpClient,
} IApplicationPaths appPaths,
else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase)) IEnvironmentInfo environment,
{ ILocalizationManager localizationManager)
return MediaBrowser.Model.System.OperatingSystem.OSX; {
} try
else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase)) {
{ return new SkiaEncoder(logger, appPaths, httpClient, fileSystem, localizationManager);
return MediaBrowser.Model.System.OperatingSystem.BSD; }
} catch (Exception ex)
throw new Exception($"Can't resolve OS with description: '{osDescription}'"); {
} logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}");
} }
}
return new NullImageEncoder();
public static void Shutdown() }
{
ApplicationTaskCompletionSource.SetResult(true); private static MediaBrowser.Model.System.OperatingSystem getOperatingSystem() {
} switch (Environment.OSVersion.Platform)
{
public static void Restart() case PlatformID.MacOSX:
{ return MediaBrowser.Model.System.OperatingSystem.OSX;
_restartOnShutdown = true; case PlatformID.Win32NT:
return MediaBrowser.Model.System.OperatingSystem.Windows;
Shutdown(); case PlatformID.Unix:
} default:
{
private static void StartNewInstance(StartupOptions startupOptions) string osDescription = RuntimeInformation.OSDescription;
{ if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase))
_logger.LogInformation("Starting new instance"); {
return MediaBrowser.Model.System.OperatingSystem.Linux;
string module = startupOptions.GetOption("-restartpath"); }
else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrWhiteSpace(module)) {
{ return MediaBrowser.Model.System.OperatingSystem.OSX;
module = Environment.GetCommandLineArgs().First(); }
} else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase))
{
string commandLineArgsString; return MediaBrowser.Model.System.OperatingSystem.BSD;
}
if (startupOptions.ContainsOption("-restartargs")) throw new Exception($"Can't resolve OS with description: '{osDescription}'");
{ }
commandLineArgsString = startupOptions.GetOption("-restartargs") ?? string.Empty; }
} }
else
{ public static void Shutdown()
commandLineArgsString = string .Join(" ", {
Environment.GetCommandLineArgs() if (!_tokenSource.IsCancellationRequested)
.Skip(1) {
.Select(NormalizeCommandLineArgument) _tokenSource.Cancel();
); }
} }
_logger.LogInformation("Executable: {0}", module); public static void Restart()
_logger.LogInformation("Arguments: {0}", commandLineArgsString); {
_restartOnShutdown = true;
Process.Start(module, commandLineArgsString);
} Shutdown();
}
private static string NormalizeCommandLineArgument(string arg)
{ private static void StartNewInstance(StartupOptions startupOptions)
if (!arg.Contains(" ", StringComparison.OrdinalIgnoreCase)) {
{ _logger.LogInformation("Starting new instance");
return arg;
} string module = startupOptions.GetOption("-restartpath");
return "\"" + arg + "\""; if (string.IsNullOrWhiteSpace(module))
} {
} module = Environment.GetCommandLineArgs().First();
} }
string commandLineArgsString;
if (startupOptions.ContainsOption("-restartargs"))
{
commandLineArgsString = startupOptions.GetOption("-restartargs") ?? string.Empty;
}
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.OrdinalIgnoreCase))
{
return arg;
}
return "\"" + arg + "\"";
}
}
}