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
+}