InstallationManager.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Common;
  9. using MediaBrowser.Common.Configuration;
  10. using MediaBrowser.Common.Net;
  11. using MediaBrowser.Common.Plugins;
  12. using MediaBrowser.Common.Progress;
  13. using MediaBrowser.Common.Updates;
  14. using MediaBrowser.Controller.Configuration;
  15. using MediaBrowser.Model.Cryptography;
  16. using MediaBrowser.Model.Events;
  17. using MediaBrowser.Model.IO;
  18. using MediaBrowser.Model.Serialization;
  19. using MediaBrowser.Model.Updates;
  20. using Microsoft.Extensions.Logging;
  21. namespace Emby.Server.Implementations.Updates
  22. {
  23. /// <summary>
  24. /// Manages all install, uninstall and update operations (both plugins and system)
  25. /// </summary>
  26. public class InstallationManager : IInstallationManager
  27. {
  28. public event EventHandler<InstallationEventArgs> PackageInstalling;
  29. public event EventHandler<InstallationEventArgs> PackageInstallationCompleted;
  30. public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
  31. public event EventHandler<InstallationEventArgs> PackageInstallationCancelled;
  32. /// <summary>
  33. /// The current installations
  34. /// </summary>
  35. public List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; }
  36. /// <summary>
  37. /// The completed installations
  38. /// </summary>
  39. private ConcurrentBag<InstallationInfo> CompletedInstallationsInternal { get; set; }
  40. public IEnumerable<InstallationInfo> CompletedInstallations => CompletedInstallationsInternal;
  41. #region PluginUninstalled Event
  42. /// <summary>
  43. /// Occurs when [plugin uninstalled].
  44. /// </summary>
  45. public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
  46. /// <summary>
  47. /// Called when [plugin uninstalled].
  48. /// </summary>
  49. /// <param name="plugin">The plugin.</param>
  50. private void OnPluginUninstalled(IPlugin plugin)
  51. {
  52. PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin });
  53. }
  54. #endregion
  55. #region PluginUpdated Event
  56. /// <summary>
  57. /// Occurs when [plugin updated].
  58. /// </summary>
  59. public event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated;
  60. /// <summary>
  61. /// Called when [plugin updated].
  62. /// </summary>
  63. /// <param name="plugin">The plugin.</param>
  64. /// <param name="newVersion">The new version.</param>
  65. private void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion)
  66. {
  67. _logger.LogInformation("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.versionStr ?? string.Empty, newVersion.classification);
  68. PluginUpdated?.Invoke(this, new GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) });
  69. _applicationHost.NotifyPendingRestart();
  70. }
  71. #endregion
  72. #region PluginInstalled Event
  73. /// <summary>
  74. /// Occurs when [plugin updated].
  75. /// </summary>
  76. public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
  77. /// <summary>
  78. /// Called when [plugin installed].
  79. /// </summary>
  80. /// <param name="package">The package.</param>
  81. private void OnPluginInstalled(PackageVersionInfo package)
  82. {
  83. _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
  84. PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo> { Argument = package });
  85. _applicationHost.NotifyPendingRestart();
  86. }
  87. #endregion
  88. /// <summary>
  89. /// The _logger
  90. /// </summary>
  91. private readonly ILogger _logger;
  92. private readonly IApplicationPaths _appPaths;
  93. private readonly IHttpClient _httpClient;
  94. private readonly IJsonSerializer _jsonSerializer;
  95. private readonly IServerConfigurationManager _config;
  96. private readonly IFileSystem _fileSystem;
  97. /// <summary>
  98. /// Gets the application host.
  99. /// </summary>
  100. /// <value>The application host.</value>
  101. private readonly IApplicationHost _applicationHost;
  102. private readonly ICryptoProvider _cryptographyProvider;
  103. private readonly IZipClient _zipClient;
  104. // netframework or netcore
  105. private readonly string _packageRuntime;
  106. public InstallationManager(
  107. ILoggerFactory loggerFactory,
  108. IApplicationHost appHost,
  109. IApplicationPaths appPaths,
  110. IHttpClient httpClient,
  111. IJsonSerializer jsonSerializer,
  112. IServerConfigurationManager config,
  113. IFileSystem fileSystem,
  114. ICryptoProvider cryptographyProvider,
  115. IZipClient zipClient,
  116. string packageRuntime)
  117. {
  118. if (loggerFactory == null)
  119. {
  120. throw new ArgumentNullException(nameof(loggerFactory));
  121. }
  122. CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>();
  123. CompletedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
  124. _applicationHost = appHost;
  125. _appPaths = appPaths;
  126. _httpClient = httpClient;
  127. _jsonSerializer = jsonSerializer;
  128. _config = config;
  129. _fileSystem = fileSystem;
  130. _cryptographyProvider = cryptographyProvider;
  131. _zipClient = zipClient;
  132. _packageRuntime = packageRuntime;
  133. _logger = loggerFactory.CreateLogger(nameof(InstallationManager));
  134. }
  135. private static Version GetPackageVersion(PackageVersionInfo version)
  136. {
  137. return new Version(ValueOrDefault(version.versionStr, "0.0.0.1"));
  138. }
  139. private static string ValueOrDefault(string str, string def)
  140. {
  141. return string.IsNullOrEmpty(str) ? def : str;
  142. }
  143. /// <summary>
  144. /// Gets all available packages.
  145. /// </summary>
  146. /// <returns>Task{List{PackageInfo}}.</returns>
  147. public async Task<List<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken,
  148. bool withRegistration = true,
  149. string packageType = null,
  150. Version applicationVersion = null)
  151. {
  152. var packages = await GetAvailablePackagesWithoutRegistrationInfo(cancellationToken).ConfigureAwait(false);
  153. return FilterPackages(packages, packageType, applicationVersion);
  154. }
  155. /// <summary>
  156. /// Gets all available packages.
  157. /// </summary>
  158. /// <param name="cancellationToken">The cancellation token.</param>
  159. /// <returns>Task{List{PackageInfo}}.</returns>
  160. public async Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken)
  161. {
  162. using (var response = await _httpClient.SendAsync(new HttpRequestOptions
  163. {
  164. Url = "https://repo.jellyfin.org/releases/plugin/manifest.json",
  165. CancellationToken = cancellationToken,
  166. Progress = new SimpleProgress<double>(),
  167. CacheLength = GetCacheLength()
  168. }, "GET").ConfigureAwait(false))
  169. {
  170. using (var stream = response.Content)
  171. {
  172. return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false));
  173. }
  174. }
  175. }
  176. private PackageVersionClass GetSystemUpdateLevel()
  177. {
  178. return _applicationHost.SystemUpdateLevel;
  179. }
  180. private static TimeSpan GetCacheLength()
  181. {
  182. return TimeSpan.FromMinutes(3);
  183. }
  184. protected List<PackageInfo> FilterPackages(IEnumerable<PackageInfo> packages)
  185. {
  186. var list = new List<PackageInfo>();
  187. foreach (var package in packages)
  188. {
  189. var versions = new List<PackageVersionInfo>();
  190. foreach (var version in package.versions)
  191. {
  192. if (string.IsNullOrEmpty(version.sourceUrl))
  193. {
  194. continue;
  195. }
  196. if (string.IsNullOrEmpty(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1)
  197. {
  198. continue;
  199. }
  200. versions.Add(version);
  201. }
  202. package.versions = versions
  203. .OrderByDescending(GetPackageVersion)
  204. .ToArray();
  205. if (package.versions.Length == 0)
  206. {
  207. continue;
  208. }
  209. list.Add(package);
  210. }
  211. // Remove packages with no versions
  212. return list;
  213. }
  214. protected List<PackageInfo> FilterPackages(IEnumerable<PackageInfo> packages, string packageType, Version applicationVersion)
  215. {
  216. var packagesList = FilterPackages(packages);
  217. var returnList = new List<PackageInfo>();
  218. var filterOnPackageType = !string.IsNullOrEmpty(packageType);
  219. foreach (var p in packagesList)
  220. {
  221. if (filterOnPackageType && !string.Equals(p.type, packageType, StringComparison.OrdinalIgnoreCase))
  222. {
  223. continue;
  224. }
  225. // If an app version was supplied, filter the versions for each package to only include supported versions
  226. if (applicationVersion != null)
  227. {
  228. p.versions = p.versions.Where(v => IsPackageVersionUpToDate(v, applicationVersion)).ToArray();
  229. }
  230. if (p.versions.Length == 0)
  231. {
  232. continue;
  233. }
  234. returnList.Add(p);
  235. }
  236. return returnList;
  237. }
  238. /// <summary>
  239. /// Determines whether [is package version up to date] [the specified package version info].
  240. /// </summary>
  241. /// <param name="packageVersionInfo">The package version info.</param>
  242. /// <param name="currentServerVersion">The current server version.</param>
  243. /// <returns><c>true</c> if [is package version up to date] [the specified package version info]; otherwise, <c>false</c>.</returns>
  244. private static bool IsPackageVersionUpToDate(PackageVersionInfo packageVersionInfo, Version currentServerVersion)
  245. {
  246. if (string.IsNullOrEmpty(packageVersionInfo.requiredVersionStr))
  247. {
  248. return true;
  249. }
  250. return Version.TryParse(packageVersionInfo.requiredVersionStr, out var requiredVersion) && currentServerVersion >= requiredVersion;
  251. }
  252. /// <summary>
  253. /// Gets the package.
  254. /// </summary>
  255. /// <param name="name">The name.</param>
  256. /// <param name="guid">The assembly guid</param>
  257. /// <param name="classification">The classification.</param>
  258. /// <param name="version">The version.</param>
  259. /// <returns>Task{PackageVersionInfo}.</returns>
  260. public async Task<PackageVersionInfo> GetPackage(string name, string guid, PackageVersionClass classification, Version version)
  261. {
  262. var packages = await GetAvailablePackages(CancellationToken.None, false).ConfigureAwait(false);
  263. var package = packages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
  264. ?? packages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
  265. if (package == null)
  266. {
  267. return null;
  268. }
  269. return package.versions.FirstOrDefault(v => GetPackageVersion(v).Equals(version) && v.classification == classification);
  270. }
  271. /// <summary>
  272. /// Gets the latest compatible version.
  273. /// </summary>
  274. /// <param name="name">The name.</param>
  275. /// <param name="guid">The assembly guid if this is a plug-in</param>
  276. /// <param name="currentServerVersion">The current server version.</param>
  277. /// <param name="classification">The classification.</param>
  278. /// <returns>Task{PackageVersionInfo}.</returns>
  279. public async Task<PackageVersionInfo> GetLatestCompatibleVersion(string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release)
  280. {
  281. var packages = await GetAvailablePackages(CancellationToken.None, false).ConfigureAwait(false);
  282. return GetLatestCompatibleVersion(packages, name, guid, currentServerVersion, classification);
  283. }
  284. /// <summary>
  285. /// Gets the latest compatible version.
  286. /// </summary>
  287. /// <param name="availablePackages">The available packages.</param>
  288. /// <param name="name">The name.</param>
  289. /// <param name="currentServerVersion">The current server version.</param>
  290. /// <param name="classification">The classification.</param>
  291. /// <returns>PackageVersionInfo.</returns>
  292. public PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release)
  293. {
  294. var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
  295. ?? availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
  296. if (package == null)
  297. {
  298. return null;
  299. }
  300. return package.versions
  301. .OrderByDescending(GetPackageVersion)
  302. .FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, currentServerVersion));
  303. }
  304. /// <summary>
  305. /// Gets the available plugin updates.
  306. /// </summary>
  307. /// <param name="applicationVersion">The current server version.</param>
  308. /// <param name="withAutoUpdateEnabled">if set to <c>true</c> [with auto update enabled].</param>
  309. /// <param name="cancellationToken">The cancellation token.</param>
  310. /// <returns>Task{IEnumerable{PackageVersionInfo}}.</returns>
  311. public async Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(Version applicationVersion, bool withAutoUpdateEnabled, CancellationToken cancellationToken)
  312. {
  313. var catalog = await GetAvailablePackagesWithoutRegistrationInfo(cancellationToken).ConfigureAwait(false);
  314. var systemUpdateLevel = GetSystemUpdateLevel();
  315. // Figure out what needs to be installed
  316. return _applicationHost.Plugins.Select(p =>
  317. {
  318. var latestPluginInfo = GetLatestCompatibleVersion(catalog, p.Name, p.Id.ToString(), applicationVersion, systemUpdateLevel);
  319. return latestPluginInfo != null && GetPackageVersion(latestPluginInfo) > p.Version ? latestPluginInfo : null;
  320. }).Where(i => i != null)
  321. .Where(p => !string.IsNullOrEmpty(p.sourceUrl) && !CompletedInstallations.Any(i => string.Equals(i.AssemblyGuid, p.guid, StringComparison.OrdinalIgnoreCase)));
  322. }
  323. /// <summary>
  324. /// Installs the package.
  325. /// </summary>
  326. /// <param name="package">The package.</param>
  327. /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param>
  328. /// <param name="progress">The progress.</param>
  329. /// <param name="cancellationToken">The cancellation token.</param>
  330. /// <returns>Task.</returns>
  331. /// <exception cref="ArgumentNullException">package</exception>
  332. public async Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken)
  333. {
  334. if (package == null)
  335. {
  336. throw new ArgumentNullException(nameof(package));
  337. }
  338. if (progress == null)
  339. {
  340. throw new ArgumentNullException(nameof(progress));
  341. }
  342. var installationInfo = new InstallationInfo
  343. {
  344. Id = Guid.NewGuid(),
  345. Name = package.name,
  346. AssemblyGuid = package.guid,
  347. UpdateClass = package.classification,
  348. Version = package.versionStr
  349. };
  350. var innerCancellationTokenSource = new CancellationTokenSource();
  351. var tuple = new Tuple<InstallationInfo, CancellationTokenSource>(installationInfo, innerCancellationTokenSource);
  352. // Add it to the in-progress list
  353. lock (CurrentInstallations)
  354. {
  355. CurrentInstallations.Add(tuple);
  356. }
  357. var innerProgress = new ActionableProgress<double>();
  358. // Whenever the progress updates, update the outer progress object and InstallationInfo
  359. innerProgress.RegisterAction(percent =>
  360. {
  361. progress.Report(percent);
  362. installationInfo.PercentComplete = percent;
  363. });
  364. var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
  365. var installationEventArgs = new InstallationEventArgs
  366. {
  367. InstallationInfo = installationInfo,
  368. PackageVersionInfo = package
  369. };
  370. PackageInstalling?.Invoke(this, installationEventArgs);
  371. try
  372. {
  373. await InstallPackageInternal(package, isPlugin, innerProgress, linkedToken).ConfigureAwait(false);
  374. lock (CurrentInstallations)
  375. {
  376. CurrentInstallations.Remove(tuple);
  377. }
  378. CompletedInstallationsInternal.Add(installationInfo);
  379. PackageInstallationCompleted?.Invoke(this, installationEventArgs);
  380. }
  381. catch (OperationCanceledException)
  382. {
  383. lock (CurrentInstallations)
  384. {
  385. CurrentInstallations.Remove(tuple);
  386. }
  387. _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr);
  388. PackageInstallationCancelled?.Invoke(this, installationEventArgs);
  389. throw;
  390. }
  391. catch (Exception ex)
  392. {
  393. _logger.LogError(ex, "Package installation failed");
  394. lock (CurrentInstallations)
  395. {
  396. CurrentInstallations.Remove(tuple);
  397. }
  398. PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs
  399. {
  400. InstallationInfo = installationInfo,
  401. Exception = ex
  402. });
  403. throw;
  404. }
  405. finally
  406. {
  407. // Dispose the progress object and remove the installation from the in-progress list
  408. tuple.Item2.Dispose();
  409. }
  410. }
  411. /// <summary>
  412. /// Installs the package internal.
  413. /// </summary>
  414. /// <param name="package">The package.</param>
  415. /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param>
  416. /// <param name="progress">The progress.</param>
  417. /// <param name="cancellationToken">The cancellation token.</param>
  418. /// <returns>Task.</returns>
  419. private async Task InstallPackageInternal(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken)
  420. {
  421. IPlugin plugin = null;
  422. if (isPlugin)
  423. {
  424. // Set last update time if we were installed before
  425. plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase))
  426. ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase));
  427. }
  428. string targetPath = plugin == null ? null : plugin.AssemblyFilePath;
  429. // Do the install
  430. await PerformPackageInstallation(progress, targetPath, package, cancellationToken).ConfigureAwait(false);
  431. // Do plugin-specific processing
  432. if (isPlugin)
  433. {
  434. if (plugin == null)
  435. {
  436. OnPluginInstalled(package);
  437. }
  438. else
  439. {
  440. OnPluginUpdated(plugin, package);
  441. }
  442. }
  443. }
  444. private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken)
  445. {
  446. var extension = Path.GetExtension(package.targetFilename);
  447. var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
  448. if (!isArchive)
  449. {
  450. _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename);
  451. return;
  452. }
  453. if (target == null)
  454. {
  455. target = Path.Combine(_appPaths.PluginsPath, Path.GetFileNameWithoutExtension(package.targetFilename));
  456. }
  457. // Download to temporary file so that, if interrupted, it won't destroy the existing installation
  458. var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
  459. {
  460. Url = package.sourceUrl,
  461. CancellationToken = cancellationToken,
  462. Progress = progress
  463. }).ConfigureAwait(false);
  464. cancellationToken.ThrowIfCancellationRequested();
  465. // TODO: Validate with a checksum, *properly*
  466. // Success - move it to the real target
  467. try
  468. {
  469. using (var stream = File.OpenRead(tempFile))
  470. {
  471. _zipClient.ExtractAllFromZip(stream, target, true);
  472. }
  473. }
  474. catch (IOException ex)
  475. {
  476. _logger.LogError(ex, "Error attempting to extract {TempFile} to {TargetFile}", tempFile, target);
  477. throw;
  478. }
  479. try
  480. {
  481. _fileSystem.DeleteFile(tempFile);
  482. }
  483. catch (IOException ex)
  484. {
  485. // Don't fail because of this
  486. _logger.LogError(ex, "Error deleting temp file {TempFile}", tempFile);
  487. }
  488. }
  489. /// <summary>
  490. /// Uninstalls a plugin
  491. /// </summary>
  492. /// <param name="plugin">The plugin.</param>
  493. /// <exception cref="ArgumentException"></exception>
  494. public void UninstallPlugin(IPlugin plugin)
  495. {
  496. plugin.OnUninstalling();
  497. // Remove it the quick way for now
  498. _applicationHost.RemovePlugin(plugin);
  499. var path = plugin.AssemblyFilePath;
  500. _logger.LogInformation("Deleting plugin file {0}", path);
  501. // Make this case-insensitive to account for possible incorrect assembly naming
  502. var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
  503. .FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
  504. if (!string.IsNullOrWhiteSpace(file))
  505. {
  506. path = file;
  507. }
  508. _fileSystem.DeleteFile(path);
  509. var list = _config.Configuration.UninstalledPlugins.ToList();
  510. var filename = Path.GetFileName(path);
  511. if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
  512. {
  513. list.Add(filename);
  514. _config.Configuration.UninstalledPlugins = list.ToArray();
  515. _config.SaveConfiguration();
  516. }
  517. OnPluginUninstalled(plugin);
  518. _applicationHost.NotifyPendingRestart();
  519. }
  520. /// <summary>
  521. /// Releases unmanaged and - optionally - managed resources.
  522. /// </summary>
  523. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  524. protected virtual void Dispose(bool dispose)
  525. {
  526. if (dispose)
  527. {
  528. lock (CurrentInstallations)
  529. {
  530. foreach (var tuple in CurrentInstallations)
  531. {
  532. tuple.Item2.Dispose();
  533. }
  534. CurrentInstallations.Clear();
  535. }
  536. }
  537. }
  538. public void Dispose()
  539. {
  540. Dispose(true);
  541. }
  542. }
  543. }