mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-13 19:54:59 +00:00
wip: address performance issues in plugin loading and versioning
This commit is contained in:
@@ -2,13 +2,17 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Crestron.SimplSharp;
|
using Crestron.SimplSharp;
|
||||||
using Crestron.SimplSharp.CrestronIO;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
|
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Essentials.Core;
|
using PepperDash.Essentials.Core;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using CrestronIO = Crestron.SimplSharp.CrestronIO;
|
||||||
|
using SystemIO = System.IO;
|
||||||
|
|
||||||
namespace PepperDash.Essentials
|
namespace PepperDash.Essentials
|
||||||
{
|
{
|
||||||
@@ -27,6 +31,11 @@ namespace PepperDash.Essentials
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
static List<LoadedAssembly> LoadedPluginFolderAssemblies;
|
static List<LoadedAssembly> LoadedPluginFolderAssemblies;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of plugins that were found to be incompatible with .NET 8
|
||||||
|
/// </summary>
|
||||||
|
public static List<IncompatiblePlugin> IncompatiblePlugins { get; private set; }
|
||||||
|
|
||||||
public static LoadedAssembly EssentialsAssembly { get; private set; }
|
public static LoadedAssembly EssentialsAssembly { get; private set; }
|
||||||
|
|
||||||
public static LoadedAssembly PepperDashCoreAssembly { get; private set; }
|
public static LoadedAssembly PepperDashCoreAssembly { get; private set; }
|
||||||
@@ -46,12 +55,28 @@ namespace PepperDash.Essentials
|
|||||||
// The temp directory where .cplz archives will be unzipped to
|
// The temp directory where .cplz archives will be unzipped to
|
||||||
static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp";
|
static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp";
|
||||||
|
|
||||||
|
// Known incompatible types in .NET 8
|
||||||
|
private static readonly HashSet<string> KnownIncompatibleTypes = new HashSet<string>
|
||||||
|
{
|
||||||
|
"System.Net.ICertificatePolicy",
|
||||||
|
"System.Security.Cryptography.SHA1CryptoServiceProvider",
|
||||||
|
"System.Web.HttpUtility",
|
||||||
|
"System.Configuration.ConfigurationManager",
|
||||||
|
"System.Web.Services.Protocols.SoapHttpClientProtocol",
|
||||||
|
"System.Runtime.Remoting",
|
||||||
|
"System.EnterpriseServices",
|
||||||
|
"System.Runtime.Serialization.Formatters.Binary.BinaryFormatter",
|
||||||
|
"System.Security.SecurityManager",
|
||||||
|
"System.Security.Permissions.FileIOPermission",
|
||||||
|
"System.AppDomain.CreateDomain"
|
||||||
|
};
|
||||||
|
|
||||||
static PluginLoader()
|
static PluginLoader()
|
||||||
{
|
{
|
||||||
LoadedAssemblies = new List<LoadedAssembly>();
|
LoadedAssemblies = new List<LoadedAssembly>();
|
||||||
LoadedPluginFolderAssemblies = new List<LoadedAssembly>();
|
LoadedPluginFolderAssemblies = new List<LoadedAssembly>();
|
||||||
EssentialsPluginAssemblies = new List<LoadedAssembly>();
|
EssentialsPluginAssemblies = new List<LoadedAssembly>();
|
||||||
|
IncompatiblePlugins = new List<IncompatiblePlugin>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -61,7 +86,7 @@ namespace PepperDash.Essentials
|
|||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials");
|
Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials");
|
||||||
// Get the loaded assembly filenames
|
// Get the loaded assembly filenames
|
||||||
var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
var appDi = new SystemIO.DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
||||||
var assemblyFiles = appDi.GetFiles("*.dll");
|
var assemblyFiles = appDi.GetFiles("*.dll");
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length);
|
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length);
|
||||||
@@ -113,7 +138,6 @@ namespace PepperDash.Essentials
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void SetEssentialsAssembly(string name, Assembly assembly)
|
public static void SetEssentialsAssembly(string name, Assembly assembly)
|
||||||
{
|
{
|
||||||
var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name));
|
var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name));
|
||||||
@@ -125,19 +149,108 @@ namespace PepperDash.Essentials
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
|
/// Checks if a plugin is compatible with .NET 8 by examining its metadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileName"></param>
|
/// <param name="filePath">Path to the plugin assembly</param>
|
||||||
static LoadedAssembly LoadAssembly(string filePath)
|
/// <returns>Tuple with compatibility result, reason if incompatible, and referenced assemblies</returns>
|
||||||
|
public static (bool IsCompatible, string Reason, List<string> References) IsPluginCompatibleWithNet8(string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load {0}", filePath);
|
List<string> referencedAssemblies = new List<string>();
|
||||||
|
|
||||||
|
using (SystemIO.FileStream fs = new SystemIO.FileStream(filePath, SystemIO.FileMode.Open,
|
||||||
|
SystemIO.FileAccess.Read, SystemIO.FileShare.ReadWrite))
|
||||||
|
using (PEReader peReader = new PEReader(fs))
|
||||||
|
{
|
||||||
|
if (!peReader.HasMetadata)
|
||||||
|
return (false, "Not a valid .NET assembly", referencedAssemblies);
|
||||||
|
|
||||||
|
MetadataReader metadataReader = peReader.GetMetadataReader();
|
||||||
|
|
||||||
|
// Collect assembly references
|
||||||
|
foreach (var assemblyRefHandle in metadataReader.AssemblyReferences)
|
||||||
|
{
|
||||||
|
var assemblyRef = metadataReader.GetAssemblyReference(assemblyRefHandle);
|
||||||
|
string assemblyName = metadataReader.GetString(assemblyRef.Name);
|
||||||
|
referencedAssemblies.Add(assemblyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for references to known incompatible types
|
||||||
|
foreach (var typeRefHandle in metadataReader.TypeReferences)
|
||||||
|
{
|
||||||
|
var typeRef = metadataReader.GetTypeReference(typeRefHandle);
|
||||||
|
string typeNamespace = metadataReader.GetString(typeRef.Namespace);
|
||||||
|
string typeName = metadataReader.GetString(typeRef.Name);
|
||||||
|
|
||||||
|
string fullTypeName = $"{typeNamespace}.{typeName}";
|
||||||
|
if (KnownIncompatibleTypes.Contains(fullTypeName))
|
||||||
|
{
|
||||||
|
return (false, $"Uses incompatible type: {fullTypeName}", referencedAssemblies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for explicit .NET 8 compatibility attribute
|
||||||
|
bool hasNet8Attribute = false;
|
||||||
|
foreach (var customAttributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes())
|
||||||
|
{
|
||||||
|
var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle);
|
||||||
|
var ctorHandle = customAttribute.Constructor;
|
||||||
|
|
||||||
|
if (ctorHandle.Kind == HandleKind.MemberReference)
|
||||||
|
{
|
||||||
|
var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle);
|
||||||
|
var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent);
|
||||||
|
|
||||||
|
string typeName = metadataReader.GetString(typeRef.Name);
|
||||||
|
if (typeName == "Net8CompatibleAttribute" || typeName == "TargetFrameworkAttribute")
|
||||||
|
{
|
||||||
|
hasNet8Attribute = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNet8Attribute)
|
||||||
|
{
|
||||||
|
return (true, null, referencedAssemblies);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't determine incompatibility, assume it's compatible
|
||||||
|
return (true, null, referencedAssemblies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, $"Error analyzing assembly: {ex.Message}", new List<string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">Path to the assembly file</param>
|
||||||
|
/// <param name="requestedBy">Name of the plugin requesting this assembly (null for direct loads)</param>
|
||||||
|
static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check .NET 8 compatibility before loading
|
||||||
|
var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath);
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
string fileName = CrestronIO.Path.GetFileName(filePath);
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason);
|
||||||
|
|
||||||
|
var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy);
|
||||||
|
IncompatiblePlugins.Add(incompatiblePlugin);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var assembly = Assembly.LoadFrom(filePath);
|
var assembly = Assembly.LoadFrom(filePath);
|
||||||
if (assembly != null)
|
if (assembly != null)
|
||||||
{
|
{
|
||||||
var assyVersion = GetAssemblyVersion(assembly);
|
var assyVersion = GetAssemblyVersion(assembly);
|
||||||
|
|
||||||
var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly);
|
var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly);
|
||||||
LoadedAssemblies.Add(loadedAssembly);
|
LoadedAssemblies.Add(loadedAssembly);
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version);
|
Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version);
|
||||||
@@ -147,14 +260,47 @@ namespace PepperDash.Essentials
|
|||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath);
|
Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch(Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
catch(SystemIO.FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded"))
|
||||||
|
{
|
||||||
|
// Get the assembly name from the file path
|
||||||
|
string assemblyName = CrestronIO.Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
|
||||||
|
// Try to find the already loaded assembly
|
||||||
|
var existingAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (existingAssembly != null)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName);
|
||||||
|
var assyVersion = GetAssemblyVersion(existingAssembly);
|
||||||
|
var loadedAssembly = new LoadedAssembly(existingAssembly.GetName().Name, assyVersion, existingAssembly);
|
||||||
|
LoadedAssemblies.Add(loadedAssembly);
|
||||||
|
return loadedAssembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
string fileName = CrestronIO.Path.GetFileName(filePath);
|
||||||
|
|
||||||
|
// Check if this might be a .NET Framework compatibility issue
|
||||||
|
if (ex.Message.Contains("Could not load type") ||
|
||||||
|
ex.Message.Contains("Unable to load one or more of the requested types"))
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}",
|
||||||
|
fileName, ex.Message);
|
||||||
|
IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -216,12 +362,25 @@ namespace PepperDash.Essentials
|
|||||||
CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
|
CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
//CrestronConsole.ConsoleCommandResponse("Loaded Assemblies:" + CrestronEnvironment.NewLine);
|
if (IncompatiblePlugins.Count > 0)
|
||||||
//foreach (var assembly in LoadedAssemblies)
|
{
|
||||||
//{
|
CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine);
|
||||||
// CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
|
foreach (var plugin in IncompatiblePlugins)
|
||||||
//}
|
{
|
||||||
|
if (plugin.TriggeredBy != "Direct load")
|
||||||
|
{
|
||||||
|
CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine,
|
||||||
|
plugin.Name, plugin.Reason, plugin.TriggeredBy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine,
|
||||||
|
plugin.Name, plugin.Reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder
|
/// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -229,14 +388,14 @@ namespace PepperDash.Essentials
|
|||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder...");
|
Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder...");
|
||||||
|
|
||||||
var pluginDi = new DirectoryInfo(_pluginDirectory);
|
var pluginDi = new SystemIO.DirectoryInfo(_pluginDirectory);
|
||||||
var pluginFiles = pluginDi.GetFiles("*.dll");
|
var pluginFiles = pluginDi.GetFiles("*.dll");
|
||||||
|
|
||||||
if (pluginFiles.Length > 0)
|
if (pluginFiles.Length > 0)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(_loadedPluginsDirectoryPath))
|
if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,14 +412,14 @@ namespace PepperDash.Essentials
|
|||||||
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name;
|
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name;
|
||||||
|
|
||||||
// Check if there is a previous file in the loadedPlugins directory and delete
|
// Check if there is a previous file in the loadedPlugins directory and delete
|
||||||
if (File.Exists(filePath))
|
if (SystemIO.File.Exists(filePath))
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
|
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
|
||||||
File.Delete(filePath);
|
SystemIO.File.Delete(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the file
|
// Move the file
|
||||||
File.Move(pluginFile.FullName, filePath);
|
SystemIO.File.Move(pluginFile.FullName, filePath);
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath);
|
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -284,21 +443,21 @@ namespace PepperDash.Essentials
|
|||||||
static void UnzipAndMoveCplzArchives()
|
static void UnzipAndMoveCplzArchives()
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder...");
|
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder...");
|
||||||
var di = new DirectoryInfo(_pluginDirectory);
|
var di = new SystemIO.DirectoryInfo(_pluginDirectory);
|
||||||
var zFiles = di.GetFiles("*.cplz");
|
var zFiles = di.GetFiles("*.cplz");
|
||||||
|
|
||||||
if (zFiles.Length > 0)
|
if (zFiles.Length > 0)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(_loadedPluginsDirectoryPath))
|
if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var zfi in zFiles)
|
foreach (var zfi in zFiles)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_tempDirectory);
|
SystemIO.Directory.CreateDirectory(_tempDirectory);
|
||||||
var tempDi = new DirectoryInfo(_tempDirectory);
|
var tempDi = new SystemIO.DirectoryInfo(_tempDirectory);
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.Name);
|
Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.Name);
|
||||||
var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName);
|
var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName);
|
||||||
@@ -316,14 +475,14 @@ namespace PepperDash.Essentials
|
|||||||
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name;
|
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name;
|
||||||
|
|
||||||
// Check if there is a previous file in the loadedPlugins directory and delete
|
// Check if there is a previous file in the loadedPlugins directory and delete
|
||||||
if (File.Exists(filePath))
|
if (SystemIO.File.Exists(filePath))
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
|
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
|
||||||
File.Delete(filePath);
|
SystemIO.File.Delete(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the file
|
// Move the file
|
||||||
File.Move(tempFile.FullName, filePath);
|
SystemIO.File.Move(tempFile.FullName, filePath);
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath);
|
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -339,7 +498,7 @@ namespace PepperDash.Essentials
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete the .cplz and the temp directory
|
// Delete the .cplz and the temp directory
|
||||||
Directory.Delete(_tempDirectory, true);
|
SystemIO.Directory.Delete(_tempDirectory, true);
|
||||||
zfi.Delete();
|
zfi.Delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,16 +511,40 @@ namespace PepperDash.Essentials
|
|||||||
static void LoadPluginAssemblies()
|
static void LoadPluginAssemblies()
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder...");
|
Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder...");
|
||||||
var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath);
|
var pluginDi = new CrestronIO.DirectoryInfo(_loadedPluginsDirectoryPath);
|
||||||
var pluginFiles = pluginDi.GetFiles("*.dll");
|
var pluginFiles = pluginDi.GetFiles("*.dll");
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length);
|
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length);
|
||||||
|
|
||||||
|
// First, check compatibility of all assemblies before loading any
|
||||||
|
var assemblyCompatibility = new Dictionary<string, (bool IsCompatible, string Reason, List<string> References)>();
|
||||||
|
|
||||||
foreach (var pluginFile in pluginFiles)
|
foreach (var pluginFile in pluginFiles)
|
||||||
{
|
{
|
||||||
var loadedAssembly = LoadAssembly(pluginFile.FullName);
|
string fileName = pluginFile.Name;
|
||||||
|
assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName);
|
||||||
LoadedPluginFolderAssemblies.Add(loadedAssembly);
|
}
|
||||||
|
|
||||||
|
// Now load compatible assemblies and track incompatible ones
|
||||||
|
foreach (var pluginFile in pluginFiles)
|
||||||
|
{
|
||||||
|
string fileName = pluginFile.Name;
|
||||||
|
var (isCompatible, reason, references) = assemblyCompatibility[fileName];
|
||||||
|
|
||||||
|
if (!isCompatible)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason);
|
||||||
|
IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the assembly
|
||||||
|
var loadedAssembly = LoadAssembly(pluginFile.FullName, null);
|
||||||
|
|
||||||
|
if (loadedAssembly != null)
|
||||||
|
{
|
||||||
|
LoadedPluginFolderAssemblies.Add(loadedAssembly);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded.");
|
Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded.");
|
||||||
@@ -373,8 +556,13 @@ namespace PepperDash.Essentials
|
|||||||
static void LoadCustomPluginTypes()
|
static void LoadCustomPluginTypes()
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types...");
|
Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types...");
|
||||||
|
|
||||||
foreach (var loadedAssembly in LoadedPluginFolderAssemblies)
|
foreach (var loadedAssembly in LoadedPluginFolderAssemblies)
|
||||||
{
|
{
|
||||||
|
// Skip if assembly is null (can happen if we had loading issues)
|
||||||
|
if (loadedAssembly == null || loadedAssembly.Assembly == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
// iteratate this assembly's classes, looking for "LoadPlugin()" methods
|
// iteratate this assembly's classes, looking for "LoadPlugin()" methods
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -385,11 +573,49 @@ namespace PepperDash.Essentials
|
|||||||
types = assy.GetTypes();
|
types = assy.GetTypes();
|
||||||
Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}");
|
Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}");
|
||||||
}
|
}
|
||||||
|
catch (ReflectionTypeLoadException e)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
|
||||||
|
loadedAssembly.Name, e.Message);
|
||||||
|
|
||||||
|
// Check if any of the loader exceptions are due to missing assemblies
|
||||||
|
foreach (var loaderEx in e.LoaderExceptions)
|
||||||
|
{
|
||||||
|
if (loaderEx is SystemIO.FileNotFoundException fileNotFoundEx)
|
||||||
|
{
|
||||||
|
string missingAssembly = fileNotFoundEx.FileName;
|
||||||
|
if (!string.IsNullOrEmpty(missingAssembly))
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}",
|
||||||
|
loadedAssembly.Name, missingAssembly);
|
||||||
|
|
||||||
|
// Add to incompatible plugins with dependency information
|
||||||
|
IncompatiblePlugins.Add(new IncompatiblePlugin(
|
||||||
|
CrestronIO.Path.GetFileName(missingAssembly),
|
||||||
|
$"Missing dependency required by {loadedAssembly.Name}",
|
||||||
|
loadedAssembly.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
catch (TypeLoadException e)
|
catch (TypeLoadException e)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
|
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
|
||||||
loadedAssembly.Name, e.Message);
|
loadedAssembly.Name, e.Message);
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
|
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
|
||||||
|
|
||||||
|
// Add to incompatible plugins if this is likely a .NET 8 compatibility issue
|
||||||
|
if (e.Message.Contains("Could not load type") ||
|
||||||
|
e.Message.Contains("Unable to load one or more of the requested types"))
|
||||||
|
{
|
||||||
|
IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name,
|
||||||
|
$"Type loading error: {e.Message}",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,22 +640,63 @@ namespace PepperDash.Essentials
|
|||||||
loadedAssembly.Name, e.Message, type.Name);
|
loadedAssembly.Name, e.Message, type.Name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}",
|
Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}",
|
||||||
loadedAssembly.Name, e.Message);
|
loadedAssembly.Name, e.Message);
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace);
|
Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace);
|
||||||
|
|
||||||
|
// Add to incompatible plugins if this is likely a .NET 8 compatibility issue
|
||||||
|
if (e.Message.Contains("Could not load type") ||
|
||||||
|
e.Message.Contains("Unable to load one or more of the requested types"))
|
||||||
|
{
|
||||||
|
IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name,
|
||||||
|
$"Assembly loading error: {e.Message}",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update incompatible plugins with dependency information
|
||||||
|
UpdateIncompatiblePluginDependencies();
|
||||||
|
|
||||||
// plugin dll will be loaded. Any classes in plugin should have a static constructor
|
// plugin dll will be loaded. Any classes in plugin should have a static constructor
|
||||||
// that registers that class with the Core.DeviceFactory
|
// that registers that class with the Core.DeviceFactory
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types.");
|
Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates incompatible plugins with information about which plugins depend on them
|
||||||
|
/// </summary>
|
||||||
|
private static void UpdateIncompatiblePluginDependencies(Dictionary<string, List<string>> pluginDependencies)
|
||||||
|
{
|
||||||
|
// For each incompatible plugin
|
||||||
|
foreach (var incompatiblePlugin in IncompatiblePlugins)
|
||||||
|
{
|
||||||
|
// If it already has a requestedBy, skip it
|
||||||
|
if (incompatiblePlugin.TriggeredBy != "Direct load")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Find plugins that depend on this incompatible plugin
|
||||||
|
foreach (var plugin in pluginDependencies)
|
||||||
|
{
|
||||||
|
string pluginName = plugin.Key;
|
||||||
|
List<string> dependencies = plugin.Value;
|
||||||
|
|
||||||
|
// If this plugin depends on the incompatible plugin
|
||||||
|
if (dependencies.Contains(incompatiblePlugin.Name) ||
|
||||||
|
dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ",")))
|
||||||
|
{
|
||||||
|
incompatiblePlugin.UpdateTriggeringPlugin(pluginName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a
|
/// Loads a
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -506,7 +773,6 @@ namespace PepperDash.Essentials
|
|||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Loading legacy plugin: {0}", loadedAssembly.Name);
|
Debug.LogMessage(LogEventLevel.Information, "Loading legacy plugin: {0}", loadedAssembly.Name);
|
||||||
loadPlugin.Invoke(null, null);
|
loadPlugin.Invoke(null, null);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -516,7 +782,7 @@ namespace PepperDash.Essentials
|
|||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory);
|
Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory);
|
||||||
|
|
||||||
if (Directory.Exists(_pluginDirectory))
|
if (SystemIO.Directory.Exists(_pluginDirectory))
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins");
|
Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins");
|
||||||
|
|
||||||
@@ -526,7 +792,7 @@ namespace PepperDash.Essentials
|
|||||||
// Deal with any .cplz files
|
// Deal with any .cplz files
|
||||||
UnzipAndMoveCplzArchives();
|
UnzipAndMoveCplzArchives();
|
||||||
|
|
||||||
if (Directory.Exists(_loadedPluginsDirectoryPath))
|
if (SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
|
||||||
{
|
{
|
||||||
// Load the assemblies from the loadedPlugins folder into the AppDomain
|
// Load the assemblies from the loadedPlugins folder into the AppDomain
|
||||||
LoadPluginAssemblies();
|
LoadPluginAssemblies();
|
||||||
@@ -534,9 +800,27 @@ namespace PepperDash.Essentials
|
|||||||
// Load the types from any custom plugin assemblies
|
// Load the types from any custom plugin assemblies
|
||||||
LoadCustomPluginTypes();
|
LoadCustomPluginTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report on incompatible plugins
|
||||||
|
if (IncompatiblePlugins.Count > 0)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, "Found {0} incompatible plugins:", IncompatiblePlugins.Count);
|
||||||
|
foreach (var plugin in IncompatiblePlugins)
|
||||||
|
{
|
||||||
|
if (plugin.TriggeredBy != "Direct load")
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})",
|
||||||
|
plugin.Name, plugin.Reason, plugin.TriggeredBy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}",
|
||||||
|
plugin.Name, plugin.Reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -563,4 +847,52 @@ namespace PepperDash.Essentials
|
|||||||
Assembly = assembly;
|
Assembly = assembly;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a plugin that was found to be incompatible with .NET 8
|
||||||
|
/// </summary>
|
||||||
|
public class IncompatiblePlugin
|
||||||
|
{
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string Name { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty("reason")]
|
||||||
|
public string Reason { get; private set; }
|
||||||
|
|
||||||
|
[JsonProperty("triggeredBy")]
|
||||||
|
public string TriggeredBy { get; private set; }
|
||||||
|
|
||||||
|
public IncompatiblePlugin(string name, string reason, string triggeredBy = null)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Reason = reason;
|
||||||
|
TriggeredBy = triggeredBy ?? "Direct load";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the plugin that triggered this incompatibility
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="triggeringPlugin">Name of the plugin that requires this incompatible plugin</param>
|
||||||
|
public void UpdateTriggeringPlugin(string triggeringPlugin)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(triggeringPlugin))
|
||||||
|
{
|
||||||
|
TriggeredBy = triggeringPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to explicitly mark a plugin as .NET 8 compatible
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Assembly)]
|
||||||
|
public class Net8CompatibleAttribute : Attribute
|
||||||
|
{
|
||||||
|
public bool IsCompatible { get; }
|
||||||
|
|
||||||
|
public Net8CompatibleAttribute(bool isCompatible = true)
|
||||||
|
{
|
||||||
|
IsCompatible = isCompatible;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user