aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Helpers/StartupHelpers.cs
blob: fda6e54656e0ec5ff155e235685c59d6ba165c08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using Emby.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Serilog;
using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Jellyfin.Server.Helpers;

/// <summary>
/// A class containing helper methods for server startup.
/// </summary>
public static class StartupHelpers
{
    private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };

    /// <summary>
    /// Logs relevant environment variables and information about the host.
    /// </summary>
    /// <param name="logger">The logger to use.</param>
    /// <param name="appPaths">The application paths to use.</param>
    public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
    {
        // Distinct these to prevent users from reporting problems that aren't actually problems
        var commandLineArgs = Environment
            .GetCommandLineArgs()
            .Distinct();

        // Get all relevant environment variables
        var allEnvVars = Environment.GetEnvironmentVariables();
        var relevantEnvVars = new Dictionary<object, object>();
        foreach (var key in allEnvVars.Keys)
        {
            if (_relevantEnvVarPrefixes.Any(prefix => key.ToString()!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
            {
                relevantEnvVars.Add(key, allEnvVars[key]!);
            }
        }

        logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
        logger.LogInformation("Arguments: {Args}", commandLineArgs);
        logger.LogInformation("Operating system: {OS}", RuntimeInformation.OSDescription);
        logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
        logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
        logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
        logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
        logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
        logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
        logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
    }

    /// <summary>
    /// 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
    /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
    /// is followed.
    /// </summary>
    /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
    /// <returns><see cref="ServerApplicationPaths" />.</returns>
    public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
    {
        // LocalApplicationData
        // Windows: %LocalAppData%
        // macOS: NSApplicationSupportDirectory
        // UNIX: $XDG_DATA_HOME
        var dataDir = options.DataDir
            ?? Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR")
            ?? Path.Join(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "jellyfin");

        var configDir = options.ConfigDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
        if (configDir is null)
        {
            configDir = Path.Join(dataDir, "config");
            if (options.DataDir is null
                && !Directory.Exists(configDir)
                && !OperatingSystem.IsWindows()
                && !OperatingSystem.IsMacOS())
            {
                // UNIX: $XDG_CONFIG_HOME
                configDir = Path.Join(
                    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                    "jellyfin");
            }
        }

        var cacheDir = options.CacheDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
        if (cacheDir is null)
        {
            if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
            {
                cacheDir = Path.Join(dataDir, "cache");
            }
            else
            {
                cacheDir = Path.Join(GetXdgCacheHome(), "jellyfin");
            }
        }

        var webDir = options.WebDir ?? Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
        if (webDir is null)
        {
            webDir = Path.Join(AppContext.BaseDirectory, "jellyfin-web");
        }

        var logDir = options.LogDir ?? Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
        if (logDir is null)
        {
            logDir = Path.Join(dataDir, "log");
        }

        // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
        dataDir = Path.GetFullPath(dataDir);
        logDir = Path.GetFullPath(logDir);
        configDir = Path.GetFullPath(configDir);
        cacheDir = Path.GetFullPath(cacheDir);
        webDir = Path.GetFullPath(webDir);

        // 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, webDir);
    }

    private static string GetXdgCacheHome()
    {
        // $XDG_CACHE_HOME defines the base directory relative to which
        // user specific non-essential data files should be stored.
        var cacheHome = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");

        // If $XDG_CACHE_HOME is either not set or a relative path,
        // a default equal to $HOME/.cache should be used.
        if (cacheHome is null || !cacheHome.StartsWith('/'))
        {
            cacheHome = Path.Join(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                ".cache");
        }

        return cacheHome;
    }

    /// <summary>
    /// Gets the path for the unix socket Kestrel should bind to.
    /// </summary>
    /// <param name="startupConfig">The startup config.</param>
    /// <param name="appPaths">The application paths.</param>
    /// <returns>The path for Kestrel to bind to.</returns>
    public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
    {
        var socketPath = startupConfig.GetUnixSocketPath();

        if (string.IsNullOrEmpty(socketPath))
        {
            const string SocketFile = "jellyfin.sock";

            var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
            if (xdgRuntimeDir is null)
            {
                // Fall back to config dir
                socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, SocketFile);
            }
            else
            {
                socketPath = Path.Join(xdgRuntimeDir, SocketFile);
            }
        }

        return socketPath;
    }

    /// <summary>
    /// Sets the unix file permissions for Kestrel's socket file.
    /// </summary>
    /// <param name="startupConfig">The startup config.</param>
    /// <param name="socketPath">The socket path.</param>
    /// <param name="logger">The logger.</param>
    [UnsupportedOSPlatform("windows")]
    public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger)
    {
        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);
        }
    }

    /// <summary>
    /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
    /// already.
    /// </summary>
    /// <param name="appPaths">The application paths.</param>
    /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
    public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
    {
        // Do nothing if the config file already exists
        string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault);
        if (File.Exists(configPath))
        {
            return;
        }

        // Get a stream of the resource contents
        // NOTE: The .csproj name is used instead of the assembly name in the resource path
        const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
        Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
                          ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
        await using (resource.ConfigureAwait(false))
        {
            Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
            await using (dst.ConfigureAwait(false))
            {
                // Copy the resource contents to the expected file path for the config file
                await resource.CopyToAsync(dst).ConfigureAwait(false);
            }
        }
    }

    /// <summary>
    /// Initialize Serilog using configuration and fall back to defaults on failure.
    /// </summary>
    /// <param name="configuration">The configuration object.</param>
    /// <param name="appPaths">The application paths.</param>
    public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
    {
        try
        {
            // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .Enrich.FromLogContext()
                .Enrich.WithThreadId()
                .CreateLogger();
        }
        catch (Exception ex)
        {
            Log.Logger = new LoggerConfiguration()
                .WriteTo.Console(
                    outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
                    formatProvider: CultureInfo.InvariantCulture)
                .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}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
                    formatProvider: CultureInfo.InvariantCulture,
                    encoding: Encoding.UTF8))
                .Enrich.FromLogContext()
                .Enrich.WithThreadId()
                .CreateLogger();

            Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
        }
    }

    /// <summary>
    /// Call static initialization methods for the application.
    /// </summary>
    public static void PerformStaticInitialization()
    {
        // Make sure we have all the code pages we can get
        // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

        // Increase the max http request limit
        // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
        ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);

        // Disable the "Expect: 100-Continue" header by default
        // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
        ServicePointManager.Expect100Continue = false;

        Batteries_V2.Init();
    }
}