diff --git a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs index ec823007..a4e1e551 100644 --- a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using PepperDash.Core; namespace PepperDash.Essentials.Core @@ -16,8 +15,16 @@ namespace PepperDash.Essentials.Core } + /// + /// Defines a factory for creating plugin development devices, including support for specific framework versions. + /// + /// This interface extends to provide additional functionality + /// specific to plugin development environments. public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory { + /// + /// Gets a list of Essentials versions that this device is compatible with. + /// List DevelopmentEssentialsFrameworkVersions { get; } } } diff --git a/src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs b/src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs new file mode 100644 index 00000000..8cce6f62 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; + + +namespace PepperDash.Essentials; + +/// +/// Represents a plugin that is incompatible with the current system or configuration. +/// +/// This class provides details about an incompatible plugin, including its name, the reason for the +/// incompatibility, and the plugin that triggered the incompatibility. The triggering plugin can be updated dynamically +/// using the method. +/// +/// +/// +public class IncompatiblePlugin(string name, string reason, string triggeredBy = null) +{ + /// + /// Gets the name associated with the object. + /// + [JsonProperty("name")] + public string Name { get; private set; } = name; + + /// + /// Gets the reason associated with the current operation or response. + /// + [JsonProperty("reason")] + public string Reason { get; private set; } = reason; + + /// + /// Gets the identifier of the entity or process that triggered the current operation. + /// + [JsonProperty("triggeredBy")] + public string TriggeredBy { get; private set; } = triggeredBy ?? "Direct load"; + + /// + /// Updates the name of the plugin that triggered the current operation. + /// + /// The name of the triggering plugin. Must not be null or empty. If the value is null or empty, the operation is + /// ignored. + public void UpdateTriggeringPlugin(string triggeringPlugin) + { + if (!string.IsNullOrEmpty(triggeringPlugin)) + { + TriggeredBy = triggeringPlugin; + } + } +} diff --git a/src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs b/src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs new file mode 100644 index 00000000..64c639e5 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Newtonsoft.Json; + + +namespace PepperDash.Essentials; + +/// +/// Represents an assembly that has been loaded, including its name, version, and the associated instance. +/// +/// This class provides information about a loaded assembly, including its name and version as strings, +/// and the associated object. The assembly instance can be updated using the +/// method. +/// +/// +/// +public class LoadedAssembly(string name, string version, Assembly assembly) +{ + /// + /// Gets the name associated with the object. + /// + [JsonProperty("name")] + public string Name { get; private set; } = name; + + /// + /// Gets the version of the object as a string. + /// + [JsonProperty("version")] + public string Version { get; private set; } = version; + + /// + /// Gets the assembly associated with the current instance. + /// + [JsonIgnore] + public Assembly Assembly { get; private set; } = assembly; + + /// + /// Sets the assembly associated with the current instance. + /// + /// The to associate with the current instance. Cannot be . + public void SetAssembly(Assembly assembly) + { + Assembly = assembly; + } +} diff --git a/src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs b/src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs new file mode 100644 index 00000000..1a5b250a --- /dev/null +++ b/src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs @@ -0,0 +1,20 @@ +using System; + + +namespace PepperDash.Essentials; + +/// +/// Indicates whether the assembly is compatible with .NET 8. +/// +/// This attribute is used to specify compatibility with .NET 8 for an assembly. By default, the +/// assembly is considered compatible unless explicitly marked otherwise. +/// A boolean value indicating whether the assembly is compatible with .NET 8. The default value is . +[AttributeUsage(AttributeTargets.Assembly)] +public class Net8CompatibleAttribute(bool isCompatible = true) : Attribute +{ + /// + /// Gets a value indicating whether the current object is compatible with the required conditions. + /// + public bool IsCompatible { get; } = isCompatible; +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index f63ac04e..8843ba29 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; @@ -10,405 +10,556 @@ using System.Reflection.Metadata; using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -using Newtonsoft.Json; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Provides functionality for loading and managing plugins and assemblies in the application. +/// +/// The class is responsible for discovering, loading, and managing assemblies +/// and plugins, including handling compatibility checks for .NET 8. It supports loading assemblies from the program +/// directory, plugins folder, and .cplz archives. Additionally, it tracks incompatible plugins and provides reporting +/// capabilities for loaded assemblies and their versions. +public static class PluginLoader { - /// - /// Deals with loading plugins at runtime + /// + /// Gets the list of assemblies that have been loaded into the application. /// - public static class PluginLoader + public static List LoadedAssemblies { get; private set; } + + /// + /// Represents a collection of assemblies loaded from the plugin folder. + /// + /// This field is used to store assemblies that have been dynamically loaded from a designated + /// plugin folder. It is intended for internal use and should not be modified directly. + private static readonly List LoadedPluginFolderAssemblies; + + /// + /// Gets the list of plugins that are incompatible with the current system or configuration. + /// + /// This property provides information about plugins that are not supported or cannot function + /// correctly in the current environment. Use this list to identify and handle incompatible plugins appropriately in + /// your application logic. + public static List IncompatiblePlugins { get; private set; } + + /// + /// Gets the loaded assembly that contains the core functionality of the application. + /// + public static LoadedAssembly EssentialsAssembly { get; private set; } + + /// + /// Gets the loaded assembly information for the PepperDash Core library. + /// + public static LoadedAssembly PepperDashCoreAssembly { get; private set; } + + /// + /// Gets the list of assemblies that are Essentials plugins loaded by the application. + /// + public static List EssentialsPluginAssemblies { get; private set; } + + /// + /// Gets the directory path where plugins are stored. + /// + private static string PluginDirectory => Global.FilePathPrefix + "plugins"; + + /// + /// Gets the directory path where loaded plugin assemblies are stored. + /// + private static string LoadedPluginsDirectoryPath => PluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; + + /// + /// Gets the path to the temporary directory used by the plugin. + /// + private static string TempDirectory => PluginDirectory + Global.DirectorySeparator + "temp"; + + /// + /// Represents a collection of fully qualified type names that are known to be incompatible with the current + /// application or framework. + /// + /// This collection contains the names of types that are deprecated, obsolete, or otherwise + /// incompatible with the intended usage of the application. These types may represent security risks, unsupported + /// features, or legacy APIs that should be avoided. + private static readonly HashSet KnownIncompatibleTypes = + [ + "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" + ]; + + /// + /// Initializes static members of the class. + /// + /// This static constructor initializes the collections used to manage plugin assemblies and + /// track incompatible plugins. + static PluginLoader() { - /// - /// The complete list of loaded assemblies. Includes Essentials Framework assemblies and plugins - /// - public static List LoadedAssemblies { get; private set; } + LoadedAssemblies = []; + LoadedPluginFolderAssemblies = []; + EssentialsPluginAssemblies = []; + IncompatiblePlugins = []; + } - /// - /// The list of assemblies loaded from the plugins folder - /// - static List LoadedPluginFolderAssemblies; + /// + /// Loads and registers assemblies from the application's directory that match specific naming patterns. + /// + /// This method scans the application's directory for assemblies with filenames containing + /// "Essentials" or "PepperDash" and registers them in the collection. It also + /// assigns specific assemblies to predefined properties, such as and , based on their names. Debug messages are logged at various stages to provide + /// detailed information about the process, including the number of assemblies found and their versions. This + /// method is intended to be used during application initialization to ensure required assemblies are loaded and + /// tracked. + public static void AddProgramAssemblies() + { + Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); + // Get the loaded assembly filenames + var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); + var assemblyFiles = appDi.GetFiles("*.dll"); - /// - /// List of plugins that were found to be incompatible with .NET 8 - /// - public static List IncompatiblePlugins { get; private set; } + Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); - public static LoadedAssembly EssentialsAssembly { get; private set; } - - public static LoadedAssembly PepperDashCoreAssembly { get; private set; } - - public static List EssentialsPluginAssemblies { get; private set; } - - /// - /// The directory to look in for .cplz plugin packages - /// - static string _pluginDirectory => Global.FilePathPrefix + "plugins"; - - /// - /// The directory where plugins will be moved to and loaded from - /// - static string _loadedPluginsDirectoryPath => _pluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; - - // The temp directory where .cplz archives will be unzipped to - static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp"; - - // Known incompatible types in .NET 8 - private static readonly HashSet KnownIncompatibleTypes = new HashSet + foreach (var fi in assemblyFiles.Where(fi => fi.Name.Contains("Essentials") || fi.Name.Contains("PepperDash"))) { - "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" - }; + string version = string.Empty; + Assembly assembly = null; - static PluginLoader() - { - LoadedAssemblies = new List(); - LoadedPluginFolderAssemblies = new List(); - EssentialsPluginAssemblies = new List(); - IncompatiblePlugins = new List(); + switch (fi.Name) + { + case ("PepperDashEssentials.dll"): + { + version = Global.AssemblyVersion; + EssentialsAssembly = new LoadedAssembly(fi.Name, version, assembly); + break; + } + case ("PepperDash_Essentials_Core.dll"): + { + version = Global.AssemblyVersion; + break; + } + case ("Essentials Devices Common.dll"): + { + version = Global.AssemblyVersion; + break; + } + case ("PepperDashCore.dll"): + { + Debug.LogMessage(LogEventLevel.Verbose, "Found PepperDash_Core.dll"); + version = Debug.PepperDashCoreVersion; + Debug.LogMessage(LogEventLevel.Verbose, "PepperDash_Core Version: {0}", version); + PepperDashCoreAssembly = new LoadedAssembly(fi.Name, version, assembly); + break; + } + } + + LoadedAssemblies.Add(new LoadedAssembly(fi.Name, version, assembly)); } - /// - /// Retrieves all the loaded assemblies from the program directory - /// - public static void AddProgramAssemblies() + if (Debug.Level > 1) { - Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); - // Get the loaded assembly filenames - var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); - var assemblyFiles = appDi.GetFiles("*.dll"); + Debug.LogMessage(LogEventLevel.Verbose, "Loaded Assemblies:"); - Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); - - foreach (var fi in assemblyFiles.Where(fi => fi.Name.Contains("Essentials") || fi.Name.Contains("PepperDash"))) + foreach (var assembly in LoadedAssemblies) { - string version = string.Empty; - Assembly assembly = null; - - switch (fi.Name) - { - case ("PepperDashEssentials.dll"): - { - version = Global.AssemblyVersion; - EssentialsAssembly = new LoadedAssembly(fi.Name, version, assembly); - break; - } - case ("PepperDash_Essentials_Core.dll"): - { - version = Global.AssemblyVersion; - break; - } - case ("Essentials Devices Common.dll"): - { - version = Global.AssemblyVersion; - break; - } - case ("PepperDashCore.dll"): - { - Debug.LogMessage(LogEventLevel.Verbose, "Found PepperDash_Core.dll"); - version = Debug.PepperDashCoreVersion; - Debug.LogMessage(LogEventLevel.Verbose, "PepperDash_Core Version: {0}", version); - PepperDashCoreAssembly = new LoadedAssembly(fi.Name, version, assembly); - break; - } - } - - LoadedAssemblies.Add(new LoadedAssembly(fi.Name, version, assembly)); - } - - if (Debug.Level > 1) - { - Debug.LogMessage(LogEventLevel.Verbose, "Loaded Assemblies:"); - - foreach (var assembly in LoadedAssemblies) - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly: {0}", assembly.Name); - } + Debug.LogMessage(LogEventLevel.Verbose, "Assembly: {0}", assembly.Name); } } + } - public static void SetEssentialsAssembly(string name, Assembly assembly) + /// + /// Associates the specified assembly with the given name in the loaded assemblies collection. + /// + /// If an assembly with the specified name already exists in the loaded assemblies collection, + /// this method updates its associated assembly. If no matching name is found, the method does nothing. + /// The name used to identify the assembly. This value is case-sensitive and must not be null or empty. + /// The assembly to associate with the specified name. This value must not be null. + public static void SetEssentialsAssembly(string name, Assembly assembly) + { + var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); + + loadedAssembly?.SetAssembly(assembly); + } + + /// + /// Determines whether a plugin assembly is compatible with .NET 8. + /// + /// This method analyzes the provided assembly to determine compatibility with .NET 8 by checking + /// for known incompatible types, inspecting custom attributes, and collecting assembly references. If the analysis + /// encounters an error, the method returns with an appropriate error message. + /// The file path to the plugin assembly to analyze. + /// A tuple containing the following: if the plugin + /// is compatible with .NET 8; otherwise, . A + /// string providing the reason for incompatibility, or if the plugin is + /// compatible. A list of assembly references found in the + /// plugin. + public static (bool IsCompatible, string Reason, List References) IsPluginCompatibleWithNet8(string filePath) + { + try { - var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); - - if (loadedAssembly != null) - { - loadedAssembly.SetAssembly(assembly); - } + List referencedAssemblies = []; + + using FileStream fs = new(filePath, FileMode.Open, + FileAccess.Read, FileShare.ReadWrite); + using PEReader peReader = new(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); } - - /// - /// Checks if a plugin is compatible with .NET 8 by examining its metadata - /// - /// Path to the plugin assembly - /// Tuple with compatibility result, reason if incompatible, and referenced assemblies - public static (bool IsCompatible, string Reason, List References) IsPluginCompatibleWithNet8(string filePath) + catch (Exception ex) { - try - { - List referencedAssemblies = new List(); - - using (FileStream fs = new FileStream(filePath, FileMode.Open, - FileAccess.Read, 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()); - } + return (false, $"Error analyzing assembly: {ex.Message}", new List()); } + } - /// - /// Loads an assembly via Reflection and adds it to the list of loaded assemblies - /// - /// Path to the assembly file - /// Name of the plugin requesting this assembly (null for direct loads) - static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null) + /// + /// Loads an assembly from the specified file path and verifies its compatibility with .NET 8. + /// + /// This method performs a compatibility check to ensure the assembly is compatible with .NET 8 + /// before attempting to load it. If the assembly is incompatible, it is added to the list of incompatible plugins, + /// and a warning is logged. If the assembly is already loaded, the existing instance is returned instead of + /// reloading it. Exceptions during the load process are handled internally, and appropriate log messages are + /// generated for issues such as: Incompatibility with .NET + /// 8. File load conflicts (e.g., an assembly with the same name is already + /// loaded). General errors, including potential .NET Framework + /// compatibility issues. + /// The full path to the assembly file to load. This cannot be null or empty. + /// An optional identifier for the entity requesting the load operation. This can be null. + /// A object representing the loaded assembly if the operation succeeds; otherwise, + /// . + private static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null) + { + try { - try - { - // Check .NET 8 compatibility before loading - var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath); - if (!isCompatible) - { - string fileName = 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); - if (assembly != null) - { - var assyVersion = GetAssemblyVersion(assembly); - var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly); - LoadedAssemblies.Add(loadedAssembly); - Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version); - return loadedAssembly; - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath); - } - return null; - } - catch(FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) - { - // Get the assembly name from the file path - string assemblyName = 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) + // Check .NET 8 compatibility before loading + var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath); + if (!isCompatible) { string fileName = Path.GetFileName(filePath); + Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); - // 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")) + var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy); + IncompatiblePlugins.Add(incompatiblePlugin); + return null; + } + + var assembly = Assembly.LoadFrom(filePath); + if (assembly != null) + { + var assyVersion = GetAssemblyVersion(assembly); + var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly); + LoadedAssemblies.Add(loadedAssembly); + Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version); + return loadedAssembly; + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath); + } + return null; + } + catch(FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) + { + // Get the assembly name from the file path + string assemblyName = 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 = 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; + } + } + + /// + /// Retrieves the version information of the specified assembly. + /// + /// This method first attempts to retrieve the version from the . If the attribute is not present, it falls back to the assembly's + /// version as defined in its metadata. + /// The assembly from which to retrieve the version information. Cannot be . + /// A string representing the version of the assembly. If the assembly has an , its value is returned. Otherwise, the assembly's + /// version is returned in the format "Major.Minor.Build.Revision". + public static string GetAssemblyVersion(Assembly assembly) + { + var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + if (ver != null && ver.Length > 0) + { + // 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; + } + } + + /// + /// Determines whether an assembly with the specified name is currently loaded. + /// + /// This method performs a case-sensitive comparison to determine if the specified assembly is + /// loaded. It logs verbose messages indicating the status of the check. + /// The name of the assembly to check. This value is case-sensitive. + /// if an assembly with the specified name is loaded; otherwise, . + public static bool CheckIfAssemblyLoaded(string name) + { + Debug.LogMessage(LogEventLevel.Verbose, "Checking if assembly: {0} is loaded...", name); + var loadedAssembly = LoadedAssemblies.FirstOrDefault(s => s.Name.Equals(name)); + + if (loadedAssembly != null) + { + Debug.LogMessage(LogEventLevel.Verbose, "Assembly already loaded."); + return true; + } + else + { + Debug.LogMessage(LogEventLevel.Verbose, "Assembly not loaded."); + return false; + } + } + + /// + /// Reports the versions of the Essentials framework, PepperDash Core, and loaded plugins to the console. + /// + /// This method outputs version information for the Essentials framework, PepperDash Core, and + /// all loaded Essentials plugins to the Crestron console. If any incompatible plugins are detected, their details + /// are also reported, including the reason for incompatibility and the plugin that required them, if + /// applicable. + /// The command string that triggered the version report. This parameter is not used directly by the method. + public static void ReportAssemblyVersions(string command) + { + CrestronConsole.ConsoleCommandResponse("Essentials Version: {0}" + CrestronEnvironment.NewLine, Global.AssemblyVersion); + CrestronConsole.ConsoleCommandResponse("PepperDash Core Version: {0}" + CrestronEnvironment.NewLine, PepperDashCoreAssembly.Version); + CrestronConsole.ConsoleCommandResponse("Essentials Plugin Versions:" + CrestronEnvironment.NewLine); + foreach (var assembly in EssentialsPluginAssemblies) + { + CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version); + } + + if (IncompatiblePlugins.Count > 0) + { + CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine); + foreach (var plugin in IncompatiblePlugins) + { + if (plugin.TriggeredBy != "Direct load") { - 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)); + CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine, + plugin.Name, plugin.Reason, plugin.TriggeredBy); } else { - Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath); + CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine, + plugin.Name, plugin.Reason); } - return null; + } + } + } + + /// + /// Moves .dll assemblies from the plugins folder to the loaded plugins directory. + /// + /// This method scans the plugins folder for .dll files and moves them to the loaded plugins + /// directory if they are not already loaded. If a file with the same name exists in the target directory, it is + /// replaced. The method logs the process at various stages and handles exceptions for individual files to ensure + /// the operation continues for other files. + private static void MoveDllAssemblies() + { + Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); + + var pluginDi = new DirectoryInfo(PluginDirectory); + var pluginFiles = pluginDi.GetFiles("*.dll"); + + if (pluginFiles.Length > 0) + { + if (!Directory.Exists(LoadedPluginsDirectoryPath)) + { + Directory.CreateDirectory(LoadedPluginsDirectoryPath); } } - /// - /// Attempts to get the assembly informational version and if not possible gets the version - /// - /// - /// - public static string GetAssemblyVersion(Assembly assembly) + foreach (var pluginFile in pluginFiles) { - var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); - if (ver != null && ver.Length > 0) + try { - // 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; - } - } + Debug.LogMessage(LogEventLevel.Information, "Found .dll: {0}", pluginFile.Name); - /// - /// Checks if the filename matches an already loaded assembly file's name - /// - /// - /// True if file already matches loaded assembly file. - public static bool CheckIfAssemblyLoaded(string name) - { - Debug.LogMessage(LogEventLevel.Verbose, "Checking if assembly: {0} is loaded...", name); - var loadedAssembly = LoadedAssemblies.FirstOrDefault(s => s.Name.Equals(name)); - - if (loadedAssembly != null) - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly already loaded."); - return true; - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly not loaded."); - return false; - } - } - - /// - /// Used by console command to report the currently loaded assemblies and versions - /// - /// - public static void ReportAssemblyVersions(string command) - { - CrestronConsole.ConsoleCommandResponse("Essentials Version: {0}" + CrestronEnvironment.NewLine, Global.AssemblyVersion); - CrestronConsole.ConsoleCommandResponse("PepperDash Core Version: {0}" + CrestronEnvironment.NewLine, PepperDashCoreAssembly.Version); - CrestronConsole.ConsoleCommandResponse("Essentials Plugin Versions:" + CrestronEnvironment.NewLine); - foreach (var assembly in EssentialsPluginAssemblies) - { - CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version); - } - - if (IncompatiblePlugins.Count > 0) - { - CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine); - foreach (var plugin in IncompatiblePlugins) + if (!CheckIfAssemblyLoaded(pluginFile.Name)) { - if (plugin.TriggeredBy != "Direct load") + string filePath = string.Empty; + + filePath = LoadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; + + // Check if there is a previous file in the loadedPlugins directory and delete + if (File.Exists(filePath)) { - 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); + Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); + File.Delete(filePath); } + + // Move the file + File.Move(pluginFile.FullName, filePath); + Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath); } + else + { + Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", pluginFile.FullName); + } + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, "Error with plugin file {0} . Exception: {1}", pluginFile.FullName, e); + continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here } } - - /// - /// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder - /// - static void MoveDllAssemblies() + + Debug.LogMessage(LogEventLevel.Information, "Done with .dll assemblies"); + } + + /// + /// Extracts and processes .cplz archive files found in the specified directories, moving their contents to the + /// appropriate plugin directory. + /// + /// This method searches for .cplz files in the plugin directory and user folder, extracts their + /// contents, and moves any .dll files to the loaded plugins directory. If a .dll file with the same name already + /// exists in the target directory, it is replaced with the new file. Temporary files and directories created during + /// the process are cleaned up after the operation completes. + private static void UnzipAndMoveCplzArchives() + { + Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); + var di = new DirectoryInfo(PluginDirectory); + var zFiles = di.GetFiles("*.cplz"); + + //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users + + //var userDi = new DirectoryInfo(Global.FilePathPrefix); + //var userZFiles = userDi.GetFiles("*.cplz"); + + Debug.LogInformation("Checking {folder} for .cplz files", Global.FilePathPrefix); + var cplzFiles = Directory.GetFiles(Global.FilePathPrefix, "*.cplz", SearchOption.AllDirectories) + .Select(f => new FileInfo(f)) + .ToArray(); + + if (cplzFiles.Length > 0) { - Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); - - var pluginDi = new DirectoryInfo(_pluginDirectory); - var pluginFiles = pluginDi.GetFiles("*.dll"); - - if (pluginFiles.Length > 0) + if (!Directory.Exists(LoadedPluginsDirectoryPath)) { - if (!Directory.Exists(_loadedPluginsDirectoryPath)) - { - Directory.CreateDirectory(_loadedPluginsDirectoryPath); - } + Directory.CreateDirectory(LoadedPluginsDirectoryPath); } + } - foreach (var pluginFile in pluginFiles) + foreach (var zfi in cplzFiles) + { + Directory.CreateDirectory(TempDirectory); + var tempDi = new DirectoryInfo(TempDirectory); + + Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); + var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); + Debug.LogMessage(LogEventLevel.Information, "UnZip Result: {0}", result.ToString()); + + var tempFiles = tempDi.GetFiles("*.dll"); + foreach (var tempFile in tempFiles) { try { - Debug.LogMessage(LogEventLevel.Information, "Found .dll: {0}", pluginFile.Name); - - if (!CheckIfAssemblyLoaded(pluginFile.Name)) + if (!CheckIfAssemblyLoaded(tempFile.Name)) { string filePath = string.Empty; - filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; + filePath = LoadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; // Check if there is a previous file in the loadedPlugins directory and delete if (File.Exists(filePath)) @@ -418,493 +569,325 @@ namespace PepperDash.Essentials } // Move the file - File.Move(pluginFile.FullName, filePath); - Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath); + File.Move(tempFile.FullName, filePath); + Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath); } else { - Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", pluginFile.FullName); + Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", tempFile.FullName); } } catch (Exception e) { - Debug.LogMessage(LogEventLevel.Verbose, "Error with plugin file {0} . Exception: {1}", pluginFile.FullName, e); + Debug.LogMessage(LogEventLevel.Verbose, "Assembly {0} is not a custom assembly. Exception: {1}", tempFile.FullName, e); continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here } } - Debug.LogMessage(LogEventLevel.Information, "Done with .dll assemblies"); + // Delete the .cplz and the temp directory + Directory.Delete(TempDirectory, true); + zfi.Delete(); } - /// - /// Unzips each .cplz archive into the temp directory and moves any unloaded files into loadedPlugins - /// - static void UnzipAndMoveCplzArchives() + Debug.LogMessage(LogEventLevel.Information, "Done with .cplz archives"); + } + + /// + /// Loads plugin assemblies from the designated plugin directory, checks their compatibility with .NET 8, and loads + /// the compatible assemblies into the application. + /// + /// This method scans the plugin directory for all `.dll` files, verifies their compatibility + /// with .NET 8, and attempts to load the compatible assemblies. Assemblies that are incompatible are logged as + /// warnings and added to a list of incompatible plugins for further inspection. + private static void LoadPluginAssemblies() + { + Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); + var pluginDi = new DirectoryInfo(LoadedPluginsDirectoryPath); + var pluginFiles = pluginDi.GetFiles("*.dll"); + + 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 References)>(); + + foreach (var pluginFile in pluginFiles) { - Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); - var di = new DirectoryInfo(_pluginDirectory); - var zFiles = di.GetFiles("*.cplz"); - - //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users - - //var userDi = new DirectoryInfo(Global.FilePathPrefix); - //var userZFiles = userDi.GetFiles("*.cplz"); - - Debug.LogInformation("Checking {folder} for .cplz files", Global.FilePathPrefix); - var cplzFiles = Directory.GetFiles(Global.FilePathPrefix, "*.cplz", SearchOption.AllDirectories) - .Select(f => new FileInfo(f)) - .ToArray(); - - if (cplzFiles.Length > 0) + string fileName = pluginFile.Name; + assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName); + } + + // Now load compatible assemblies and track incompatible ones + foreach (var pluginFile in pluginFiles) + { + string fileName = pluginFile.Name; + var (isCompatible, reason, _) = assemblyCompatibility[fileName]; + + if (!isCompatible) { - if (!Directory.Exists(_loadedPluginsDirectoryPath)) - { - Directory.CreateDirectory(_loadedPluginsDirectoryPath); - } + Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); + IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null)); + continue; } - - foreach (var zfi in cplzFiles) + + // Try to load the assembly + var loadedAssembly = LoadAssembly(pluginFile.FullName, null); + + if (loadedAssembly != null) { - Directory.CreateDirectory(_tempDirectory); - var tempDi = new DirectoryInfo(_tempDirectory); - - Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); - var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); - Debug.LogMessage(LogEventLevel.Information, "UnZip Result: {0}", result.ToString()); - - var tempFiles = tempDi.GetFiles("*.dll"); - foreach (var tempFile in tempFiles) - { - try - { - if (!CheckIfAssemblyLoaded(tempFile.Name)) - { - string filePath = string.Empty; - - filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; - - // Check if there is a previous file in the loadedPlugins directory and delete - if (File.Exists(filePath)) - { - Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); - File.Delete(filePath); - } - - // Move the file - File.Move(tempFile.FullName, filePath); - Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", tempFile.FullName); - } - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly {0} is not a custom assembly. Exception: {1}", tempFile.FullName, e); - 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 - Directory.Delete(_tempDirectory, true); - zfi.Delete(); + LoadedPluginFolderAssemblies.Add(loadedAssembly); } - - Debug.LogMessage(LogEventLevel.Information, "Done with .cplz archives"); } - /// - /// Attempts to load the assemblies from the loadedPlugins folder - /// - static void LoadPluginAssemblies() + Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded."); + } + + /// + /// Loads and initializes custom plugin types from the assemblies in the plugin folder. + /// + /// This method iterates through all loaded plugin assemblies, identifies types that implement + /// the interface, and attempts to instantiate and load them. Assemblies or + /// types that cannot be loaded due to missing dependencies, type loading errors, or other exceptions are logged, + /// and incompatible plugins are tracked for further analysis. + private static void LoadCustomPluginTypes() + { + Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); + + foreach (var loadedAssembly in LoadedPluginFolderAssemblies) { - Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); - var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath); - var pluginFiles = pluginDi.GetFiles("*.dll"); - - 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 References)>(); + // Skip if assembly is null (can happen if we had loading issues) + if (loadedAssembly == null || loadedAssembly.Assembly == null) + continue; - foreach (var pluginFile in pluginFiles) + // iteratate this assembly's classes, looking for "LoadPlugin()" methods + try { - string fileName = pluginFile.Name; - assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName); - } - - // 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."); - } - - /// - /// Iterate the loaded assemblies and try to call the LoadPlugin method - /// - static void LoadCustomPluginTypes() - { - Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); - - 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 + var assy = loadedAssembly.Assembly; + Type[] types = []; try { - var assy = loadedAssembly.Assembly; - Type[] types = {}; - try - { - types = assy.GetTypes(); - 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 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( - Path.GetFileName(missingAssembly), - $"Missing dependency required by {loadedAssembly.Name}", - loadedAssembly.Name)); - } - } - } - - Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); - continue; - } - catch (TypeLoadException e) - { - Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", - loadedAssembly.Name, e.Message); - 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; - } - - foreach (var type in types) - { - try - { - if (typeof (IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) - { - var plugin = - (IPluginDeviceFactory)Activator.CreateInstance(type); - LoadCustomPlugin(plugin, loadedAssembly); - } - } - catch (NotSupportedException) - { - //this happens for dlls that aren't PD dlls, like ports of Mono classes into S#. Swallowing. - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Error, "Load Plugin not found. {0}.{2} is not a plugin factory. Exception: {1}", - loadedAssembly.Name, e.Message, type.Name); - continue; - } - } + types = assy.GetTypes(); + Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}"); } - catch (Exception e) + catch (ReflectionTypeLoadException e) { - Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}", + Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", loadedAssembly.Name, e.Message); - Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace); + + // Check if any of the loader exceptions are due to missing assemblies + foreach (var loaderEx in e.LoaderExceptions) + { + if (loaderEx is 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( + Path.GetFileName(missingAssembly), + $"Missing dependency required by {loadedAssembly.Name}", + loadedAssembly.Name)); + } + } + } + + Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); + continue; + } + catch (TypeLoadException e) + { + Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", + loadedAssembly.Name, e.Message); + 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, - $"Assembly loading error: {e.Message}", + $"Type loading error: {e.Message}", null)); } continue; } - } - - // Update incompatible plugins with dependency information - var pluginDependencies = new Dictionary>(); - // Populate pluginDependencies with relevant data - // Example: pluginDependencies["PluginA"] = new List { "Dependency1", "Dependency2" }; - UpdateIncompatiblePluginDependencies(pluginDependencies); - - // plugin dll will be loaded. Any classes in plugin should have a static constructor - // that registers that class with the Core.DeviceFactory - Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types."); - } - /// - /// Updates incompatible plugins with information about which plugins depend on them - /// - private static void UpdateIncompatiblePluginDependencies(Dictionary> 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) + foreach (var type in types) { - string pluginName = plugin.Key; - List dependencies = plugin.Value; - - // If this plugin depends on the incompatible plugin - if (dependencies.Contains(incompatiblePlugin.Name) || - dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ","))) + try + { + if (typeof (IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) + { + var plugin = + (IPluginDeviceFactory)Activator.CreateInstance(type); + LoadCustomPlugin(plugin, loadedAssembly); + } + } + catch (NotSupportedException) { - incompatiblePlugin.UpdateTriggeringPlugin(pluginName); - break; + //this happens for dlls that aren't PD dlls, like ports of Mono classes into S#. Swallowing. + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Error, "Load Plugin not found. {0}.{2} is not a plugin factory. Exception: {1}", + loadedAssembly.Name, e.Message, type.Name); + continue; } } } - } - /// - /// Loads a - /// - /// - /// - static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly) - { - var developmentPlugin = plugin as IPluginDevelopmentDeviceFactory; - - var passed = developmentPlugin != null ? Global.IsRunningDevelopmentVersion - (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) - : Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion); - - if (!passed) + catch (Exception e) { - Debug.LogMessage(LogEventLevel.Information, - "\r\n********************\r\n\tPlugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin {1}\r\n********************", - plugin.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); - return; - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", plugin.MinimumEssentialsFrameworkVersion); - } - - Debug.LogMessage(LogEventLevel.Information, "Loading plugin: {0}", loadedAssembly.Name); - plugin.LoadTypeFactories(); - - if(!EssentialsPluginAssemblies.Contains(loadedAssembly)) - EssentialsPluginAssemblies.Add(loadedAssembly); - } - - /// - /// Loads a a custom plugin via the legacy method - /// - /// - /// - static void LoadCustomLegacyPlugin(Type type, MethodInfo loadPlugin, LoadedAssembly loadedAssembly) - { - Debug.LogMessage(LogEventLevel.Verbose, "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.LogMessage(LogEventLevel.Verbose, "MinimumEssentialsFrameworkVersion found"); - - var minimumVersionString = minimumVersion.GetValue(null) as string; - - if (!string.IsNullOrEmpty(minimumVersionString)) + Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}", + loadedAssembly.Name, e.Message); + 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")) { - var passed = Global.IsRunningMinimumVersionOrHigher(minimumVersionString); + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Assembly loading error: {e.Message}", + null)); + } + + continue; + } + } + + // Update incompatible plugins with dependency information + var pluginDependencies = new Dictionary>(); + // Populate pluginDependencies with relevant data + // Example: pluginDependencies["PluginA"] = new List { "Dependency1", "Dependency2" }; + UpdateIncompatiblePluginDependencies(pluginDependencies); + + // plugin dll will be loaded. Any classes in plugin should have a static constructor + // that registers that class with the Core.DeviceFactory + Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types."); + } - if (!passed) + /// + /// Updates the triggering plugin information for incompatible plugins based on their dependencies. + /// + /// This method iterates through a predefined list of incompatible plugins and updates their + /// triggering plugin information if they were directly loaded and are found to be dependencies of other plugins. + /// The update is performed for the first plugin that depends on the incompatible plugin. + /// A dictionary where the key is the name of a plugin and the value is a list of its dependencies. Each dependency + /// is represented as a string, which may include additional metadata. + private static void UpdateIncompatiblePluginDependencies(Dictionary> 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 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; + } + } + } + } + + /// + /// Loads a custom plugin and performs a dependency check to ensure compatibility with the required Essentials + /// framework version. + /// + /// This method verifies that the plugin meets the minimum required Essentials framework version + /// before loading it. If the plugin fails the dependency check, it is skipped, and a log message is generated. If + /// the plugin passes the check, it is loaded, and its type factories are initialized. + /// The plugin to be loaded, implementing the interface. If the plugin also + /// implements , additional checks for development versions are + /// performed. + /// The assembly associated with the plugin being loaded. This is used for logging and tracking purposes. + private static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly) + { + var passed = plugin is IPluginDevelopmentDeviceFactory developmentPlugin ? Global.IsRunningDevelopmentVersion + (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) + : Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion); + + if (!passed) + { + Debug.LogMessage(LogEventLevel.Information, + "\r\n********************\r\n\tPlugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin {1}\r\n********************", + plugin.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); + return; + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", plugin.MinimumEssentialsFrameworkVersion); + } + + Debug.LogMessage(LogEventLevel.Information, "Loading plugin: {0}", loadedAssembly.Name); + plugin.LoadTypeFactories(); + + if(!EssentialsPluginAssemblies.Contains(loadedAssembly)) + EssentialsPluginAssemblies.Add(loadedAssembly); + } + + /// + /// Loads plugins from the designated plugin directory, processes them, and integrates them into the application. + /// + /// This method performs the following steps: Checks if + /// the plugin directory exists. Processes any plugin files, including .dll + /// and .cplz files, by moving or extracting them as needed. Loads + /// assemblies from the processed plugins into the application domain. + /// Identifies and reports any incompatible plugins, including the reason for + /// incompatibility. Plugins that are successfully loaded are made available for use, + /// while incompatible plugins are logged for review. + public static void LoadPlugins() + { + Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", PluginDirectory); + + if (Directory.Exists(PluginDirectory)) + { + Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); + + // Deal with any .dll files + MoveDllAssemblies(); + + // Deal with any .cplz files + UnzipAndMoveCplzArchives(); + + if (Directory.Exists(LoadedPluginsDirectoryPath)) + { + // Load the assemblies from the loadedPlugins folder into the AppDomain + LoadPluginAssemblies(); + + // Load the types from any custom plugin assemblies + 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.Information, "Plugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin", minimumVersionString); - return; + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})", + plugin.Name, plugin.Reason, plugin.TriggeredBy); } else { - Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", minimumVersionString); - } - } - else - { - Debug.LogMessage(LogEventLevel.Information, "MinimumEssentialsFrameworkVersion found but not set. Loading plugin, but your mileage may vary."); - } - } - else - { - Debug.LogMessage(LogEventLevel.Information, "MinimumEssentialsFrameworkVersion not found. Loading plugin, but your mileage may vary."); - } - - Debug.LogMessage(LogEventLevel.Information, "Loading legacy plugin: {0}", loadedAssembly.Name); - loadPlugin.Invoke(null, null); - } - - /// - /// Loads plugins - /// - public static void LoadPlugins() - { - Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory); - - if (Directory.Exists(_pluginDirectory)) - { - Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); - - // Deal with any .dll files - MoveDllAssemblies(); - - // Deal with any .cplz files - UnzipAndMoveCplzArchives(); - - if (Directory.Exists(_loadedPluginsDirectoryPath)) - { - // Load the assemblies from the loadedPlugins folder into the AppDomain - LoadPluginAssemblies(); - - // Load the types from any custom plugin assemblies - 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); - } + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}", + plugin.Name, plugin.Reason); } } } } } - - /// - /// Represents an assembly loaded at runtime and it's associated metadata - /// - public class LoadedAssembly - { - [JsonProperty("name")] - public string Name { get; private set; } - [JsonProperty("version")] - public string Version { get; private set; } - [JsonIgnore] - public Assembly Assembly { get; private set; } - - public LoadedAssembly(string name, string version, Assembly assembly) - { - Name = name; - Version = version; - Assembly = assembly; - } - - public void SetAssembly(Assembly assembly) - { - Assembly = assembly; - } - } - - /// - /// Represents a plugin that was found to be incompatible with .NET 8 - /// - 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"; - } - - /// - /// Updates the plugin that triggered this incompatibility - /// - /// Name of the plugin that requires this incompatible plugin - public void UpdateTriggeringPlugin(string triggeringPlugin) - { - if (!string.IsNullOrEmpty(triggeringPlugin)) - { - TriggeredBy = triggeringPlugin; - } - } - } - - /// - /// Attribute to explicitly mark a plugin as .NET 8 compatible - /// - [AttributeUsage(AttributeTargets.Assembly)] - public class Net8CompatibleAttribute : Attribute - { - public bool IsCompatible { get; } - - public Net8CompatibleAttribute(bool isCompatible = true) - { - IsCompatible = isCompatible; - } - } -} \ No newline at end of file +}