using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Progress; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Controller.Updates { /// /// Manages all install, uninstall and update operations (both plugins and system) /// public class InstallationManager : BaseManager { /// /// The current installations /// public readonly List> CurrentInstallations = new List>(); /// /// The completed installations /// public readonly ConcurrentBag CompletedInstallations = new ConcurrentBag(); #region PluginUninstalled Event /// /// Occurs when [plugin uninstalled]. /// public event EventHandler> PluginUninstalled; /// /// Called when [plugin uninstalled]. /// /// The plugin. private void OnPluginUninstalled(IPlugin plugin) { EventHelper.QueueEventIfNotNull(PluginUninstalled, this, new GenericEventArgs { Argument = plugin }, _logger); // Notify connected ui's Kernel.TcpManager.SendWebSocketMessage("PluginUninstalled", plugin.GetPluginInfo()); } #endregion #region PluginUpdated Event /// /// Occurs when [plugin updated]. /// public event EventHandler>> PluginUpdated; /// /// Called when [plugin updated]. /// /// The plugin. /// The new version. public void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion) { _logger.Info("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.version, newVersion.classification); EventHelper.QueueEventIfNotNull(PluginUpdated, this, new GenericEventArgs> { Argument = new Tuple(plugin, newVersion) }, _logger); Kernel.NotifyPendingRestart(); } #endregion #region PluginInstalled Event /// /// Occurs when [plugin updated]. /// public event EventHandler> PluginInstalled; /// /// Called when [plugin installed]. /// /// The package. public void OnPluginInstalled(PackageVersionInfo package) { _logger.Info("New plugin installed: {0} {1} {2}", package.name, package.version, package.classification); EventHelper.QueueEventIfNotNull(PluginInstalled, this, new GenericEventArgs { Argument = package }, _logger); Kernel.NotifyPendingRestart(); } #endregion /// /// Gets or sets the zip client. /// /// The zip client. private IZipClient ZipClient { get; set; } /// /// The _logger /// private readonly ILogger _logger; /// /// The _network manager /// private readonly INetworkManager _networkManager; /// /// Gets the json serializer. /// /// The json serializer. protected IJsonSerializer JsonSerializer { get; private set; } /// /// Initializes a new instance of the class. /// /// The kernel. /// The zip client. /// The network manager. /// /// The logger. /// zipClient public InstallationManager(Kernel kernel, IZipClient zipClient, INetworkManager networkManager, IJsonSerializer jsonSerializer, ILogger logger) : base(kernel) { if (zipClient == null) { throw new ArgumentNullException("zipClient"); } if (networkManager == null) { throw new ArgumentNullException("networkManager"); } if (logger == null) { throw new ArgumentNullException("logger"); } if (jsonSerializer == null) { throw new ArgumentNullException("jsonSerializer"); } JsonSerializer = jsonSerializer; _networkManager = networkManager; _logger = logger; ZipClient = zipClient; } /// /// Gets all available packages. /// /// The cancellation token. /// Type of the package. /// The application version. /// Task{List{PackageInfo}}. public async Task> GetAvailablePackages(CancellationToken cancellationToken, PackageType? packageType = null, Version applicationVersion = null) { var data = new Dictionary { { "key", Kernel.PluginSecurityManager.SupporterKey }, { "mac", _networkManager.GetMacAddress() } }; using (var json = await Kernel.HttpManager.Post(Controller.Kernel.MBAdminUrl + "service/package/retrieveall", data, Kernel.ResourcePools.Mb, cancellationToken).ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); var packages = JsonSerializer.DeserializeFromStream>(json).ToList(); foreach (var package in packages) { package.versions = package.versions.Where(v => !string.IsNullOrWhiteSpace(v.sourceUrl)) .OrderByDescending(v => v.version).ToList(); } if (packageType.HasValue) { packages = packages.Where(p => p.type == packageType.Value).ToList(); } // If an app version was supplied, filter the versions for each package to only include supported versions if (applicationVersion != null) { foreach (var package in packages) { package.versions = package.versions.Where(v => IsPackageVersionUpToDate(v, applicationVersion)).ToList(); } } // Remove packages with no versions packages = packages.Where(p => p.versions.Any()).ToList(); return packages; } } /// /// Determines whether [is package version up to date] [the specified package version info]. /// /// The package version info. /// The application version. /// true if [is package version up to date] [the specified package version info]; otherwise, false. private bool IsPackageVersionUpToDate(PackageVersionInfo packageVersionInfo, Version applicationVersion) { if (string.IsNullOrEmpty(packageVersionInfo.requiredVersionStr)) { return true; } Version requiredVersion; return Version.TryParse(packageVersionInfo.requiredVersionStr, out requiredVersion) && applicationVersion >= requiredVersion; } /// /// Gets the package. /// /// The name. /// The classification. /// The version. /// Task{PackageVersionInfo}. public async Task GetPackage(string name, PackageVersionClass classification, Version version) { var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false); var package = packages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (package == null) { return null; } return package.versions.FirstOrDefault(v => v.version.Equals(version) && v.classification == classification); } /// /// Gets the latest compatible version. /// /// The name. /// The classification. /// Task{PackageVersionInfo}. public async Task GetLatestCompatibleVersion(string name, PackageVersionClass classification = PackageVersionClass.Release) { var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false); return GetLatestCompatibleVersion(packages, name, classification); } /// /// Gets the latest compatible version. /// /// The available packages. /// The name. /// The classification. /// PackageVersionInfo. public PackageVersionInfo GetLatestCompatibleVersion(IEnumerable availablePackages, string name, PackageVersionClass classification = PackageVersionClass.Release) { var package = availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (package == null) { return null; } return package.versions .OrderByDescending(v => v.version) .FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, Kernel.ApplicationVersion)); } /// /// Gets the available plugin updates. /// /// if set to true [with auto update enabled]. /// The cancellation token. /// Task{IEnumerable{PackageVersionInfo}}. public async Task> GetAvailablePluginUpdates(bool withAutoUpdateEnabled, CancellationToken cancellationToken) { var catalog = await Kernel.InstallationManager.GetAvailablePackages(cancellationToken).ConfigureAwait(false); var plugins = Kernel.Plugins; if (withAutoUpdateEnabled) { plugins = plugins.Where(p => p.Configuration.EnableAutoUpdate); } // Figure out what needs to be installed return plugins.Select(p => { var latestPluginInfo = Kernel.InstallationManager.GetLatestCompatibleVersion(catalog, p.Name, p.Configuration.UpdateClass); return latestPluginInfo != null && latestPluginInfo.version > p.Version ? latestPluginInfo : null; }).Where(p => !CompletedInstallations.Any(i => i.Name.Equals(p.name, StringComparison.OrdinalIgnoreCase))) .Where(p => p != null && !string.IsNullOrWhiteSpace(p.sourceUrl)); } /// /// Installs the package. /// /// The package. /// The progress. /// The cancellation token. /// Task. /// package public async Task InstallPackage(PackageVersionInfo package, IProgress progress, CancellationToken cancellationToken) { if (package == null) { throw new ArgumentNullException("package"); } if (progress == null) { throw new ArgumentNullException("progress"); } if (cancellationToken == null) { throw new ArgumentNullException("cancellationToken"); } var installationInfo = new InstallationInfo { Id = Guid.NewGuid(), Name = package.name, UpdateClass = package.classification, Version = package.versionStr }; var innerCancellationTokenSource = new CancellationTokenSource(); var tuple = new Tuple(installationInfo, innerCancellationTokenSource); // Add it to the in-progress list lock (CurrentInstallations) { CurrentInstallations.Add(tuple); } var innerProgress = new ActionableProgress { }; // Whenever the progress updates, update the outer progress object and InstallationInfo innerProgress.RegisterAction(percent => { progress.Report(percent); installationInfo.PercentComplete = percent; }); var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token; Kernel.TcpManager.SendWebSocketMessage("PackageInstalling", installationInfo); try { await InstallPackageInternal(package, innerProgress, linkedToken).ConfigureAwait(false); lock (CurrentInstallations) { CurrentInstallations.Remove(tuple); } CompletedInstallations.Add(installationInfo); Kernel.TcpManager.SendWebSocketMessage("PackageInstallationCompleted", installationInfo); } catch (OperationCanceledException) { lock (CurrentInstallations) { CurrentInstallations.Remove(tuple); } _logger.Info("Package installation cancelled: {0} {1}", package.name, package.versionStr); Kernel.TcpManager.SendWebSocketMessage("PackageInstallationCancelled", installationInfo); throw; } catch { lock (CurrentInstallations) { CurrentInstallations.Remove(tuple); } Kernel.TcpManager.SendWebSocketMessage("PackageInstallationFailed", installationInfo); throw; } finally { // Dispose the progress object and remove the installation from the in-progress list innerProgress.Dispose(); tuple.Item2.Dispose(); } } /// /// Installs the package internal. /// /// The package. /// The progress. /// The cancellation token. /// Task. private async Task InstallPackageInternal(PackageVersionInfo package, IProgress progress, CancellationToken cancellationToken) { // Target based on if it is an archive or single assembly // zip archives are assumed to contain directory structures relative to our ProgramDataPath var isArchive = string.Equals(Path.GetExtension(package.sourceUrl), ".zip", StringComparison.OrdinalIgnoreCase); var target = isArchive ? Kernel.ApplicationPaths.ProgramDataPath : Path.Combine(Kernel.ApplicationPaths.PluginsPath, package.targetFilename); // Download to temporary file so that, if interrupted, it won't destroy the existing installation var tempFile = await Kernel.HttpManager.FetchToTempFile(package.sourceUrl, Kernel.ResourcePools.Mb, cancellationToken, progress).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); // Validate with a checksum if (package.checksum != Guid.Empty) // support for legacy uploads for now { using (var crypto = new MD5CryptoServiceProvider()) using (var stream = new BufferedStream(File.OpenRead(tempFile), 100000)) { var check = Guid.Parse(BitConverter.ToString(crypto.ComputeHash(stream)).Replace("-", String.Empty)); if (check != package.checksum) { throw new ApplicationException(string.Format("Download validation failed for {0}. Probably corrupted during transfer.", package.name)); } } } cancellationToken.ThrowIfCancellationRequested(); // Success - move it to the real target based on type if (isArchive) { try { ZipClient.ExtractAll(tempFile, target, true); } catch (IOException e) { _logger.ErrorException("Error attempting to extract archive from {0} to {1}", e, tempFile, target); throw; } } else { try { File.Copy(tempFile, target, true); File.Delete(tempFile); } catch (IOException e) { _logger.ErrorException("Error attempting to move file from {0} to {1}", e, tempFile, target); throw; } } // Set last update time if we were installed before var plugin = Kernel.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase)); if (plugin != null) { // Synchronize the UpdateClass value if (plugin.Configuration.UpdateClass != package.classification) { plugin.Configuration.UpdateClass = package.classification; plugin.SaveConfiguration(); } OnPluginUpdated(plugin, package); } else { OnPluginInstalled(package); } } /// /// Uninstalls a plugin /// /// The plugin. /// public void UninstallPlugin(IPlugin plugin) { if (plugin.IsCorePlugin) { throw new ArgumentException(string.Format("{0} cannot be uninstalled because it is a core plugin.", plugin.Name)); } plugin.OnUninstalling(); // Remove it the quick way for now Kernel.RemovePlugin(plugin); File.Delete(plugin.AssemblyFilePath); OnPluginUninstalled(plugin); Kernel.NotifyPendingRestart(); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool dispose) { if (dispose) { lock (CurrentInstallations) { foreach (var tuple in CurrentInstallations) { tuple.Item2.Dispose(); } CurrentInstallations.Clear(); } } base.Dispose(dispose); } } }