Moves all plugin logic to new PluginLoader static class

This commit is contained in:
Neil Dorin
2020-03-25 15:04:44 -06:00
parent e6fb9cd1eb
commit 733dbf9bd7
5 changed files with 295 additions and 422 deletions

View File

@@ -25,7 +25,6 @@ namespace PepperDash.Essentials
{
HttpLogoServer LogoServer;
List<string> LoadedAssemblies = new List<string>();
public ControlSystem()
: base()
@@ -48,6 +47,8 @@ namespace PepperDash.Essentials
ConsoleAccessLevelEnum.AccessOperator);
}
CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator);
// CrestronConsole.AddNewConsoleCommand(S => { ConfigWriter.WriteConfigFile(null); }, "writeconfig", "writes the current config to a file", ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(s =>
{
@@ -80,6 +81,8 @@ namespace PepperDash.Essentials
GoWithLoad();
}
/// <summary>
/// Determines if the program is running on a processor (appliance) or server (VC-4).
///
@@ -160,20 +163,13 @@ namespace PepperDash.Essentials
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Starting Essentials load from configuration");
// Get the loaded assembly filenames
var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
var assemblyFiles = appDi.GetFiles("*.dll");
foreach (var file in assemblyFiles)
{
LoadedAssemblies.Add(file.Name);
}
PluginLoader.AddProgramAssemblies();
var filesReady = SetupFilesystem();
if (filesReady)
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Checking for plugins");
LoadPlugins();
PluginLoader.LoadPlugins();
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Folder structure verified. Loading config...");
if (!ConfigReader.LoadConfig2())
@@ -208,142 +204,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// Loads plugins
/// </summary>
void LoadPlugins()
{
var dir = Global.FilePathPrefix + "plugins";
if (Directory.Exists(dir))
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Plugins directory found, checking for factory plugins");
var di = new DirectoryInfo(dir);
var zFiles = di.GetFiles("*.cplz");
foreach (var fi in zFiles)
{
Debug.Console(0, "Found cplz: {0}. Unzipping into plugins directory", fi.Name);
var result = CrestronZIP.Unzip(fi.FullName, di.FullName);
Debug.Console(0, "UnZip Result: {0}", result.ToString());
fi.Delete();
}
var files = di.GetFiles("*.dll");
Dictionary<string, Assembly> assyList = new Dictionary<string, Assembly>();
foreach (FileInfo fi in files)
{
if (!CheckIfAssemblyExists(fi.Name))
{
try
{
string assyVersion = string.Empty;
var assy = Assembly.LoadFrom(fi.FullName);
var ver = assy.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
if (ver != null)
{
AssemblyInformationalVersionAttribute verAttribute = ver[0] as AssemblyInformationalVersionAttribute;
assyVersion = verAttribute.InformationalVersion;
}
else
assyList.Add(fi.FullName, assy);
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loaded plugin file '{0}', version {1}", fi.FullName, assyVersion);
}
catch
{
Debug.Console(2, "Assembly {0} is not a custom assembly", fi.FullName);
continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here
}
}
else
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Skipping plugin: {0}. There is already an assembly with that name loaded.", fi.Name);
}
foreach (var assy in assyList)
{
// iteratate this assembly's classes, looking for "LoadPlugin()" methods
try
{
var types = assy.Value.GetTypes();
foreach (var type in types)
{
try
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
var loadPlugin = methods.FirstOrDefault(m => m.Name.Equals("LoadPlugin"));
if (loadPlugin != null)
{
Debug.Console(2, "LoadPlugin method found in {0}", type.Name);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
var minimumVersion = fields.FirstOrDefault(p => p.Name.Equals("MinimumEssentialsFrameworkVersion"));
if (minimumVersion != null)
{
Debug.Console(2, "MinimumEssentialsFrameworkVersion found");
var minimumVersionString = minimumVersion.GetValue(null) as string;
if (!string.IsNullOrEmpty(minimumVersionString))
{
var passed = Global.IsRunningMinimumVersionOrHigher(minimumVersionString);
if (!passed)
{
Debug.Console(0, Debug.ErrorLogLevel.Error, "Plugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin", minimumVersionString);
continue;
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Passed plugin passed dependency check (required version {0})", minimumVersionString);
}
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Warning, "MinimumEssentialsFrameworkVersion found but not set. Loading plugin, but your mileage may vary.");
}
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Warning, "MinimumEssentialsFrameworkVersion not found. Loading plugin, but your mileage may vary.");
}
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Adding plugin: {0}", assy.Key);
loadPlugin.Invoke(null, null);
}
}
catch
{
Debug.Console(2, "Load Plugin not found. {0} is not a plugin assembly", assy.Value.FullName);
continue;
}
}
}
catch
{
Debug.Console(2, "Assembly {0} is not a custom assembly. Types cannot be loaded.", assy.Value.FullName);
continue;
}
}
// plugin dll will be loaded. Any classes in plugin should have a static constructor
// that registers that class with the Core.DeviceFactory
}
}
/// <summary>
/// Checks if the filename matches an already loaded assembly file's name
/// </summary>
/// <param name="filename"></param>
/// <returns>True if file already matches loaded assembly file.</returns>
bool CheckIfAssemblyExists(string filename)
{
var loadedAssembly = LoadedAssemblies.FirstOrDefault(s => s.Equals(filename));
if (loadedAssembly != null)
return true;
else
return false;
}
/// <summary>
/// Verifies filesystem is set up. IR, SGD, and programX folders

View File

@@ -1,266 +0,0 @@
//using System;
//using System.Collections.Generic;
//using System.Linq;
//using System.Text;
//using Crestron.SimplSharp;
//using Crestron.SimplSharp.CrestronIO;
//using Crestron.SimplSharp.Net.Http;
//using Newtonsoft.Json;
//using Newtonsoft.Json.Linq;
//using PepperDash.Essentials.Core;
//using PepperDash.Essentials.Core.Http;
//using PepperDash.Core;
//namespace PepperDash.Essentials
//{
// public class EssentialsHttpApiHandler
// {
// string ConfigPath;
// string PresetsPathPrefix;
// EssentialsHttpServer Server;
// /// <summary>
// ///
// /// </summary>
// /// <param name="server">HTTP server to attach to</param>
// /// <param name="configPath">The full path to configuration file</param>
// /// <param name="presetsListPath">The folder prefix for the presets path, eq "\HTML\presets\"</param>
// public EssentialsHttpApiHandler(EssentialsHttpServer server, string configPath, string presetsPathPrefix)
// {
// if (server == null) throw new ArgumentNullException("server");
// Server = server;
// ConfigPath = configPath;
// PresetsPathPrefix = presetsPathPrefix;
// server.ApiRequest += Server_ApiRequest;
// }
// void Server_ApiRequest(object sender, Crestron.SimplSharp.Net.Http.OnHttpRequestArgs args)
// {
// try
// {
// var path = args.Request.Path.ToLower();
// if (path == "/api/config")
// HandleApiConfig(args);
// else if (path.StartsWith("/api/presetslist/"))
// HandleApiPresetsList(args);
// else if (path == "/api/presetslists")
// HandleApiGetPresetsLists(args.Request, args.Response);
// else
// {
// args.Response.Code = 404;
// return;
// }
// args.Response.Header.SetHeaderValue("Access-Control-Allow-Origin", "*");
// args.Response.Header.SetHeaderValue("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS");
// }
// catch (Exception e)
// {
// Debug.Console(1, "Uncaught HTTP server error: \n{0}", e);
// args.Response.Code = 500;
// }
// }
// /// <summary>
// /// GET will return the running configuration. POST will attempt to take in a new config
// /// and restart the program.
// /// </summary>
// void HandleApiConfig(OnHttpRequestArgs args)
// {
// var request = args.Request;
// if (request.Header.RequestType == "GET")
// {
// if (File.Exists(ConfigPath))
// {
// Debug.Console(2, "Sending config:{0}", ConfigPath);
// args.Response.Header.ContentType = EssentialsHttpServer.GetContentType(new FileInfo(ConfigPath).Extension);
// args.Response.ContentStream = new FileStream(ConfigPath, FileMode.Open, FileAccess.Read);
// }
// }
// else if (request.Header.RequestType == "POST")
// {
// Debug.Console(2, "Post type: '{0}'", request.Header.ContentType);
// // Make sure we're receiving at least good json
// Debug.Console(1, "Receving new config");
// if (GetContentStringJson(args) == null)
// return;
// //---------------------------- try to move these into common method
// // Move current file aside
// var bakPath = ConfigPath + ".bak";
// if (File.Exists(bakPath))
// File.Delete(bakPath);
// File.Move(ConfigPath, bakPath);
// // Write the file
// using (FileStream fs = File.Open(ConfigPath, FileMode.OpenOrCreate))
// using (StreamWriter sw = new StreamWriter(fs))
// {
// try
// {
// sw.Write(args.Request.ContentString);
// }
// catch (Exception e)
// {
// string err = string.Format("Error writing received config file:\r{0}", e);
// CrestronConsole.PrintLine(err);
// ErrorLog.Warn(err);
// // Put file back
// File.Move(ConfigPath + ".bak", ConfigPath);
// args.Response.Code = 500;
// return;
// }
// }
// // If client says "yeah, restart" and has a good token
// // Restart program
// string consoleResponse = null;
// var restart = CrestronConsole.SendControlSystemCommand("progreset -p:" +
// InitialParametersClass.ApplicationNumber, ref consoleResponse);
// if (!restart) Debug.Console(0, "CAN'T DO THAT YO: {0}", consoleResponse);
// }
// }
// void HandleApiPresetsList(OnHttpRequestArgs args)
// {
// var listPath = PresetsPathPrefix + args.Request.Path.Remove(0, 17);
// Debug.Console(2, "Checking for preset list '{0}'", listPath);
// if (args.Request.Header.RequestType == "GET")
// {
// if (File.Exists(listPath))
// {
// Debug.Console(2, "Sending presets file:{0}", listPath);
// args.Response.Header.ContentType = EssentialsHttpServer.GetContentType(new FileInfo(listPath).Extension);
// args.Response.ContentStream = new FileStream(listPath, FileMode.Open, FileAccess.Read);
// }
// }
// else if (args.Request.Header.RequestType == "POST")
// {
// // Make sure we're receiving at least good json
// Debug.Console(1, "Receving new presets");
// if (GetContentStringJson(args) == null)
// return;
// //---------------------------- try to move these into common method
// // Move current file aside
// var bakPath = listPath + ".new";
// Debug.Console(2, "Moving presets file to {0}", bakPath);
// if(File.Exists(bakPath))
// File.Delete(bakPath);
// File.Move(listPath, bakPath);
// Debug.Console(2, "Writing new file");
// // Write the file
// using (FileStream fs = File.OpenWrite(listPath))
// using (StreamWriter sw = new StreamWriter(fs))
// {
// try
// {
// Debug.Console(2, "Writing {1}, {0} bytes", args.Request.ContentString.Length, listPath);
// sw.Write(args.Request.ContentString);
// }
// catch (Exception e)
// {
// string err = string.Format("Error writing received presets file:\r{0}", e);
// CrestronConsole.PrintLine(err);
// ErrorLog.Warn(err);
// // Put file back
// File.Move(listPath + ".bak", listPath);
// args.Response.Code = 500;
// return;
// }
// }
// }
// }
// void HandleApiGetPresetsLists(HttpServerRequest request, HttpServerResponse response)
// {
// if (request.Header.RequestType != "GET")
// {
// response.Code = 404; // This should be a 405 with an allow header
// return;
// }
// if (Directory.Exists(PresetsPathPrefix))
// {
// //CrestronConsole.PrintLine("Parsing presets directory");
// List<string> files = Directory.GetFiles(PresetsPathPrefix, "*.json")
// .ToList().Select(f => Path.GetFileName(f)).ToList();
// if (files.Count > 0)
// files.Sort();
// var json = JsonConvert.SerializeObject(files);
// response.Header.ContentType = "application/json";
// response.ContentString = json;
// }
// // //CrestronConsole.PrintLine("Found {0} files", files.Count);
// // JObject jo = new JObject();
// // JArray ja = new JArray();
// // foreach (var filename in files)
// // {
// // try
// // {
// // using (StreamReader sr = new StreamReader(filename))
// // {
// // JObject tempJo = JObject.Parse(sr.ReadToEnd());
// // if (tempJo.Value<string>("content").Equals("presetsList"))
// // {
// // var jItem = new JObject(); // make a new object
// // jItem.Add("Name", tempJo["name"]);
// // jItem.Add("File", filename);
// // jItem.Add("Url", Uri.EscapeUriString(new Uri(
// // filename.Replace("\\html", "")
// // .Replace("\\HTML", "")
// // .Replace('\\', '/'), UriKind.Relative).ToString()));
// // ja.Add(jItem); // add to array
// // }
// // else
// // CrestronConsole.PrintLine("Cannot use presets file '{0}'", filename);
// // }
// // }
// // catch
// // {
// // // ignore failures - maybe delete them
// // CrestronConsole.PrintLine("Unable to read presets file '{0}'", filename);
// // }
// // }
// // jo.Add("PresetChannelLists", ja);
// // //CrestronConsole.PrintLine(jo.ToString());
// // response.Header.ContentType = "application/json";
// // response.ContentString = jo.ToString();
// //}
// //else
// // CrestronConsole.PrintLine("No presets files in directory");
// }
// /// <summary>
// /// Simply does what it says
// /// </summary>
// JObject GetContentStringJson(OnHttpRequestArgs args)
// {
// //var content = args.Request.ContentString;
// //Debug.Console(1, "{0}", content);
// try
// {
// // just see if it parses properly
// return JObject.Parse(args.Request.ContentString);
// }
// catch (Exception e)
// {
// string err = string.Format("JSON Error reading config file:\r{0}", e);
// CrestronConsole.PrintLine(err);
// ErrorLog.Warn(err);
// args.Response.Code = 400; // Bad request
// return null;
// }
// }
// }
//}

View File

@@ -166,7 +166,7 @@
<Compile Include="ControlSystem.cs" />
<Compile Include="Factory\UiDeviceFactory.cs" />
<Compile Include="Fusion\EssentialsHuddleVtc1FusionController.cs" />
<Compile Include="HttpApiHandler.cs" />
<Compile Include="PluginLoading\PluginLoading.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Room\Config\EssentialsDualDisplayRoomPropertiesConfig.cs" />
<Compile Include="Room\Config\EssentialsNDisplayRoomPropertiesConfig.cs" />

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Reflection;
using PepperDash.Core;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials
{
/// <summary>
/// Deals with loading plugins at runtime
/// </summary>
public static class PluginLoader
{
/// <summary>
/// The complete list of loaded assemblies. Includes Essentials Framework assemblies and plugins
/// </summary>
public static List<LoadedAssembly> LoadedAssemblies { get; private set; }
/// <summary>
/// The list of assemblies loaded from the plugins folder
/// </summary>
static List<LoadedAssembly> LoadedPluginFolderAssemblies;
static PluginLoader()
{
LoadedAssemblies = new List<LoadedAssembly>();
LoadedPluginFolderAssemblies = new List<LoadedAssembly>();
}
/// <summary>
/// Retrieves all the loaded assemblies from the program directory
/// </summary>
public static void AddProgramAssemblies()
{
// Get the loaded assembly filenames
var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
var assemblyFiles = appDi.GetFiles("*.dll");
foreach (var file in assemblyFiles)
{
try
{
LoadAssembly(file.FullName);
}
catch
{
Debug.Console(2, "Assembly {0} is not a custom assembly", file.FullName);
}
}
}
/// <summary>
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
/// </summary>
/// <param name="fileName"></param>
static LoadedAssembly LoadAssembly(string fileName)
{
if (!CheckIfAssemblyExists(fileName))
{
var assembly = Assembly.LoadFrom(fileName);
if (assembly != null)
{
var assyVersion = GetAssemblyVersion(assembly);
var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly);
LoadedAssemblies.Add(loadedAssembly);
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loaded plugin file '{0}', version {1}", loadedAssembly.FileName, loadedAssembly.Version);
return loadedAssembly;
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Unable to load assembly: '{0}'", fileName);
}
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Skipping assembly: {0}. There is already an assembly with that name loaded.", fileName);
}
return null;
}
/// <summary>
/// Attempts to get the assembly informational version and if not possible gets the version
/// </summary>
/// <param name="assembly"></param>
/// <returns></returns>
static string GetAssemblyVersion(Assembly assembly)
{
var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
if (ver != null)
{
// Get the AssemblyInformationalVersion
AssemblyInformationalVersionAttribute verAttribute = ver[0] as AssemblyInformationalVersionAttribute;
return verAttribute.InformationalVersion;
}
else
{
// Get the AssemblyVersion
var version = assembly.GetName().Version;
var verStr = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
return verStr;
}
}
/// <summary>
/// Checks if the filename matches an already loaded assembly file's name
/// </summary>
/// <param name="filename"></param>
/// <returns>True if file already matches loaded assembly file.</returns>
public static bool CheckIfAssemblyExists(string filename)
{
Debug.Console(2, "Checking if assembly: {0} is loaded...", filename);
var loadedAssembly = LoadedAssemblies.FirstOrDefault(s => s.FileName.Equals(filename));
if (loadedAssembly != null)
{
return true;
}
else
{
return false;
}
}
/// <summary>
/// Used by console command to report the currently loaded assemblies and versions
/// </summary>
/// <param name="command"></param>
public static void ReportAssemblyVersions(string command)
{
Debug.Console(0, "Loaded Assemblies:");
foreach (var assembly in LoadedAssemblies)
{
Debug.Console(0, "{0} Version: {1}", assembly.FileName, assembly.Version);
}
}
/// <summary>
/// Loads plugins
/// </summary>
public static void LoadPlugins()
{
var dir = Global.FilePathPrefix + "plugins";
var tempDir = dir + Global.DirectorySeparator + "temp";
if (Directory.Exists(dir))
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Plugins directory found, checking for factory plugins");
var di = new DirectoryInfo(dir);
var zFiles = di.GetFiles("*.cplz");
foreach (var fi in zFiles)
{
Directory.CreateDirectory(tempDir);
var tempDi = new DirectoryInfo(tempDir);
Debug.Console(0, "Found cplz: {0}. Unzipping into temp plugins directory", fi.Name);
var result = CrestronZIP.Unzip(fi.FullName, tempDi.FullName);
Debug.Console(0, "UnZip Result: {0}", result.ToString());
var files = tempDi.GetFiles("*.dll");
foreach (FileInfo file in files)
{
try
{
var pluginAssembly = LoadAssembly(fi.FullName);
if (pluginAssembly != null)
{
LoadedPluginFolderAssemblies.Add(pluginAssembly);
}
}
catch
{
Debug.Console(2, "Assembly {0} is not a custom assembly", file.FullName);
continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here
}
}
// Delete the .cplz and the temp directory
fi.Delete();
Directory.Delete(tempDir);
}
// Iterate the loaded assemblies and try to call the LoadPlugin method
foreach (var loadedAssembly in LoadedPluginFolderAssemblies)
{
// iteratate this assembly's classes, looking for "LoadPlugin()" methods
try
{
var assy = loadedAssembly.Assembly;
var types = assy.GetTypes();
foreach (var type in types)
{
try
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
var loadPlugin = methods.FirstOrDefault(m => m.Name.Equals("LoadPlugin"));
if (loadPlugin != null)
{
Debug.Console(2, "LoadPlugin method found in {0}", type.Name);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
var minimumVersion = fields.FirstOrDefault(p => p.Name.Equals("MinimumEssentialsFrameworkVersion"));
if (minimumVersion != null)
{
Debug.Console(2, "MinimumEssentialsFrameworkVersion found");
var minimumVersionString = minimumVersion.GetValue(null) as string;
if (!string.IsNullOrEmpty(minimumVersionString))
{
var passed = Global.IsRunningMinimumVersionOrHigher(minimumVersionString);
if (!passed)
{
Debug.Console(0, Debug.ErrorLogLevel.Error, "Plugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin", minimumVersionString);
continue;
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Passed plugin passed dependency check (required version {0})", minimumVersionString);
}
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Warning, "MinimumEssentialsFrameworkVersion found but not set. Loading plugin, but your mileage may vary.");
}
}
else
{
Debug.Console(0, Debug.ErrorLogLevel.Warning, "MinimumEssentialsFrameworkVersion not found. Loading plugin, but your mileage may vary.");
}
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Adding plugin: {0}", loadedAssembly.FileName);
loadPlugin.Invoke(null, null);
}
}
catch
{
Debug.Console(2, "Load Plugin not found. {0} is not a plugin assembly", loadedAssembly.FileName);
continue;
}
}
}
catch
{
Debug.Console(2, "Assembly {0} is not a custom assembly. Types cannot be loaded.", loadedAssembly.FileName);
continue;
}
}
// plugin dll will be loaded. Any classes in plugin should have a static constructor
// that registers that class with the Core.DeviceFactory
}
}
}
/// <summary>
/// Represents an assembly loaded at runtime and it's associated metadata
/// </summary>
public class LoadedAssembly
{
public string FileName { get; private set; }
public string Version { get; private set; }
public Assembly Assembly { get; private set; }
public LoadedAssembly(string fileName, string version, Assembly assembly)
{
FileName = fileName;
Version = version;
Assembly = assembly;
}
}
}

View File

@@ -62,15 +62,6 @@ namespace PepperDash.Essentials.Core
FilePathPrefix = prefix;
}
/// <summary>
/// Sets the file path prefix
/// </summary>
/// <param name="prefix"></param>
public static void SetApplicationDirectoryPathPrefix(string prefix)
{
ApplicationDirectoryPathPrefix = prefix;
}
static string _AssemblyVersion;
/// <summary>