Shutdown gracefully when recieving a termination signal

This commit is contained in:
Bond_009 2019-01-12 23:31:45 +01:00
parent 78a5d999f4
commit 0abdfbb526

View File

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