StartupHelpers.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Runtime.InteropServices;
  8. using System.Runtime.Versioning;
  9. using System.Text;
  10. using System.Threading.Tasks;
  11. using Emby.Server.Implementations;
  12. using MediaBrowser.Common.Configuration;
  13. using MediaBrowser.Controller.Extensions;
  14. using MediaBrowser.Model.IO;
  15. using Microsoft.Extensions.Configuration;
  16. using Microsoft.Extensions.Logging;
  17. using Serilog;
  18. using SQLitePCL;
  19. using ILogger = Microsoft.Extensions.Logging.ILogger;
  20. namespace Jellyfin.Server.Helpers;
  21. /// <summary>
  22. /// A class containing helper methods for server startup.
  23. /// </summary>
  24. public static class StartupHelpers
  25. {
  26. private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
  27. /// <summary>
  28. /// Logs relevant environment variables and information about the host.
  29. /// </summary>
  30. /// <param name="logger">The logger to use.</param>
  31. /// <param name="appPaths">The application paths to use.</param>
  32. public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
  33. {
  34. // Distinct these to prevent users from reporting problems that aren't actually problems
  35. var commandLineArgs = Environment
  36. .GetCommandLineArgs()
  37. .Distinct();
  38. // Get all relevant environment variables
  39. var allEnvVars = Environment.GetEnvironmentVariables();
  40. var relevantEnvVars = new Dictionary<object, object>();
  41. foreach (var key in allEnvVars.Keys)
  42. {
  43. if (_relevantEnvVarPrefixes.Any(prefix => key.ToString()!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
  44. {
  45. relevantEnvVars.Add(key, allEnvVars[key]!);
  46. }
  47. }
  48. logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
  49. logger.LogInformation("Arguments: {Args}", commandLineArgs);
  50. logger.LogInformation("Operating system: {OS}", RuntimeInformation.OSDescription);
  51. logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
  52. logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
  53. logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
  54. logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
  55. logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
  56. logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
  57. logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
  58. }
  59. /// <summary>
  60. /// Create the data, config and log paths from the variety of inputs(command line args,
  61. /// environment variables) or decide on what default to use. For Windows it's %AppPath%
  62. /// for everything else the
  63. /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
  64. /// is followed.
  65. /// </summary>
  66. /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
  67. /// <returns><see cref="ServerApplicationPaths" />.</returns>
  68. public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
  69. {
  70. // LocalApplicationData
  71. // Windows: %LocalAppData%
  72. // macOS: NSApplicationSupportDirectory
  73. // UNIX: $XDG_DATA_HOME
  74. var dataDir = options.DataDir
  75. ?? Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR")
  76. ?? Path.Join(
  77. Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
  78. "jellyfin");
  79. var configDir = options.ConfigDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
  80. if (configDir is null)
  81. {
  82. configDir = Path.Join(dataDir, "config");
  83. if (options.DataDir is null
  84. && !Directory.Exists(configDir)
  85. && !OperatingSystem.IsWindows()
  86. && !OperatingSystem.IsMacOS())
  87. {
  88. // UNIX: $XDG_CONFIG_HOME
  89. configDir = Path.Join(
  90. Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
  91. "jellyfin");
  92. }
  93. }
  94. var cacheDir = options.CacheDir ?? Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
  95. if (cacheDir is null)
  96. {
  97. if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
  98. {
  99. cacheDir = Path.Join(dataDir, "cache");
  100. }
  101. else
  102. {
  103. cacheDir = Path.Join(GetXdgCacheHome(), "jellyfin");
  104. }
  105. }
  106. var webDir = options.WebDir ?? Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
  107. if (webDir is null)
  108. {
  109. webDir = Path.Join(AppContext.BaseDirectory, "jellyfin-web");
  110. }
  111. var logDir = options.LogDir ?? Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
  112. if (logDir is null)
  113. {
  114. logDir = Path.Join(dataDir, "log");
  115. }
  116. // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
  117. dataDir = Path.GetFullPath(dataDir);
  118. logDir = Path.GetFullPath(logDir);
  119. configDir = Path.GetFullPath(configDir);
  120. cacheDir = Path.GetFullPath(cacheDir);
  121. webDir = Path.GetFullPath(webDir);
  122. // Ensure the main folders exist before we continue
  123. try
  124. {
  125. Directory.CreateDirectory(dataDir);
  126. Directory.CreateDirectory(logDir);
  127. Directory.CreateDirectory(configDir);
  128. Directory.CreateDirectory(cacheDir);
  129. }
  130. catch (IOException ex)
  131. {
  132. Console.Error.WriteLine("Error whilst attempting to create folder");
  133. Console.Error.WriteLine(ex.ToString());
  134. Environment.Exit(1);
  135. }
  136. return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
  137. }
  138. private static string GetXdgCacheHome()
  139. {
  140. // $XDG_CACHE_HOME defines the base directory relative to which
  141. // user specific non-essential data files should be stored.
  142. var cacheHome = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
  143. // If $XDG_CACHE_HOME is either not set or a relative path,
  144. // a default equal to $HOME/.cache should be used.
  145. if (cacheHome is null || !cacheHome.StartsWith('/'))
  146. {
  147. cacheHome = Path.Join(
  148. Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
  149. ".cache");
  150. }
  151. return cacheHome;
  152. }
  153. /// <summary>
  154. /// Gets the path for the unix socket Kestrel should bind to.
  155. /// </summary>
  156. /// <param name="startupConfig">The startup config.</param>
  157. /// <param name="appPaths">The application paths.</param>
  158. /// <returns>The path for Kestrel to bind to.</returns>
  159. public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
  160. {
  161. var socketPath = startupConfig.GetUnixSocketPath();
  162. if (string.IsNullOrEmpty(socketPath))
  163. {
  164. const string SocketFile = "jellyfin.sock";
  165. var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
  166. if (xdgRuntimeDir is null)
  167. {
  168. // Fall back to config dir
  169. socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, SocketFile);
  170. }
  171. else
  172. {
  173. socketPath = Path.Join(xdgRuntimeDir, SocketFile);
  174. }
  175. }
  176. return socketPath;
  177. }
  178. /// <summary>
  179. /// Sets the unix file permissions for Kestrel's socket file.
  180. /// </summary>
  181. /// <param name="startupConfig">The startup config.</param>
  182. /// <param name="socketPath">The socket path.</param>
  183. /// <param name="logger">The logger.</param>
  184. [UnsupportedOSPlatform("windows")]
  185. public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger)
  186. {
  187. var socketPerms = startupConfig.GetUnixSocketPermissions();
  188. if (!string.IsNullOrEmpty(socketPerms))
  189. {
  190. File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8));
  191. logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms);
  192. }
  193. }
  194. /// <summary>
  195. /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
  196. /// already.
  197. /// </summary>
  198. /// <param name="appPaths">The application paths.</param>
  199. /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
  200. public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
  201. {
  202. // Do nothing if the config file already exists
  203. string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault);
  204. if (File.Exists(configPath))
  205. {
  206. return;
  207. }
  208. // Get a stream of the resource contents
  209. // NOTE: The .csproj name is used instead of the assembly name in the resource path
  210. const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
  211. Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
  212. ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
  213. await using (resource.ConfigureAwait(false))
  214. {
  215. Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
  216. await using (dst.ConfigureAwait(false))
  217. {
  218. // Copy the resource contents to the expected file path for the config file
  219. await resource.CopyToAsync(dst).ConfigureAwait(false);
  220. }
  221. }
  222. }
  223. /// <summary>
  224. /// Initialize Serilog using configuration and fall back to defaults on failure.
  225. /// </summary>
  226. /// <param name="configuration">The configuration object.</param>
  227. /// <param name="appPaths">The application paths.</param>
  228. public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
  229. {
  230. try
  231. {
  232. // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
  233. Log.Logger = new LoggerConfiguration()
  234. .ReadFrom.Configuration(configuration)
  235. .Enrich.FromLogContext()
  236. .Enrich.WithThreadId()
  237. .CreateLogger();
  238. }
  239. catch (Exception ex)
  240. {
  241. Log.Logger = new LoggerConfiguration()
  242. .WriteTo.Console(
  243. outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
  244. formatProvider: CultureInfo.InvariantCulture)
  245. .WriteTo.Async(x => x.File(
  246. Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
  247. rollingInterval: RollingInterval.Day,
  248. outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
  249. formatProvider: CultureInfo.InvariantCulture,
  250. encoding: Encoding.UTF8))
  251. .Enrich.FromLogContext()
  252. .Enrich.WithThreadId()
  253. .CreateLogger();
  254. Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
  255. }
  256. }
  257. /// <summary>
  258. /// Call static initialization methods for the application.
  259. /// </summary>
  260. public static void PerformStaticInitialization()
  261. {
  262. // Make sure we have all the code pages we can get
  263. // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
  264. Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
  265. // Increase the max http request limit
  266. // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
  267. ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
  268. // Disable the "Expect: 100-Continue" header by default
  269. // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
  270. ServicePointManager.Expect100Continue = false;
  271. Batteries_V2.Init();
  272. }
  273. }