Files
Disco/Disco.Services/Plugins/Plugins.cs
T
2014-07-07 15:19:46 +10:00

640 lines
29 KiB
C#

using Disco.Data.Repository;
using Disco.Models.BI.Interop.Community;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
namespace Disco.Services.Plugins
{
public static class Plugins
{
private static Dictionary<Assembly, PluginManifest> _PluginAssemblyManifests;
private static Dictionary<string, PluginManifest> _PluginManifests;
internal static Dictionary<Type, string> 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<PluginManifest> 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<PluginFeatureManifest> 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<PluginFeatureManifest> 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<PluginLibraryUpdateResponse>(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<PluginLibraryCompatibilityResponse>(File.ReadAllText(localCompatFile));
Data.HostVersion = hostVersion.ToString(4);
}
if (File.Exists(serverCompatFile))
{
var serverData = JsonConvert.DeserializeObject<PluginLibraryCompatibilityResponse>(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<PluginLibraryCompatibilityItem>(),
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<PluginLibraryCompatibilityResponse>(() => LoadCompatibilityData(Database));
Dictionary<string, PluginManifest> loadedPlugins = new Dictionary<string, PluginManifest>();
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)
{
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<Type, string> InitializeFeatureCategoryDetails(IEnumerable<PluginManifest> pluginManifests)
{
Dictionary<Type, string> categoryDisplayNames = new Dictionary<Type, string>();
// 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
}
}