using System; 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.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 MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Serilog; using Serilog.AspNetCore; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server { public static class Program { private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static ILogger _logger; private static bool _restartOnShutdown; public static async 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++) { args[i] = regex.Replace(args[i], substitution); } // Parse the command line arguments and either start the app or exit indicating error await Parser.Default.ParseArguments(args) .MapResult( options => StartApp(options), errs => Task.FromResult(0)).ConfigureAwait(false); } private static async Task StartApp(StartupOptions options) { ServerApplicationPaths appPaths = CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); await CreateLogger(appPaths); _logger = _loggerFactory.CreateLogger("Main"); AppDomain.CurrentDomain.UnhandledException += (sender, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); // Intercept Ctrl+C and Ctrl+Break Console.CancelKeyPress += (sender, 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 += (sender, e) => { 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); EnvironmentInfo environmentInfo = new EnvironmentInfo(GetOperatingSystem()); ApplicationHost.LogEnvironmentInfo(_logger, appPaths, environmentInfo); SQLitePCL.Batteries_V2.Init(); // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, null, appPaths.TempDirectory, true); using (var appHost = new CoreAppHost( appPaths, _loggerFactory, options, fileSystem, environmentInfo, new NullImageEncoder(), new NetworkManager(_loggerFactory, environmentInfo))) { await appHost.Init(); appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); await appHost.RunStartupTasks(); // TODO: read input for a stop command try { // Block main thread until shutdown await Task.Delay(-1, _tokenSource.Token); } catch (TaskCanceledException) { // Don't throw on cancellation } } if (_restartOnShutdown) { StartNewInstance(options); } } /// /// 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 /// /// /// ServerApplicationPaths private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) { // dataDir // IF --datadir // ELSE IF $JELLYFIN_DATA_PATH // 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"); if (string.IsNullOrEmpty(dataDir)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { dataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); } else { // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. dataDir = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); // If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. if (string.IsNullOrEmpty(dataDir)) { dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share"); } } dataDir = Path.Combine(dataDir, "jellyfin"); } } // configDir // IF --configdir // ELSE IF $JELLYFIN_CONFIG_DIR // ELSE IF --datadir, use /config (assume portable run) // ELSE IF /config exists, use that // ELSE IF windows, use /config // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin // ELSE $HOME/.config/jellyfin var configDir = options.ConfigDir; if (string.IsNullOrEmpty(configDir)) { configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); if (string.IsNullOrEmpty(configDir)) { if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // 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. 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 (string.IsNullOrEmpty(configDir)) { configDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); } configDir = Path.Combine(configDir, "jellyfin"); } } } // cacheDir // IF --cachedir // ELSE IF $JELLYFIN_CACHE_DIR // ELSE IF windows, use /cache // 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)) { // 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. 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 (string.IsNullOrEmpty(cacheDir)) { cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache"); } cacheDir = Path.Combine(cacheDir, "jellyfin"); } } } // logDir // IF --logdir // ELSE IF $JELLYFIN_LOG_DIR // ELSE IF --datadir, use /log (assume portable run) // ELSE /log var logDir = options.LogDir; if (string.IsNullOrEmpty(logDir)) { logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); if (string.IsNullOrEmpty(logDir)) { // Hang log folder off already set dataDir logDir = Path.Combine(dataDir, "log"); } } // Ensure the main folders exist before we continue try { Directory.CreateDirectory(dataDir); Directory.CreateDirectory(logDir); Directory.CreateDirectory(configDir); Directory.CreateDirectory(cacheDir); } catch (IOException ex) { Console.Error.WriteLine("Error whilst attempting to create folder"); Console.Error.WriteLine(ex.ToString()); Environment.Exit(1); } return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir); } private static async Task CreateLogger(IApplicationPaths appPaths) { try { string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json"); 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); } } var configuration = new ConfigurationBuilder() .SetBasePath(appPaths.ConfigurationDirectoryPath) .AddJsonFile("logging.json") .AddEnvironmentVariables("JELLYFIN_") .Build(); // Serilog.Log is used by SerilogLoggerFactory when no logger is specified Serilog.Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .Enrich.FromLogContext() .CreateLogger(); } catch (Exception ex) { Serilog.Log.Logger = new LoggerConfiguration() .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, outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}")) .Enrich.FromLogContext() .CreateLogger(); Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); } } public 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}'"); } } } public static void Shutdown() { if (!_tokenSource.IsCancellationRequested) { _tokenSource.Cancel(); } } public static void Restart() { _restartOnShutdown = true; Shutdown(); } private static void StartNewInstance(StartupOptions options) { _logger.LogInformation("Starting new instance"); string module = options.RestartPath; if (string.IsNullOrWhiteSpace(module)) { module = Environment.GetCommandLineArgs().First(); } string commandLineArgsString; if (options.RestartArgs != null) { commandLineArgsString = options.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 + "\""; } } }