using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Disco.Data.Repository; using System.IO.Compression; using Disco.Models.BI.Interop.Community; using System.Web; using Newtonsoft.Json; using System.Threading; namespace Disco.Services.Plugins { public static class Plugins { private static Dictionary _PluginAssemblyManifests; private static Dictionary _PluginManifests; internal static Dictionary FeatureCategoryDisplayNames; internal static object _PluginLock = new object(); public static string PluginPath { get; private set; } public static bool PluginsLoaded { get { return (_PluginManifests != null); } } internal static void AddPlugin(PluginManifest Manifest) { lock (_PluginLock) { if (_PluginManifests.ContainsKey(Manifest.Id)) throw new InvalidOperationException(string.Format("The '{0} [{1}]' Plugin is already installed, please uninstall any existing versions before trying again", Manifest.Name, Manifest.Id)); // Add Plugin Manifest to Environment _PluginManifests[Manifest.Id] = Manifest; // Reinitialize Plugin Host Environment Plugins.ReinitializePluginHostEnvironment(); } } public static bool PluginInstalled(string PluginId) { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); PluginManifest manifest; return _PluginManifests.TryGetValue(PluginId, out manifest); } public static PluginManifest GetPlugin(string PluginId, Type ContainsCategoryType) { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); PluginManifest manifest; if (_PluginManifests.TryGetValue(PluginId, out manifest)) { if (ContainsCategoryType == null) return manifest; else { foreach (var featureManifest in manifest.Features) { if (ContainsCategoryType.IsAssignableFrom(featureManifest.CategoryType)) return manifest; } throw new InvalidFeatureCategoryTypeException(ContainsCategoryType, PluginId); } } else { throw new UnknownPluginException(PluginId); } } public static bool TryGetPlugin(string PluginId, Type ContainsCategoryType, out PluginManifest PluginManifest) { PluginManifest = null; if (_PluginManifests == null) return false; PluginManifest manifest; if (_PluginManifests.TryGetValue(PluginId, out manifest)) { if (ContainsCategoryType == null) { PluginManifest = manifest; return true; } else { foreach (var featureManifest in manifest.Features) { if (ContainsCategoryType.IsAssignableFrom(featureManifest.CategoryType)) { PluginManifest = manifest; return true; } } } } return false; } public static PluginManifest GetPlugin(string PluginId) { return GetPlugin(PluginId, null); } public static bool TryGetPlugin(string PluginId, out PluginManifest PluginManifest) { return TryGetPlugin(PluginId, null, out PluginManifest); } public static PluginManifest GetPlugin(Assembly PluginAssembly) { if (_PluginAssemblyManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); PluginManifest manifest; if (_PluginAssemblyManifests.TryGetValue(PluginAssembly, out manifest)) { return manifest; } else { throw new UnknownPluginException(PluginAssembly.FullName); } } public static bool TryGetPlugin(Assembly PluginAssembly, out PluginManifest PluginManifest) { PluginManifest = null; if (_PluginAssemblyManifests == null) return false; PluginManifest manifest; if (_PluginAssemblyManifests.TryGetValue(PluginAssembly, out manifest)) { PluginManifest = manifest; return true; } else { return false; } } public static List GetPlugins() { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); return _PluginManifests.Values.ToList(); } public static bool PluginFeatureInstalled(string PluginFeatureId) { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); return _PluginManifests.Values.SelectMany(pm => pm.Features).Where(fm => fm.Id == PluginFeatureId).Count() > 0; } public static PluginFeatureManifest GetPluginFeature(string PluginFeatureId, Type CategoryType) { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); var featureManifest = _PluginManifests.Values.SelectMany(pm => pm.Features).Where(fm => fm.Id == PluginFeatureId).FirstOrDefault(); if (featureManifest == null) throw new UnknownPluginException(PluginFeatureId, "Unknown Feature"); if (CategoryType == null) return featureManifest; else if (CategoryType.IsAssignableFrom(featureManifest.CategoryType)) return featureManifest; else throw new InvalidFeatureCategoryTypeException(CategoryType, PluginFeatureId); } public static bool TryGetPluginFeature(string PluginFeatureId, Type CategoryType, out PluginFeatureManifest PluginFeatureManifest) { PluginFeatureManifest = null; if (_PluginManifests == null) return false; var featureManifest = _PluginManifests.Values.SelectMany(pm => pm.Features).Where(fm => fm.Id == PluginFeatureId).FirstOrDefault(); if (featureManifest == null) return false; if (CategoryType == null) { PluginFeatureManifest = featureManifest; return true; } else { if (CategoryType.IsAssignableFrom(featureManifest.CategoryType)) { PluginFeatureManifest = featureManifest; return true; } else return false; } } public static PluginFeatureManifest GetPluginFeature(string PluginFeatureId) { return GetPluginFeature(PluginFeatureId, null); } public static bool TryGetPluginFeature(string PluginFeatureId, out PluginFeatureManifest PluginFeatureManifest) { return TryGetPluginFeature(PluginFeatureId, null, out PluginFeatureManifest); } public static List GetPluginFeatures(Type FeatureCategoryType) { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); return _PluginManifests.Values.SelectMany(pm => pm.Features).Where(fm => fm.CategoryType.IsAssignableFrom(FeatureCategoryType)).OrderBy(fm => fm.PluginManifest.Name).ToList(); } public static List GetPluginFeatures() { if (_PluginManifests == null) throw new InvalidOperationException("Plugins have not been initialized"); return _PluginManifests.Values.SelectMany(pm => pm.Features).ToList(); } public static string PluginFeatureCategoryDisplayName(Type FeatureCategoryType) { if (FeatureCategoryType == null) throw new ArgumentNullException("FeatureType"); string displayName; if (FeatureCategoryDisplayNames.TryGetValue(FeatureCategoryType, out displayName)) return displayName; else throw new InvalidOperationException(string.Format("Unknown Plugin Feature Category Type: [{0}]", FeatureCategoryType.Name)); } public static string CatalogueFile(DiscoDataContext Database) { return Path.Combine(Database.DiscoConfiguration.PluginPackagesLocation, "Catalogue.json"); } public static string CompatibilityFile(DiscoDataContext Database) { return Path.Combine(Database.DiscoConfiguration.PluginPackagesLocation, "Compatibility.json"); } public static PluginLibraryUpdateResponse LoadCatalogue(DiscoDataContext Database) { var catalogueFile = CatalogueFile(Database); if (!File.Exists(catalogueFile)) return null; return JsonConvert.DeserializeObject(File.ReadAllText(catalogueFile)); } public static PluginLibraryCompatibilityResponse LoadCompatibilityData(DiscoDataContext Database) { var pluginAssembly = typeof(Plugins).Assembly; Version hostVersion = pluginAssembly.GetName().Version; PluginLibraryCompatibilityResponse Data = null; var localCompatFile = Path.Combine(Path.GetDirectoryName(pluginAssembly.Location), "ReleasePluginCompatibility.json"); var serverCompatFile = CompatibilityFile(Database); if (File.Exists(localCompatFile)) { Data = JsonConvert.DeserializeObject(File.ReadAllText(localCompatFile)); Data.HostVersion = hostVersion.ToString(4); } if (File.Exists(serverCompatFile)) { var serverData = JsonConvert.DeserializeObject(File.ReadAllText(serverCompatFile)); if (Version.Parse(serverData.HostVersion) == hostVersion) { if (Data == null) { // No Local Compatibility File Data = serverData; } else { // Join Compatibility Files var localItems = Data.Plugins; var localItemVersions = localItems.ToDictionary(i => i, i => Version.Parse(i.Version)); var joinedItems = localItems.ToList(); Data.ResponseTimestamp = serverData.ResponseTimestamp; foreach (var serverItem in serverData.Plugins) { var serverItemVersion = Version.Parse(serverItem.Version); var localItem = localItems.FirstOrDefault(i => i.Id.Equals(serverItem.Id, StringComparison.OrdinalIgnoreCase) && serverItemVersion == localItemVersions[i]); if (localItem != null) joinedItems.Remove(localItem); joinedItems.Add(serverItem); } Data.Plugins = joinedItems; } } } if (Data == null) { Data = new PluginLibraryCompatibilityResponse() { HostVersion = hostVersion.ToString(4), Plugins = new List(), ResponseTimestamp = new DateTime(2011, 7, 1) }; } return Data; } public static void InitalizePlugins(DiscoDataContext Database) { if (_PluginManifests == null) { lock (_PluginLock) { if (_PluginManifests == null) { Version hostVersion = typeof(Plugins).Assembly.GetName().Version; var compatibilityData = new Lazy(() => LoadCompatibilityData(Database)); Dictionary loadedPlugins = new Dictionary(); PluginPath = Database.DiscoConfiguration.PluginsLocation; AppDomain appDomain = AppDomain.CurrentDomain; // Subscribe to Assembly Resolving appDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; DirectoryInfo pluginDirectoryRoot = new DirectoryInfo(PluginPath); if (pluginDirectoryRoot.Exists) { foreach (DirectoryInfo pluginDirectory in pluginDirectoryRoot.EnumerateDirectories()) { string pluginManifestFilename = Path.Combine(pluginDirectory.FullName, "manifest.json"); if (File.Exists(pluginManifestFilename)) { PluginManifest pluginManifest = null; try { pluginManifest = PluginManifest.FromPluginManifestFile(pluginManifestFilename); if (pluginManifest != null) { if (loadedPlugins.ContainsKey(pluginManifest.Id)) throw new InvalidOperationException(string.Format("The plugin [{0}] is already initialized", pluginManifest.Id)); // Check for Update string updatePackagePath = Path.Combine(pluginDirectoryRoot.FullName, string.Format("{0}.discoPlugin", pluginManifest.Id)); if (File.Exists(updatePackagePath)) { // Update Plugin pluginManifest = UpdatePlugin(Database, pluginManifest, updatePackagePath, compatibilityData.Value); } if (pluginManifest != null) { // Check Version Compatibility var pluginCompatibility = compatibilityData.Value.Plugins.FirstOrDefault(i => i.Id.Equals(pluginManifest.Id, StringComparison.OrdinalIgnoreCase) && pluginManifest.Version == Version.Parse(i.Version)); if (pluginCompatibility != null && !pluginCompatibility.Compatible) throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", pluginManifest.Id, pluginManifest.VersionFormatted, pluginCompatibility.Reason)); if (pluginManifest.HostVersionMin != null && pluginManifest.HostVersionMin > hostVersion) throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] does not support this version of Disco (Requires v{2} or greater)", pluginManifest.Id, pluginManifest.VersionFormatted, pluginManifest.HostVersionMin.ToString())); if (pluginManifest.HostVersionMax != null && pluginManifest.HostVersionMax < hostVersion) throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] does not support this version of Disco (Support expired as of v{2})", pluginManifest.Id, pluginManifest.VersionFormatted, pluginManifest.HostVersionMax.ToString())); pluginManifest.InitializePlugin(Database); loadedPlugins[pluginManifest.Id] = pluginManifest; } } } catch (Exception ex) { PluginsLog.LogInitializeException(pluginManifestFilename, ex); } } else { string pluginManifestUninstallFilename = Path.Combine(pluginDirectory.FullName, "manifest.uninstall.json"); if (File.Exists(pluginManifestUninstallFilename)) { PluginManifest uninstallManifest = PluginManifest.FromPluginManifestFile(pluginManifestUninstallFilename); // Remove All Files DateTime removeRetryTime = DateTime.Now.AddSeconds(60); while (true) { UnauthorizedAccessException lastAccessException; try { pluginDirectory.Delete(true); break; } catch (UnauthorizedAccessException ex) { lastAccessException = ex; } if (removeRetryTime < DateTime.Now) throw lastAccessException; System.Threading.Thread.Sleep(2000); } // Check for Data Removal bool DataUninstalled = false; string pluginStorageLocation = Path.Combine(Database.DiscoConfiguration.PluginStorageLocation, uninstallManifest.Id); string pluginManifestUninstallDataFilename = Path.Combine(pluginStorageLocation, "manifest.uninstall.json"); if (File.Exists(pluginManifestUninstallDataFilename)) { DataUninstalled = true; Directory.Delete(pluginStorageLocation, true); } PluginsLog.LogUninstalled(uninstallManifest, DataUninstalled); } } } } _PluginManifests = loadedPlugins; ReinitializePluginHostEnvironment(); } } } } private static void ReinitializePluginHostEnvironment() { FeatureCategoryDisplayNames = InitializeFeatureCategoryDetails(_PluginManifests.Values); _PluginAssemblyManifests = _PluginManifests.Values.ToDictionary(p => p.PluginAssembly, p => p); } public static PluginManifest UpdatePlugin(DiscoDataContext Database, PluginManifest ExistingManifest, String UpdatePluginPackageFilePath, PluginLibraryCompatibilityResponse CompatibilityData = null) { PluginManifest updatedManifest; using (var packageStream = File.OpenRead(UpdatePluginPackageFilePath)) { updatedManifest = UpdatePlugin(Database, ExistingManifest, packageStream, CompatibilityData); } // Remove Update after processing File.Delete(UpdatePluginPackageFilePath); return updatedManifest; } public static PluginManifest UpdatePlugin(DiscoDataContext Database, PluginManifest ExistingManifest, Stream UpdatePluginPackage, PluginLibraryCompatibilityResponse CompatibilityData = null) { using (MemoryStream packageStream = new MemoryStream()) { if (UpdatePluginPackage.Position != 0) UpdatePluginPackage.Position = 0; UpdatePluginPackage.CopyTo(packageStream); packageStream.Position = 0; using (ZipArchive packageArchive = new ZipArchive(packageStream, ZipArchiveMode.Read, false)) { ZipArchiveEntry packageManifestEntry = packageArchive.GetEntry("manifest.json"); if (packageManifestEntry == null) throw new InvalidDataException("The plugin package does not contain the 'manifest.json' entry"); PluginManifest packageManifest; using (Stream packageManifestStream = packageManifestEntry.Open()) { packageManifest = PluginManifest.FromPluginManifestFile(packageManifestStream); } if (ExistingManifest.Version == packageManifest.Version) { // Skip Update if already installed PluginsLog.LogInitializeWarning(string.Format("This plugin [{0}] version [{1}] is already installed, skipping Update", ExistingManifest.Id, ExistingManifest.Version)); return ExistingManifest; } if (ExistingManifest.Version > packageManifest.Version) { throw new InvalidDataException("A newer version of this plugin is already installed"); } // Check Compatibility if (CompatibilityData == null) CompatibilityData = LoadCompatibilityData(Database); var pluginCompatibility = CompatibilityData.Plugins.FirstOrDefault(i => i.Id.Equals(packageManifest.Id, StringComparison.OrdinalIgnoreCase) && packageManifest.Version == Version.Parse(i.Version)); if (pluginCompatibility != null && !pluginCompatibility.Compatible) throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", packageManifest.Id, packageManifest.VersionFormatted, pluginCompatibility.Reason)); string packagePath = Path.Combine(Database.DiscoConfiguration.PluginsLocation, packageManifest.Id); // Force Delete of Existing Folder if (Directory.Exists(packagePath)) Directory.Delete(packagePath, true); Directory.CreateDirectory(packagePath); // Extract Package Contents foreach (var packageEntry in packageArchive.Entries) { // Determine Extraction Path var packageEntryTarget = Path.Combine(packagePath, packageEntry.FullName); // Create Sub Directories Directory.CreateDirectory(Path.GetDirectoryName(packageEntryTarget)); using (var packageEntryStream = packageEntry.Open()) { using (var packageTargetStream = File.Open(packageEntryTarget, FileMode.Create, FileAccess.Write, FileShare.None)) { packageEntryStream.CopyTo(packageTargetStream); } } } // Reload Manifest packageManifest = PluginManifest.FromPluginManifestFile(Path.Combine(packagePath, "manifest.json")); // Trigger AfterPluginUpdate packageManifest.AfterPluginUpdate(Database, ExistingManifest); PluginsLog.LogAfterUpdate(ExistingManifest, packageManifest); // Return Updated Manifest return packageManifest; } } } private static Dictionary InitializeFeatureCategoryDetails(IEnumerable pluginManifests) { Dictionary categoryDisplayNames = new Dictionary(); // Always add 'Other' var otherFeatureType = typeof(Features.Other.OtherFeature); categoryDisplayNames.Add(otherFeatureType, ((PluginFeatureCategoryAttribute)otherFeatureType.GetCustomAttributes(typeof(PluginFeatureCategoryAttribute), false).FirstOrDefault()).DisplayName); foreach (var pluginManifest in pluginManifests) { foreach (var featureManifest in pluginManifest.Features) { if (!categoryDisplayNames.ContainsKey(featureManifest.CategoryType)) { string displayName = null; var displayAttributes = featureManifest.CategoryType.GetCustomAttributes(typeof(PluginFeatureCategoryAttribute), true); if (displayAttributes != null && displayAttributes.Length > 0) displayName = ((PluginFeatureCategoryAttribute)(displayAttributes[0])).DisplayName; if (string.IsNullOrWhiteSpace(displayName)) displayName = featureManifest.CategoryType.Name; categoryDisplayNames[featureManifest.CategoryType] = displayName; } } } return categoryDisplayNames; } #region Restart App private static object _restartTimerLock = new object(); private static Timer _restartTimer; internal static void RestartApp(int DelayMilliseconds) { lock (_restartTimerLock) { if (_restartTimer != null) { _restartTimer.Dispose(); } _restartTimer = new Timer((state) => { HttpRuntime.UnloadAppDomain(); //AppDomain.Unload(AppDomain.CurrentDomain); }, null, DelayMilliseconds, Timeout.Infinite); } } #endregion #region Plugin Referenced Assemblies Resolving public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { if (args.RequestingAssembly != null && args.RequestingAssembly.Location.StartsWith(PluginPath, StringComparison.OrdinalIgnoreCase) && _PluginManifests != null) { // Try best guess first PluginManifest requestingPlugin = _PluginManifests.Values.Where(p => p.Type.Assembly == args.RequestingAssembly).FirstOrDefault(); if (requestingPlugin != null) { Assembly loadedAssembly = CurrentDomain_AssemblyResolve_ByPlugin(requestingPlugin, args); if (loadedAssembly != null) return loadedAssembly; } // Try all Plugin References foreach (var pluginDef in _PluginManifests.Values) { Assembly loadedAssembly = CurrentDomain_AssemblyResolve_ByPlugin(pluginDef, args); if (loadedAssembly != null) return loadedAssembly; } } return null; } private static Assembly CurrentDomain_AssemblyResolve_ByPlugin(PluginManifest pluginManifest, ResolveEventArgs args) { if (pluginManifest.AssemblyReferences != null) { string assemblyPath; if (pluginManifest.AssemblyReferences.TryGetValue(args.Name, out assemblyPath)) { var resolvedAssemblyPath = Path.Combine(pluginManifest.PluginLocation, assemblyPath); try { Assembly loadedAssembly = Assembly.LoadFile(resolvedAssemblyPath); PluginsLog.LogPluginReferenceAssemblyLoaded(args.Name, resolvedAssemblyPath, args.RequestingAssembly.FullName); return loadedAssembly; } catch (Exception ex) { PluginsLog.LogPluginException(string.Format("Resolving Plugin Reference Assembly: '{0}' [{1}]; Requested by: '{2}' [{3}]; Disco.Plugins.DiscoPlugins.CurrentDomain_AssemblyResolve()", args.Name, resolvedAssemblyPath, args.RequestingAssembly.FullName, args.RequestingAssembly.Location), ex); } } } return null; } #endregion } }