< Summary - Jellyfin

Information
Class: Jellyfin.Server.Helpers.StartupHelpers
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Helpers/StartupHelpers.cs
Line coverage
1%
Covered lines: 2
Uncovered lines: 110
Coverable lines: 112
Total lines: 300
Line coverage: 1.7%
Branch coverage
0%
Covered branches: 0
Total branches: 46
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
LogEnvironmentInfo(...)0%2040%
CreateApplicationPaths(...)0%1056320%
GetXdgCacheHome()0%2040%
GetUnixSocketPath(...)0%2040%
SetUnixSocketPermissions(...)0%620%
InitializeLoggingFramework(...)100%210%
PerformStaticInitialization()100%11100%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Helpers/StartupHelpers.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Runtime.InteropServices;
 7using System.Runtime.Versioning;
 8using System.Text;
 9using System.Threading.Tasks;
 10using Emby.Server.Implementations;
 11using Jellyfin.Server.ServerSetupApp;
 12using MediaBrowser.Common.Configuration;
 13using MediaBrowser.Controller.Extensions;
 14using MediaBrowser.Model.IO;
 15using Microsoft.Extensions.Configuration;
 16using Microsoft.Extensions.Logging;
 17using Serilog;
 18using Serilog.Extensions.Logging;
 19using ILogger = Microsoft.Extensions.Logging.ILogger;
 20
 21namespace Jellyfin.Server.Helpers;
 22
 23/// <summary>
 24/// A class containing helper methods for server startup.
 25/// </summary>
 26public static class StartupHelpers
 27{
 028    private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
 29
 30    /// <summary>
 31    /// Logs relevant environment variables and information about the host.
 32    /// </summary>
 33    /// <param name="logger">The logger to use.</param>
 34    /// <param name="appPaths">The application paths to use.</param>
 35    public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
 36    {
 37        // Distinct these to prevent users from reporting problems that aren't actually problems
 038        var commandLineArgs = Environment
 039            .GetCommandLineArgs()
 040            .Distinct();
 41
 42        // Get all relevant environment variables
 043        var allEnvVars = Environment.GetEnvironmentVariables();
 044        var relevantEnvVars = new Dictionary<object, object>();
 045        foreach (var key in allEnvVars.Keys)
 46        {
 047            if (_relevantEnvVarPrefixes.Any(prefix => key.ToString()!.StartsWith(prefix, StringComparison.OrdinalIgnoreC
 48            {
 049                relevantEnvVars.Add(key, allEnvVars[key]!);
 50            }
 51        }
 52
 053        logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
 054        logger.LogInformation("Arguments: {Args}", commandLineArgs);
 055        logger.LogInformation("Operating system: {OS}", RuntimeInformation.OSDescription);
 056        logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
 057        logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
 058        logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
 059        logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
 060        logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
 061        logger.LogInformation("Log directory path: {LogDirectoryPath}", appPaths.LogDirectoryPath);
 062        logger.LogInformation("Config directory path: {ConfigurationDirectoryPath}", appPaths.ConfigurationDirectoryPath
 063        logger.LogInformation("Cache path: {CachePath}", appPaths.CachePath);
 064        logger.LogInformation("Temp directory path: {TempDirPath}", appPaths.TempDirectory);
 065        logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
 066        logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
 067    }
 68
 69    /// <summary>
 70    /// Create the data, config and log paths from the variety of inputs(command line args,
 71    /// environment variables) or decide on what default to use. For Windows it's %AppPath%
 72    /// for everything else the
 73    /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
 74    /// is followed.
 75    /// </summary>
 76    /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
 77    /// <returns><see cref="ServerApplicationPaths" />.</returns>
 78    public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
 79    {
 80        // LocalApplicationData
 81        // Windows: %LocalAppData%
 82        // macOS: NSApplicationSupportDirectory
 83        // UNIX: $XDG_DATA_HOME
 084        var dataDir = options.DataDir
 085            ?? Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR")
 086            ?? Path.Join(
 087                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOptio
 088                "jellyfin");
 89
 090        var configDir = options.ConfigDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
 091        if (configDir is null)
 92        {
 093            configDir = Path.Join(dataDir, "config");
 094            if (options.DataDir is null
 095                && !Directory.Exists(configDir)
 096                && !OperatingSystem.IsWindows()
 097                && !OperatingSystem.IsMacOS())
 98            {
 99                // UNIX: $XDG_CONFIG_HOME
 0100                configDir = Path.Join(
 0101                    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption
 0102                    "jellyfin");
 103            }
 104        }
 105
 0106        var cacheDir = options.CacheDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
 0107        if (cacheDir is null)
 108        {
 0109            if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
 110            {
 0111                cacheDir = Path.Join(dataDir, "cache");
 112            }
 113            else
 114            {
 0115                cacheDir = Path.Join(GetXdgCacheHome(), "jellyfin");
 116            }
 117        }
 118
 0119        var webDir = options.WebDir ?? Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
 0120        if (webDir is null)
 121        {
 0122            webDir = Path.Join(AppContext.BaseDirectory, "jellyfin-web");
 123        }
 124
 0125        var logDir = options.LogDir ?? Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
 0126        if (logDir is null)
 127        {
 0128            logDir = Path.Join(dataDir, "log");
 129        }
 130
 131        // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
 0132        dataDir = Path.GetFullPath(dataDir);
 0133        logDir = Path.GetFullPath(logDir);
 0134        configDir = Path.GetFullPath(configDir);
 0135        cacheDir = Path.GetFullPath(cacheDir);
 0136        webDir = Path.GetFullPath(webDir);
 137
 138        // Ensure the main folders exist before we continue
 139        try
 140        {
 0141            Directory.CreateDirectory(dataDir);
 0142            Directory.CreateDirectory(logDir);
 0143            Directory.CreateDirectory(configDir);
 0144            Directory.CreateDirectory(cacheDir);
 0145        }
 0146        catch (IOException ex)
 147        {
 0148            Console.Error.WriteLine("Error whilst attempting to create folder");
 0149            Console.Error.WriteLine(ex.ToString());
 0150            Environment.Exit(1);
 0151        }
 152
 0153        return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
 154    }
 155
 156    private static string GetXdgCacheHome()
 157    {
 158        // $XDG_CACHE_HOME defines the base directory relative to which
 159        // user specific non-essential data files should be stored.
 0160        var cacheHome = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
 161
 162        // If $XDG_CACHE_HOME is either not set or a relative path,
 163        // a default equal to $HOME/.cache should be used.
 0164        if (cacheHome is null || !cacheHome.StartsWith('/'))
 165        {
 0166            cacheHome = Path.Join(
 0167                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVe
 0168                ".cache");
 169        }
 170
 0171        return cacheHome;
 172    }
 173
 174    /// <summary>
 175    /// Gets the path for the unix socket Kestrel should bind to.
 176    /// </summary>
 177    /// <param name="startupConfig">The startup config.</param>
 178    /// <param name="appPaths">The application paths.</param>
 179    /// <returns>The path for Kestrel to bind to.</returns>
 180    public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
 181    {
 0182        var socketPath = startupConfig.GetUnixSocketPath();
 183
 0184        if (string.IsNullOrEmpty(socketPath))
 185        {
 186            const string SocketFile = "jellyfin.sock";
 187
 0188            var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
 0189            if (xdgRuntimeDir is null)
 190            {
 191                // Fall back to config dir
 0192                socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, SocketFile);
 193            }
 194            else
 195            {
 0196                socketPath = Path.Join(xdgRuntimeDir, SocketFile);
 197            }
 198        }
 199
 0200        return socketPath;
 201    }
 202
 203    /// <summary>
 204    /// Sets the unix file permissions for Kestrel's socket file.
 205    /// </summary>
 206    /// <param name="startupConfig">The startup config.</param>
 207    /// <param name="socketPath">The socket path.</param>
 208    /// <param name="logger">The logger.</param>
 209    [UnsupportedOSPlatform("windows")]
 210    public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger)
 211    {
 0212        var socketPerms = startupConfig.GetUnixSocketPermissions();
 213
 0214        if (!string.IsNullOrEmpty(socketPerms))
 215        {
 0216            File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8));
 0217            logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms);
 218        }
 0219    }
 220
 221    /// <summary>
 222    /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
 223    /// already.
 224    /// </summary>
 225    /// <param name="appPaths">The application paths.</param>
 226    /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exi
 227    public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
 228    {
 229        // Do nothing if the config file already exists
 230        string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault);
 231        if (File.Exists(configPath))
 232        {
 233            return;
 234        }
 235
 236        // Get a stream of the resource contents
 237        // NOTE: The .csproj name is used instead of the assembly name in the resource path
 238        const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
 239        Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
 240                          ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
 241        await using (resource.ConfigureAwait(false))
 242        {
 243            Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.Fil
 244            await using (dst.ConfigureAwait(false))
 245            {
 246                // Copy the resource contents to the expected file path for the config file
 247                await resource.CopyToAsync(dst).ConfigureAwait(false);
 248            }
 249        }
 250    }
 251
 252    /// <summary>
 253    /// Initialize Serilog using configuration and fall back to defaults on failure.
 254    /// </summary>
 255    /// <param name="configuration">The configuration object.</param>
 256    /// <param name="appPaths">The application paths.</param>
 257    public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
 258    {
 259        try
 260        {
 0261            var startupLogger = new LoggerProviderCollection();
 0262            startupLogger.AddProvider(new SetupServer.SetupLoggerFactory());
 263            // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
 0264            Log.Logger = new LoggerConfiguration()
 0265                .ReadFrom.Configuration(configuration)
 0266                .Enrich.FromLogContext()
 0267                .Enrich.WithThreadId()
 0268                .WriteTo.Async(e => e.Providers(startupLogger))
 0269                .CreateLogger();
 0270        }
 0271        catch (Exception ex)
 272        {
 0273            Log.Logger = new LoggerConfiguration()
 0274                .WriteTo.Console(
 0275                    outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewL
 0276                    formatProvider: CultureInfo.InvariantCulture)
 0277                .WriteTo.Async(x => x.File(
 0278                    Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
 0279                    rollingInterval: RollingInterval.Day,
 0280                    outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}
 0281                    formatProvider: CultureInfo.InvariantCulture,
 0282                    encoding: Encoding.UTF8))
 0283                .Enrich.FromLogContext()
 0284                .Enrich.WithThreadId()
 0285                .CreateLogger();
 286
 0287            Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
 0288        }
 0289    }
 290
 291    /// <summary>
 292    /// Call static initialization methods for the application.
 293    /// </summary>
 294    public static void PerformStaticInitialization()
 295    {
 296        // Make sure we have all the code pages we can get
 297        // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-
 1298        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
 1299    }
 300}