StartupHelpers.cs 13 KB

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