Files
Disco/Disco.Services/Plugins/Plugins.cs
T
Gary Sharp 27c21175d7 Certificate/wireless plugins; major refactoring
Migrate much of BI to Services.
Added Wireless Profile Provider plugin feature.
Added Certificate Authority Provider plugin feature.
Modified Certificate Provider plugin feature.
Database migration v17, for Device Profiles.
Enrolment Client Updated to support CA Certificates, Wireless Profiles
and Hardware Info.
New Client Enrolment Protocol to support new features.
Plugin Manifest Generator added to main solution.
Improved AD search performance.
2016-09-28 20:17:55 +10:00

575 lines
26 KiB
C#

using Disco.Data.Repository;
using Disco.Models.Services.Interop.DiscoServices;
using Disco.Services.Interop.DiscoServices;
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)
{
if (_PluginManifests == null)
{
PluginFeatureManifest = null;
return false;
}
var featureManifest = _PluginManifests.Values
.SelectMany(pm => pm.Features)
.Where(fm => fm.Id == PluginFeatureId)
.FirstOrDefault();
if (featureManifest == null)
{
PluginFeatureManifest = null;
return false;
}
if (CategoryType == null)
{
PluginFeatureManifest = featureManifest;
return true;
}
else
{
if (CategoryType.IsAssignableFrom(featureManifest.CategoryType))
{
PluginFeatureManifest = featureManifest;
return true;
}
else
{
PluginFeatureManifest = null;
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 void InitalizePlugins(DiscoDataContext Database)
{
if (_PluginManifests == null)
{
lock (_PluginLock)
{
if (_PluginManifests == null)
{
Version hostVersion = typeof(Plugins).Assembly.GetName().Version;
var compatibilityData = new Lazy<PluginLibraryIncompatibility>(() => PluginLibrary.LoadManifest(Database).LoadIncompatibilityData());
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 pluginIncompatible = compatibilityData.Value.IncompatiblePlugins.FirstOrDefault(i => i.PluginId.Equals(pluginManifest.Id, StringComparison.OrdinalIgnoreCase) && pluginManifest.Version == i.Version);
if (pluginIncompatible != null)
throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", pluginManifest.Id, pluginManifest.VersionFormatted, pluginIncompatible.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, PluginLibraryIncompatibility PluginLibraryIncompatibility = null)
{
PluginManifest updatedManifest;
using (var packageStream = File.OpenRead(UpdatePluginPackageFilePath))
{
updatedManifest = UpdatePlugin(Database, ExistingManifest, packageStream, PluginLibraryIncompatibility);
}
// Remove Update after processing
File.Delete(UpdatePluginPackageFilePath);
return updatedManifest;
}
public static PluginManifest UpdatePlugin(DiscoDataContext Database, PluginManifest ExistingManifest, Stream UpdatePluginPackage, PluginLibraryIncompatibility PluginLibraryIncompatibility = 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 (PluginLibraryIncompatibility == null)
PluginLibraryIncompatibility = PluginLibrary.LoadManifest(Database).LoadIncompatibilityData();
var pluginIncompatibility = PluginLibraryIncompatibility.IncompatiblePlugins.FirstOrDefault(i => i.PluginId.Equals(packageManifest.Id, StringComparison.OrdinalIgnoreCase) && packageManifest.Version == i.Version);
if (pluginIncompatibility != null)
throw new InvalidOperationException(string.Format("The plugin [{0} v{1}] is not compatible: {2}", packageManifest.Id, packageManifest.VersionFormatted, pluginIncompatibility.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
}
}