LinuxIsoManager.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. using System;
  2. using System.IO;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using MediaBrowser.Model.Diagnostics;
  6. using MediaBrowser.Model.IO;
  7. using Microsoft.Extensions.Logging;
  8. using MediaBrowser.Model.System;
  9. using System.Runtime.InteropServices;
  10. namespace IsoMounter
  11. {
  12. public class LinuxIsoManager : IIsoMounter
  13. {
  14. [DllImport("libc", SetLastError = true)]
  15. public static extern uint getuid();
  16. #region Private Fields
  17. private readonly IEnvironmentInfo EnvironmentInfo;
  18. private readonly bool ExecutablesAvailable;
  19. private readonly IFileSystem FileSystem;
  20. private readonly ILogger _logger;
  21. private readonly string MountCommand;
  22. private readonly string MountPointRoot;
  23. private readonly IProcessFactory ProcessFactory;
  24. private readonly string SudoCommand;
  25. private readonly string UmountCommand;
  26. #endregion
  27. #region Constructor(s)
  28. public LinuxIsoManager(ILogger logger, IFileSystem fileSystem, IEnvironmentInfo environment, IProcessFactory processFactory)
  29. {
  30. EnvironmentInfo = environment;
  31. FileSystem = fileSystem;
  32. _logger = logger;
  33. ProcessFactory = processFactory;
  34. MountPointRoot = FileSystem.DirectorySeparatorChar + "tmp" + FileSystem.DirectorySeparatorChar + "Emby";
  35. _logger.LogDebug(
  36. "[{0}] System PATH is currently set to [{1}].",
  37. Name,
  38. Environment.GetEnvironmentVariable("PATH") ?? ""
  39. );
  40. _logger.LogDebug(
  41. "[{0}] System path separator is [{1}].",
  42. Name,
  43. Path.PathSeparator
  44. );
  45. _logger.LogDebug(
  46. "[{0}] Mount point root is [{1}].",
  47. Name,
  48. MountPointRoot
  49. );
  50. //
  51. // Get the location of the executables we need to support mounting/unmounting ISO images.
  52. //
  53. SudoCommand = GetFullPathForExecutable("sudo");
  54. _logger.LogInformation(
  55. "[{0}] Using version of [sudo] located at [{1}].",
  56. Name,
  57. SudoCommand
  58. );
  59. MountCommand = GetFullPathForExecutable("mount");
  60. _logger.LogInformation(
  61. "[{0}] Using version of [mount] located at [{1}].",
  62. Name,
  63. MountCommand
  64. );
  65. UmountCommand = GetFullPathForExecutable("umount");
  66. _logger.LogInformation(
  67. "[{0}] Using version of [umount] located at [{1}].",
  68. Name,
  69. UmountCommand
  70. );
  71. if (!String.IsNullOrEmpty(SudoCommand) && !String.IsNullOrEmpty(MountCommand) && !String.IsNullOrEmpty(UmountCommand)) {
  72. ExecutablesAvailable = true;
  73. } else {
  74. ExecutablesAvailable = false;
  75. }
  76. }
  77. #endregion
  78. #region Interface Implementation for IIsoMounter
  79. public bool IsInstalled {
  80. get {
  81. return true;
  82. }
  83. }
  84. public string Name {
  85. get { return "LinuxMount"; }
  86. }
  87. public bool RequiresInstallation {
  88. get {
  89. return false;
  90. }
  91. }
  92. public bool CanMount(string path)
  93. {
  94. if (EnvironmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Linux)
  95. {
  96. return false;
  97. }
  98. _logger.LogInformation(
  99. "[{0}] Checking we can attempt to mount [{1}], Extension = [{2}], Operating System = [{3}], Executables Available = [{4}].",
  100. Name,
  101. path,
  102. Path.GetExtension(path),
  103. EnvironmentInfo.OperatingSystem,
  104. ExecutablesAvailable.ToString()
  105. );
  106. if (ExecutablesAvailable)
  107. {
  108. return string.Equals(Path.GetExtension(path), ".iso", StringComparison.OrdinalIgnoreCase);
  109. }
  110. else
  111. {
  112. return false;
  113. }
  114. }
  115. public Task Install(CancellationToken cancellationToken)
  116. {
  117. return Task.FromResult(false);
  118. }
  119. public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken)
  120. {
  121. if (MountISO(isoPath, out LinuxMount mountedISO)) {
  122. return Task.FromResult<IIsoMount>(mountedISO);
  123. }
  124. else {
  125. throw new IOException(String.Format(
  126. "An error occurred trying to mount image [$0].",
  127. isoPath
  128. ));
  129. }
  130. }
  131. #endregion
  132. #region Interface Implementation for IDisposable
  133. // Flag: Has Dispose already been called?
  134. private bool disposed = false;
  135. public void Dispose()
  136. {
  137. // Dispose of unmanaged resources.
  138. Dispose(true);
  139. // Suppress finalization.
  140. GC.SuppressFinalize(this);
  141. }
  142. protected virtual void Dispose(bool disposing)
  143. {
  144. if (disposed) {
  145. return;
  146. }
  147. _logger.LogInformation(
  148. "[{0}] Disposing [{1}].",
  149. Name,
  150. disposing.ToString()
  151. );
  152. if (disposing) {
  153. //
  154. // Free managed objects here.
  155. //
  156. }
  157. //
  158. // Free any unmanaged objects here.
  159. //
  160. disposed = true;
  161. }
  162. #endregion
  163. #region Private Methods
  164. private string GetFullPathForExecutable(string name)
  165. {
  166. foreach (string test in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
  167. {
  168. string path = test.Trim();
  169. if (!String.IsNullOrEmpty(path) && FileSystem.FileExists(path = Path.Combine(path, name))) {
  170. return FileSystem.GetFullPath(path);
  171. }
  172. }
  173. return string.Empty;
  174. }
  175. private uint GetUID()
  176. {
  177. var uid = getuid();
  178. _logger.LogDebug(
  179. "[{0}] Our current UID is [{1}], GetUserId() returned [{2}].",
  180. Name,
  181. uid.ToString(),
  182. uid
  183. );
  184. return uid;
  185. }
  186. private bool ExecuteCommand(string cmdFilename, string cmdArguments)
  187. {
  188. bool processFailed = false;
  189. var process = ProcessFactory.Create(
  190. new ProcessOptions {
  191. CreateNoWindow = true,
  192. RedirectStandardOutput = true,
  193. RedirectStandardError = true,
  194. UseShellExecute = false,
  195. FileName = cmdFilename,
  196. Arguments = cmdArguments,
  197. IsHidden = true,
  198. ErrorDialog = false,
  199. EnableRaisingEvents = true
  200. }
  201. );
  202. try
  203. {
  204. process.Start();
  205. //StreamReader outputReader = process.StandardOutput.;
  206. //StreamReader errorReader = process.StandardError;
  207. _logger.LogDebug(
  208. "[{Name}] Standard output from process is [{Error}].",
  209. Name,
  210. process.StandardOutput.ReadToEnd()
  211. );
  212. _logger.LogDebug(
  213. "[{Name}] Standard error from process is [{Error}].",
  214. Name,
  215. process.StandardError.ReadToEnd()
  216. );
  217. }
  218. catch (Exception ex)
  219. {
  220. processFailed = true;
  221. _logger.LogDebug(ex, "[{Name}] Unhandled exception executing command.", Name);
  222. }
  223. if (!processFailed && process.ExitCode == 0) {
  224. return true;
  225. } else {
  226. return false;
  227. }
  228. }
  229. private bool MountISO(string isoPath, out LinuxMount mountedISO)
  230. {
  231. string cmdArguments;
  232. string cmdFilename;
  233. string mountPoint = Path.Combine(MountPointRoot, Guid.NewGuid().ToString());
  234. if (!string.IsNullOrEmpty(isoPath)) {
  235. _logger.LogInformation(
  236. "[{Name}] Attempting to mount [{Path}].",
  237. Name,
  238. isoPath
  239. );
  240. _logger.LogDebug(
  241. "[{Name}] ISO will be mounted at [{Path}].",
  242. Name,
  243. mountPoint
  244. );
  245. } else {
  246. throw new ArgumentNullException(nameof(isoPath));
  247. }
  248. try
  249. {
  250. FileSystem.CreateDirectory(mountPoint);
  251. }
  252. catch (UnauthorizedAccessException)
  253. {
  254. throw new IOException("Unable to create mount point(Permission denied) for " + isoPath);
  255. }
  256. catch (Exception)
  257. {
  258. throw new IOException("Unable to create mount point for " + isoPath);
  259. }
  260. if (GetUID() == 0) {
  261. cmdFilename = MountCommand;
  262. cmdArguments = string.Format("\"{0}\" \"{1}\"", isoPath, mountPoint);
  263. } else {
  264. cmdFilename = SudoCommand;
  265. cmdArguments = string.Format("\"{0}\" \"{1}\" \"{2}\"", MountCommand, isoPath, mountPoint);
  266. }
  267. _logger.LogDebug(
  268. "[{0}] Mount command [{1}], mount arguments [{2}].",
  269. Name,
  270. cmdFilename,
  271. cmdArguments
  272. );
  273. if (ExecuteCommand(cmdFilename, cmdArguments)) {
  274. _logger.LogInformation(
  275. "[{0}] ISO mount completed successfully.",
  276. Name
  277. );
  278. mountedISO = new LinuxMount(this, isoPath, mountPoint);
  279. } else {
  280. _logger.LogInformation(
  281. "[{0}] ISO mount completed with errors.",
  282. Name
  283. );
  284. try
  285. {
  286. FileSystem.DeleteDirectory(mountPoint, false);
  287. }
  288. catch (Exception ex)
  289. {
  290. _logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name);
  291. }
  292. mountedISO = null;
  293. }
  294. return mountedISO != null;
  295. }
  296. private void UnmountISO(LinuxMount mount)
  297. {
  298. string cmdArguments;
  299. string cmdFilename;
  300. if (mount != null) {
  301. _logger.LogInformation(
  302. "[{0}] Attempting to unmount ISO [{1}] mounted on [{2}].",
  303. Name,
  304. mount.IsoPath,
  305. mount.MountedPath
  306. );
  307. } else {
  308. throw new ArgumentNullException(nameof(mount));
  309. }
  310. if (GetUID() == 0) {
  311. cmdFilename = UmountCommand;
  312. cmdArguments = string.Format("\"{0}\"", mount.MountedPath);
  313. } else {
  314. cmdFilename = SudoCommand;
  315. cmdArguments = string.Format("\"{0}\" \"{1}\"", UmountCommand, mount.MountedPath);
  316. }
  317. _logger.LogDebug(
  318. "[{0}] Umount command [{1}], umount arguments [{2}].",
  319. Name,
  320. cmdFilename,
  321. cmdArguments
  322. );
  323. if (ExecuteCommand(cmdFilename, cmdArguments)) {
  324. _logger.LogInformation(
  325. "[{0}] ISO unmount completed successfully.",
  326. Name
  327. );
  328. } else {
  329. _logger.LogInformation(
  330. "[{0}] ISO unmount completed with errors.",
  331. Name
  332. );
  333. }
  334. try
  335. {
  336. FileSystem.DeleteDirectory(mount.MountedPath, false);
  337. }
  338. catch (Exception ex)
  339. {
  340. _logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name);
  341. }
  342. }
  343. #endregion
  344. #region Internal Methods
  345. internal void OnUnmount(LinuxMount mount)
  346. {
  347. UnmountISO(mount);
  348. }
  349. #endregion
  350. }
  351. }