using MediaBrowser.Common.Logging; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Serialization; using MediaBrowser.Model.DTO; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.IO; using System.Linq; using System.Threading.Tasks; namespace MediaBrowser.UI.Controller { /// /// This keeps ui plugin assemblies in sync with plugins installed on the server /// public class PluginUpdater { /// /// Gets the list of currently installed UI plugins /// [ImportMany(typeof(BasePlugin))] private IEnumerable CurrentPlugins { get; set; } private CompositionContainer CompositionContainer { get; set; } public async Task UpdatePlugins() { // First load the plugins that are currently installed ReloadComposableParts(); Logger.LogInfo("Downloading list of installed plugins"); PluginInfo[] allInstalledPlugins = await UIKernel.Instance.ApiClient.GetInstalledPluginsAsync().ConfigureAwait(false); IEnumerable uiPlugins = allInstalledPlugins.Where(p => p.DownloadToUI); PluginUpdateResult result = new PluginUpdateResult(); result.DeletedPlugins = DeleteUninstalledPlugins(uiPlugins); await DownloadPluginAssemblies(uiPlugins, result).ConfigureAwait(false); // If any new assemblies were downloaded we'll have to reload the CurrentPlugins list if (result.NewlyInstalledPlugins.Any()) { ReloadComposableParts(); } result.UpdatedConfigurations = await DownloadPluginConfigurations(uiPlugins).ConfigureAwait(false); CompositionContainer.Dispose(); return result; } /// /// Downloads plugin assemblies from the server, if they need to be installed or updated. /// private async Task DownloadPluginAssemblies(IEnumerable uiPlugins, PluginUpdateResult result) { List newlyInstalledPlugins = new List(); List updatedPlugins = new List(); // Loop through the list of plugins that are on the server foreach (PluginInfo pluginInfo in uiPlugins) { // See if it is already installed in the UI BasePlugin installedPlugin = CurrentPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(pluginInfo.AssemblyFileName, StringComparison.OrdinalIgnoreCase)); // Download the plugin if it is not present, or if the current version is out of date bool downloadPlugin = installedPlugin == null; if (installedPlugin != null) { Version serverVersion = Version.Parse(pluginInfo.Version); downloadPlugin = serverVersion > installedPlugin.Version; } if (downloadPlugin) { await DownloadPlugin(pluginInfo).ConfigureAwait(false); if (installedPlugin == null) { newlyInstalledPlugins.Add(pluginInfo); } else { updatedPlugins.Add(pluginInfo); } } } result.NewlyInstalledPlugins = newlyInstalledPlugins; result.UpdatedPlugins = updatedPlugins; } /// /// Downloads plugin configurations from the server. /// private async Task> DownloadPluginConfigurations(IEnumerable uiPlugins) { List updatedPlugins = new List(); // Loop through the list of plugins that are on the server foreach (PluginInfo pluginInfo in uiPlugins) { // See if it is already installed in the UI BasePlugin installedPlugin = CurrentPlugins.First(p => p.AssemblyFileName.Equals(pluginInfo.AssemblyFileName, StringComparison.OrdinalIgnoreCase)); if (installedPlugin.ConfigurationDateLastModified < pluginInfo.ConfigurationDateLastModified) { await DownloadPluginConfiguration(installedPlugin, pluginInfo).ConfigureAwait(false); updatedPlugins.Add(pluginInfo); } } return updatedPlugins; } /// /// Downloads a plugin assembly from the server /// private async Task DownloadPlugin(PluginInfo plugin) { Logger.LogInfo("Downloading {0} Plugin", plugin.Name); string path = Path.Combine(UIKernel.Instance.ApplicationPaths.PluginsPath, plugin.AssemblyFileName); // First download to a MemoryStream. This way if the download is cut off, we won't be left with a partial file using (MemoryStream memoryStream = new MemoryStream()) { Stream assemblyStream = await UIKernel.Instance.ApiClient.GetPluginAssemblyAsync(plugin).ConfigureAwait(false); await assemblyStream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; using (FileStream fileStream = new FileStream(path, FileMode.Create)) { await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false); } } } /// /// Downloads the latest configuration for a plugin /// private async Task DownloadPluginConfiguration(BasePlugin plugin, PluginInfo pluginInfo) { Logger.LogInfo("Downloading {0} Configuration", plugin.Name); object config = await UIKernel.Instance.ApiClient.GetPluginConfigurationAsync(pluginInfo, plugin.ConfigurationType).ConfigureAwait(false); XmlSerializer.SerializeToFile(config, plugin.ConfigurationFilePath); File.SetLastWriteTimeUtc(plugin.ConfigurationFilePath, pluginInfo.ConfigurationDateLastModified); } /// /// Deletes any plugins that have been uninstalled from the server /// private IEnumerable DeleteUninstalledPlugins(IEnumerable uiPlugins) { var deletedPlugins = new List(); foreach (BasePlugin plugin in CurrentPlugins) { PluginInfo latest = uiPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(plugin.AssemblyFileName, StringComparison.OrdinalIgnoreCase)); if (latest == null) { DeletePlugin(plugin); deletedPlugins.Add(plugin.Name); } } return deletedPlugins; } /// /// Deletes an installed ui plugin. /// Leaves config and data behind in the event it is later re-installed /// private void DeletePlugin(BasePlugin plugin) { Logger.LogInfo("Deleting {0} Plugin", plugin.Name); string path = plugin.AssemblyFilePath; if (File.Exists(path)) { File.Delete(path); } } /// /// Re-uses MEF within the kernel to discover installed plugins /// private void ReloadComposableParts() { if (CompositionContainer != null) { CompositionContainer.Dispose(); } CompositionContainer = UIKernel.Instance.GetCompositionContainer(); CompositionContainer.ComposeParts(this); CompositionContainer.Catalog.Dispose(); foreach (BasePlugin plugin in CurrentPlugins) { plugin.Initialize(UIKernel.Instance, false); } } } public class PluginUpdateResult { public IEnumerable DeletedPlugins { get; set; } public IEnumerable NewlyInstalledPlugins { get; set; } public IEnumerable UpdatedPlugins { get; set; } public IEnumerable UpdatedConfigurations { get; set; } } }