diff options
| author | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
|---|---|---|
| committer | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
| commit | 767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch) | |
| tree | 49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/Updates/InstallationManager.cs | |
| parent | 845554722efaed872948a9e0f7202e3ef52f1b6e (diff) | |
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/Updates/InstallationManager.cs')
| -rw-r--r-- | MediaBrowser.Controller/Updates/InstallationManager.cs | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/Updates/InstallationManager.cs b/MediaBrowser.Controller/Updates/InstallationManager.cs new file mode 100644 index 000000000..63afa2ce8 --- /dev/null +++ b/MediaBrowser.Controller/Updates/InstallationManager.cs @@ -0,0 +1,486 @@ +using System.Security.Cryptography; +using Ionic.Zip; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Progress; +using MediaBrowser.Common.Serialization; +using MediaBrowser.Model.Updates; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Updates +{ + /// <summary> + /// Manages all install, uninstall and update operations (both plugins and system) + /// </summary> + public class InstallationManager : BaseManager<Kernel> + { + /// <summary> + /// The current installations + /// </summary> + public readonly List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations = + new List<Tuple<InstallationInfo, CancellationTokenSource>>(); + + /// <summary> + /// The completed installations + /// </summary> + public readonly ConcurrentBag<InstallationInfo> CompletedInstallations = new ConcurrentBag<InstallationInfo>(); + + #region PluginUninstalled Event + /// <summary> + /// Occurs when [plugin uninstalled]. + /// </summary> + public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; + + /// <summary> + /// Called when [plugin uninstalled]. + /// </summary> + /// <param name="plugin">The plugin.</param> + private void OnPluginUninstalled(IPlugin plugin) + { + EventHelper.QueueEventIfNotNull(PluginUninstalled, this, new GenericEventArgs<IPlugin> { Argument = plugin }); + + // Notify connected ui's + Kernel.TcpManager.SendWebSocketMessage("PluginUninstalled", plugin.GetPluginInfo()); + } + #endregion + + #region PluginUpdated Event + /// <summary> + /// Occurs when [plugin updated]. + /// </summary> + public event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated; + /// <summary> + /// Called when [plugin updated]. + /// </summary> + /// <param name="plugin">The plugin.</param> + /// <param name="newVersion">The new version.</param> + 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<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) }); + + Kernel.NotifyPendingRestart(); + } + #endregion + + #region PluginInstalled Event + /// <summary> + /// Occurs when [plugin updated]. + /// </summary> + public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; + /// <summary> + /// Called when [plugin installed]. + /// </summary> + /// <param name="package">The package.</param> + 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<PackageVersionInfo> { Argument = package }); + + Kernel.NotifyPendingRestart(); + } + #endregion + + /// <summary> + /// Initializes a new instance of the <see cref="InstallationManager" /> class. + /// </summary> + /// <param name="kernel">The kernel.</param> + public InstallationManager(Kernel kernel) + : base(kernel) + { + + } + + /// <summary> + /// Gets all available packages. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="packageType">Type of the package.</param> + /// <param name="applicationVersion">The application version.</param> + /// <returns>Task{List{PackageInfo}}.</returns> + public async Task<IEnumerable<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken, + PackageType? packageType = null, + Version applicationVersion = null) + { + var data = new Dictionary<string, string> { { "key", Kernel.PluginSecurityManager.SupporterKey }, { "mac", NetUtils.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<List<PackageInfo>>(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; + } + } + + /// <summary> + /// Determines whether [is package version up to date] [the specified package version info]. + /// </summary> + /// <param name="packageVersionInfo">The package version info.</param> + /// <param name="applicationVersion">The application version.</param> + /// <returns><c>true</c> if [is package version up to date] [the specified package version info]; otherwise, <c>false</c>.</returns> + 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; + } + + /// <summary> + /// Gets the package. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="classification">The classification.</param> + /// <param name="version">The version.</param> + /// <returns>Task{PackageVersionInfo}.</returns> + public async Task<PackageVersionInfo> 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); + } + + /// <summary> + /// Gets the latest compatible version. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="classification">The classification.</param> + /// <returns>Task{PackageVersionInfo}.</returns> + public async Task<PackageVersionInfo> GetLatestCompatibleVersion(string name, PackageVersionClass classification = PackageVersionClass.Release) + { + var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false); + + return GetLatestCompatibleVersion(packages, name, classification); + } + + /// <summary> + /// Gets the latest compatible version. + /// </summary> + /// <param name="availablePackages">The available packages.</param> + /// <param name="name">The name.</param> + /// <param name="classification">The classification.</param> + /// <returns>PackageVersionInfo.</returns> + public PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> 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)); + } + + /// <summary> + /// Gets the available plugin updates. + /// </summary> + /// <param name="withAutoUpdateEnabled">if set to <c>true</c> [with auto update enabled].</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{PackageVersionInfo}}.</returns> + public async Task<IEnumerable<PackageVersionInfo>> 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)); + } + + /// <summary> + /// Installs the package. + /// </summary> + /// <param name="package">The package.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">package</exception> + public async Task InstallPackage(PackageVersionInfo package, IProgress<double> 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, CancellationTokenSource>(installationInfo, innerCancellationTokenSource); + + // Add it to the in-progress list + lock (CurrentInstallations) + { + CurrentInstallations.Add(tuple); + } + + var innerProgress = new ActionableProgress<double> { }; + + // 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(); + } + } + + /// <summary> + /// Installs the package internal. + /// </summary> + /// <param name="package">The package.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task InstallPackageInternal(PackageVersionInfo package, IProgress<double> 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 + { + // Extract to target in full - overwriting + using (var zipFile = ZipFile.Read(tempFile)) + { + zipFile.ExtractAll(target, ExtractExistingFileAction.OverwriteSilently); + } + } + 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); + } + } + + /// <summary> + /// Uninstalls a plugin + /// </summary> + /// <param name="plugin">The plugin.</param> + /// <exception cref="System.ArgumentException"></exception> + 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(); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected override void Dispose(bool dispose) + { + if (dispose) + { + lock (CurrentInstallations) + { + foreach (var tuple in CurrentInstallations) + { + tuple.Item2.Dispose(); + } + + CurrentInstallations.Clear(); + } + } + base.Dispose(dispose); + } + } +} |
