docs: update XML docs

This commit is contained in:
Andrew Welker
2025-07-04 13:07:21 -05:00
parent cc7e2ab675
commit 58a2a5c008
5 changed files with 883 additions and 780 deletions

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using PepperDash.Core;
namespace PepperDash.Essentials.Core namespace PepperDash.Essentials.Core
@@ -16,8 +15,16 @@ namespace PepperDash.Essentials.Core
} }
/// <summary>
/// Defines a factory for creating plugin development devices, including support for specific framework versions.
/// </summary>
/// <remarks>This interface extends <see cref="IPluginDeviceFactory"/> to provide additional functionality
/// specific to plugin development environments.</remarks>
public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory
{ {
/// <summary>
/// Gets a list of Essentials versions that this device is compatible with.
/// </summary>
List<string> DevelopmentEssentialsFrameworkVersions { get; } List<string> DevelopmentEssentialsFrameworkVersions { get; }
} }
} }

View File

@@ -0,0 +1,47 @@
using Newtonsoft.Json;
namespace PepperDash.Essentials;
/// <summary>
/// Represents a plugin that is incompatible with the current system or configuration.
/// </summary>
/// <remarks>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 <see cref="UpdateTriggeringPlugin(string)"/> method.</remarks>
/// <param name="name"></param>
/// <param name="reason"></param>
/// <param name="triggeredBy"></param>
public class IncompatiblePlugin(string name, string reason, string triggeredBy = null)
{
/// <summary>
/// Gets the name associated with the object.
/// </summary>
[JsonProperty("name")]
public string Name { get; private set; } = name;
/// <summary>
/// Gets the reason associated with the current operation or response.
/// </summary>
[JsonProperty("reason")]
public string Reason { get; private set; } = reason;
/// <summary>
/// Gets the identifier of the entity or process that triggered the current operation.
/// </summary>
[JsonProperty("triggeredBy")]
public string TriggeredBy { get; private set; } = triggeredBy ?? "Direct load";
/// <summary>
/// Updates the name of the plugin that triggered the current operation.
/// </summary>
/// <param name="triggeringPlugin">The name of the triggering plugin. Must not be null or empty. If the value is null or empty, the operation is
/// ignored.</param>
public void UpdateTriggeringPlugin(string triggeringPlugin)
{
if (!string.IsNullOrEmpty(triggeringPlugin))
{
TriggeredBy = triggeringPlugin;
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Reflection;
using Newtonsoft.Json;
namespace PepperDash.Essentials;
/// <summary>
/// Represents an assembly that has been loaded, including its name, version, and the associated <see
/// cref="System.Reflection.Assembly"/> instance.
/// </summary>
/// <remarks>This class provides information about a loaded assembly, including its name and version as strings,
/// and the associated <see cref="System.Reflection.Assembly"/> object. The assembly instance can be updated using the
/// <see cref="SetAssembly(Assembly)"/> method.</remarks>
/// <param name="name"></param>
/// <param name="version"></param>
/// <param name="assembly"></param>
public class LoadedAssembly(string name, string version, Assembly assembly)
{
/// <summary>
/// Gets the name associated with the object.
/// </summary>
[JsonProperty("name")]
public string Name { get; private set; } = name;
/// <summary>
/// Gets the version of the object as a string.
/// </summary>
[JsonProperty("version")]
public string Version { get; private set; } = version;
/// <summary>
/// Gets the assembly associated with the current instance.
/// </summary>
[JsonIgnore]
public Assembly Assembly { get; private set; } = assembly;
/// <summary>
/// Sets the assembly associated with the current instance.
/// </summary>
/// <param name="assembly">The <see cref="System.Reflection.Assembly"/> to associate with the current instance. Cannot be <see
/// langword="null"/>.</param>
public void SetAssembly(Assembly assembly)
{
Assembly = assembly;
}
}

View File

@@ -0,0 +1,20 @@
using System;
namespace PepperDash.Essentials;
/// <summary>
/// Indicates whether the assembly is compatible with .NET 8.
/// </summary>
/// <remarks>This attribute is used to specify compatibility with .NET 8 for an assembly. By default, the
/// assembly is considered compatible unless explicitly marked otherwise.</remarks>
/// <param name="isCompatible">A boolean value indicating whether the assembly is compatible with .NET 8. The default value is <see
/// langword="true"/>.</param>
[AttributeUsage(AttributeTargets.Assembly)]
public class Net8CompatibleAttribute(bool isCompatible = true) : Attribute
{
/// <summary>
/// Gets a value indicating whether the current object is compatible with the required conditions.
/// </summary>
public bool IsCompatible { get; } = isCompatible;
}

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Crestron.SimplSharp; using Crestron.SimplSharp;
@@ -10,53 +10,78 @@ using System.Reflection.Metadata;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Essentials.Core; using PepperDash.Essentials.Core;
using Serilog.Events; using Serilog.Events;
using Newtonsoft.Json;
namespace PepperDash.Essentials namespace PepperDash.Essentials;
/// <summary>
/// Provides functionality for loading and managing plugins and assemblies in the application.
/// </summary>
/// <remarks>The <see cref="PluginLoader"/> 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.</remarks>
public static class PluginLoader
{ {
/// <summary> /// <summary>
/// Deals with loading plugins at runtime /// Gets the list of assemblies that have been loaded into the application.
/// </summary>
public static class PluginLoader
{
/// <summary>
/// The complete list of loaded assemblies. Includes Essentials Framework assemblies and plugins
/// </summary> /// </summary>
public static List<LoadedAssembly> LoadedAssemblies { get; private set; } public static List<LoadedAssembly> LoadedAssemblies { get; private set; }
/// <summary> /// <summary>
/// The list of assemblies loaded from the plugins folder /// Represents a collection of assemblies loaded from the plugin folder.
/// </summary> /// </summary>
static List<LoadedAssembly> LoadedPluginFolderAssemblies; /// <remarks>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.</remarks>
private static readonly List<LoadedAssembly> LoadedPluginFolderAssemblies;
/// <summary> /// <summary>
/// List of plugins that were found to be incompatible with .NET 8 /// Gets the list of plugins that are incompatible with the current system or configuration.
/// </summary> /// </summary>
/// <remarks>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.</remarks>
public static List<IncompatiblePlugin> IncompatiblePlugins { get; private set; } public static List<IncompatiblePlugin> IncompatiblePlugins { get; private set; }
/// <summary>
/// Gets the loaded assembly that contains the core functionality of the application.
/// </summary>
public static LoadedAssembly EssentialsAssembly { get; private set; } public static LoadedAssembly EssentialsAssembly { get; private set; }
/// <summary>
/// Gets the loaded assembly information for the PepperDash Core library.
/// </summary>
public static LoadedAssembly PepperDashCoreAssembly { get; private set; } public static LoadedAssembly PepperDashCoreAssembly { get; private set; }
/// <summary>
/// Gets the list of assemblies that are Essentials plugins loaded by the application.
/// </summary>
public static List<LoadedAssembly> EssentialsPluginAssemblies { get; private set; } public static List<LoadedAssembly> EssentialsPluginAssemblies { get; private set; }
/// <summary> /// <summary>
/// The directory to look in for .cplz plugin packages /// Gets the directory path where plugins are stored.
/// </summary> /// </summary>
static string _pluginDirectory => Global.FilePathPrefix + "plugins"; private static string PluginDirectory => Global.FilePathPrefix + "plugins";
/// <summary> /// <summary>
/// The directory where plugins will be moved to and loaded from /// Gets the directory path where loaded plugin assemblies are stored.
/// </summary> /// </summary>
static string _loadedPluginsDirectoryPath => _pluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; private static string LoadedPluginsDirectoryPath => PluginDirectory + Global.DirectorySeparator + "loadedAssemblies";
// The temp directory where .cplz archives will be unzipped to /// <summary>
static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp"; /// Gets the path to the temporary directory used by the plugin.
/// </summary>
private static string TempDirectory => PluginDirectory + Global.DirectorySeparator + "temp";
// Known incompatible types in .NET 8 /// <summary>
private static readonly HashSet<string> KnownIncompatibleTypes = new HashSet<string> /// Represents a collection of fully qualified type names that are known to be incompatible with the current
{ /// application or framework.
/// </summary>
/// <remarks>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.</remarks>
private static readonly HashSet<string> KnownIncompatibleTypes =
[
"System.Net.ICertificatePolicy", "System.Net.ICertificatePolicy",
"System.Security.Cryptography.SHA1CryptoServiceProvider", "System.Security.Cryptography.SHA1CryptoServiceProvider",
"System.Web.HttpUtility", "System.Web.HttpUtility",
@@ -68,19 +93,31 @@ namespace PepperDash.Essentials
"System.Security.SecurityManager", "System.Security.SecurityManager",
"System.Security.Permissions.FileIOPermission", "System.Security.Permissions.FileIOPermission",
"System.AppDomain.CreateDomain" "System.AppDomain.CreateDomain"
}; ];
/// <summary>
/// Initializes static members of the <see cref="PluginLoader"/> class.
/// </summary>
/// <remarks>This static constructor initializes the collections used to manage plugin assemblies and
/// track incompatible plugins.</remarks>
static PluginLoader() static PluginLoader()
{ {
LoadedAssemblies = new List<LoadedAssembly>(); LoadedAssemblies = [];
LoadedPluginFolderAssemblies = new List<LoadedAssembly>(); LoadedPluginFolderAssemblies = [];
EssentialsPluginAssemblies = new List<LoadedAssembly>(); EssentialsPluginAssemblies = [];
IncompatiblePlugins = new List<IncompatiblePlugin>(); IncompatiblePlugins = [];
} }
/// <summary> /// <summary>
/// Retrieves all the loaded assemblies from the program directory /// Loads and registers assemblies from the application's directory that match specific naming patterns.
/// </summary> /// </summary>
/// <remarks>This method scans the application's directory for assemblies with filenames containing
/// "Essentials" or "PepperDash" and registers them in the <see cref="LoadedAssemblies"/> collection. It also
/// assigns specific assemblies to predefined properties, such as <see cref="EssentialsAssembly"/> and <see
/// cref="PepperDashCoreAssembly"/>, 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.</remarks>
public static void AddProgramAssemblies() public static void AddProgramAssemblies()
{ {
Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials");
@@ -137,31 +174,42 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Associates the specified assembly with the given name in the loaded assemblies collection.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="name">The name used to identify the assembly. This value is case-sensitive and must not be null or empty.</param>
/// <param name="assembly">The assembly to associate with the specified name. This value must not be null.</param>
public static void SetEssentialsAssembly(string name, Assembly assembly) public static void SetEssentialsAssembly(string name, Assembly assembly)
{ {
var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name));
if (loadedAssembly != null) loadedAssembly?.SetAssembly(assembly);
{
loadedAssembly.SetAssembly(assembly);
}
} }
/// <summary> /// <summary>
/// Checks if a plugin is compatible with .NET 8 by examining its metadata /// Determines whether a plugin assembly is compatible with .NET 8.
/// </summary> /// </summary>
/// <param name="filePath">Path to the plugin assembly</param> /// <remarks>This method analyzes the provided assembly to determine compatibility with .NET 8 by checking
/// <returns>Tuple with compatibility result, reason if incompatible, and referenced assemblies</returns> /// for known incompatible types, inspecting custom attributes, and collecting assembly references. If the analysis
/// encounters an error, the method returns <see langword="false"/> with an appropriate error message.</remarks>
/// <param name="filePath">The file path to the plugin assembly to analyze.</param>
/// <returns>A tuple containing the following: <list type="bullet"> <item> <description><see langword="true"/> if the plugin
/// is compatible with .NET 8; otherwise, <see langword="false"/>.</description> </item> <item> <description>A
/// string providing the reason for incompatibility, or <see langword="null"/> if the plugin is
/// compatible.</description> </item> <item> <description>A list of assembly references found in the
/// plugin.</description> </item> </list></returns>
public static (bool IsCompatible, string Reason, List<string> References) IsPluginCompatibleWithNet8(string filePath) public static (bool IsCompatible, string Reason, List<string> References) IsPluginCompatibleWithNet8(string filePath)
{ {
try try
{ {
List<string> referencedAssemblies = new List<string>(); List<string> referencedAssemblies = [];
using FileStream fs = new(filePath, FileMode.Open,
FileAccess.Read, FileShare.ReadWrite);
using PEReader peReader = new(fs);
using (FileStream fs = new FileStream(filePath, FileMode.Open,
FileAccess.Read, FileShare.ReadWrite))
using (PEReader peReader = new PEReader(fs))
{
if (!peReader.HasMetadata) if (!peReader.HasMetadata)
return (false, "Not a valid .NET assembly", referencedAssemblies); return (false, "Not a valid .NET assembly", referencedAssemblies);
@@ -218,7 +266,6 @@ namespace PepperDash.Essentials
// If we can't determine incompatibility, assume it's compatible // If we can't determine incompatibility, assume it's compatible
return (true, null, referencedAssemblies); return (true, null, referencedAssemblies);
} }
}
catch (Exception ex) catch (Exception ex)
{ {
return (false, $"Error analyzing assembly: {ex.Message}", new List<string>()); return (false, $"Error analyzing assembly: {ex.Message}", new List<string>());
@@ -226,11 +273,21 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies /// Loads an assembly from the specified file path and verifies its compatibility with .NET 8.
/// </summary> /// </summary>
/// <param name="filePath">Path to the assembly file</param> /// <remarks>This method performs a compatibility check to ensure the assembly is compatible with .NET 8
/// <param name="requestedBy">Name of the plugin requesting this assembly (null for direct loads)</param> /// before attempting to load it. If the assembly is incompatible, it is added to the list of incompatible plugins,
static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null) /// 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: <list type="bullet"> <item><description>Incompatibility with .NET
/// 8.</description></item> <item><description>File load conflicts (e.g., an assembly with the same name is already
/// loaded).</description></item> <item><description>General errors, including potential .NET Framework
/// compatibility issues.</description></item> </list></remarks>
/// <param name="filePath">The full path to the assembly file to load. This cannot be null or empty.</param>
/// <param name="requestedBy">An optional identifier for the entity requesting the load operation. This can be null.</param>
/// <returns>A <see cref="LoadedAssembly"/> object representing the loaded assembly if the operation succeeds; otherwise,
/// <see langword="null"/>.</returns>
private static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null)
{ {
try try
{ {
@@ -303,10 +360,16 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Attempts to get the assembly informational version and if not possible gets the version /// Retrieves the version information of the specified assembly.
/// </summary> /// </summary>
/// <param name="assembly"></param> /// <remarks>This method first attempts to retrieve the version from the <see
/// <returns></returns> /// cref="AssemblyInformationalVersionAttribute"/>. If the attribute is not present, it falls back to the assembly's
/// version as defined in its metadata.</remarks>
/// <param name="assembly">The assembly from which to retrieve the version information. Cannot be <see langword="null"/>.</param>
/// <returns>A string representing the version of the assembly. If the assembly has an <see
/// cref="AssemblyInformationalVersionAttribute"/>, its <see
/// cref="AssemblyInformationalVersionAttribute.InformationalVersion"/> value is returned. Otherwise, the assembly's
/// version is returned in the format "Major.Minor.Build.Revision".</returns>
public static string GetAssemblyVersion(Assembly assembly) public static string GetAssemblyVersion(Assembly assembly)
{ {
var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
@@ -326,10 +389,12 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Checks if the filename matches an already loaded assembly file's name /// Determines whether an assembly with the specified name is currently loaded.
/// </summary> /// </summary>
/// <param name="filename"></param> /// <remarks>This method performs a case-sensitive comparison to determine if the specified assembly is
/// <returns>True if file already matches loaded assembly file.</returns> /// loaded. It logs verbose messages indicating the status of the check.</remarks>
/// <param name="name">The name of the assembly to check. This value is case-sensitive.</param>
/// <returns><see langword="true"/> if an assembly with the specified name is loaded; otherwise, <see langword="false"/>.</returns>
public static bool CheckIfAssemblyLoaded(string name) public static bool CheckIfAssemblyLoaded(string name)
{ {
Debug.LogMessage(LogEventLevel.Verbose, "Checking if assembly: {0} is loaded...", name); Debug.LogMessage(LogEventLevel.Verbose, "Checking if assembly: {0} is loaded...", name);
@@ -348,9 +413,13 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Used by console command to report the currently loaded assemblies and versions /// Reports the versions of the Essentials framework, PepperDash Core, and loaded plugins to the console.
/// </summary> /// </summary>
/// <param name="command"></param> /// <remarks>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.</remarks>
/// <param name="command">The command string that triggered the version report. This parameter is not used directly by the method.</param>
public static void ReportAssemblyVersions(string command) public static void ReportAssemblyVersions(string command)
{ {
CrestronConsole.ConsoleCommandResponse("Essentials Version: {0}" + CrestronEnvironment.NewLine, Global.AssemblyVersion); CrestronConsole.ConsoleCommandResponse("Essentials Version: {0}" + CrestronEnvironment.NewLine, Global.AssemblyVersion);
@@ -381,20 +450,24 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder /// Moves .dll assemblies from the plugins folder to the loaded plugins directory.
/// </summary> /// </summary>
static void MoveDllAssemblies() /// <remarks>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.</remarks>
private static void MoveDllAssemblies()
{ {
Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder...");
var pluginDi = new DirectoryInfo(_pluginDirectory); var pluginDi = new DirectoryInfo(PluginDirectory);
var pluginFiles = pluginDi.GetFiles("*.dll"); var pluginFiles = pluginDi.GetFiles("*.dll");
if (pluginFiles.Length > 0) if (pluginFiles.Length > 0)
{ {
if (!Directory.Exists(_loadedPluginsDirectoryPath)) if (!Directory.Exists(LoadedPluginsDirectoryPath))
{ {
Directory.CreateDirectory(_loadedPluginsDirectoryPath); Directory.CreateDirectory(LoadedPluginsDirectoryPath);
} }
} }
@@ -408,7 +481,7 @@ namespace PepperDash.Essentials
{ {
string filePath = string.Empty; string filePath = string.Empty;
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; filePath = LoadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name;
// Check if there is a previous file in the loadedPlugins directory and delete // Check if there is a previous file in the loadedPlugins directory and delete
if (File.Exists(filePath)) if (File.Exists(filePath))
@@ -437,12 +510,17 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Unzips each .cplz archive into the temp directory and moves any unloaded files into loadedPlugins /// Extracts and processes .cplz archive files found in the specified directories, moving their contents to the
/// appropriate plugin directory.
/// </summary> /// </summary>
static void UnzipAndMoveCplzArchives() /// <remarks>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.</remarks>
private static void UnzipAndMoveCplzArchives()
{ {
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder...");
var di = new DirectoryInfo(_pluginDirectory); var di = new DirectoryInfo(PluginDirectory);
var zFiles = di.GetFiles("*.cplz"); 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 //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users
@@ -457,16 +535,16 @@ namespace PepperDash.Essentials
if (cplzFiles.Length > 0) if (cplzFiles.Length > 0)
{ {
if (!Directory.Exists(_loadedPluginsDirectoryPath)) if (!Directory.Exists(LoadedPluginsDirectoryPath))
{ {
Directory.CreateDirectory(_loadedPluginsDirectoryPath); Directory.CreateDirectory(LoadedPluginsDirectoryPath);
} }
} }
foreach (var zfi in cplzFiles) foreach (var zfi in cplzFiles)
{ {
Directory.CreateDirectory(_tempDirectory); Directory.CreateDirectory(TempDirectory);
var tempDi = new DirectoryInfo(_tempDirectory); var tempDi = new DirectoryInfo(TempDirectory);
Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName);
var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName);
@@ -481,7 +559,7 @@ namespace PepperDash.Essentials
{ {
string filePath = string.Empty; string filePath = string.Empty;
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; filePath = LoadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name;
// Check if there is a previous file in the loadedPlugins directory and delete // Check if there is a previous file in the loadedPlugins directory and delete
if (File.Exists(filePath)) if (File.Exists(filePath))
@@ -507,7 +585,7 @@ namespace PepperDash.Essentials
} }
// Delete the .cplz and the temp directory // Delete the .cplz and the temp directory
Directory.Delete(_tempDirectory, true); Directory.Delete(TempDirectory, true);
zfi.Delete(); zfi.Delete();
} }
@@ -515,12 +593,16 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Attempts to load the assemblies from the loadedPlugins folder /// Loads plugin assemblies from the designated plugin directory, checks their compatibility with .NET 8, and loads
/// the compatible assemblies into the application.
/// </summary> /// </summary>
static void LoadPluginAssemblies() /// <remarks>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.</remarks>
private static void LoadPluginAssemblies()
{ {
Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder...");
var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath); var pluginDi = new DirectoryInfo(LoadedPluginsDirectoryPath);
var pluginFiles = pluginDi.GetFiles("*.dll"); var pluginFiles = pluginDi.GetFiles("*.dll");
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length); Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length);
@@ -538,7 +620,7 @@ namespace PepperDash.Essentials
foreach (var pluginFile in pluginFiles) foreach (var pluginFile in pluginFiles)
{ {
string fileName = pluginFile.Name; string fileName = pluginFile.Name;
var (isCompatible, reason, references) = assemblyCompatibility[fileName]; var (isCompatible, reason, _) = assemblyCompatibility[fileName];
if (!isCompatible) if (!isCompatible)
{ {
@@ -560,9 +642,13 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Iterate the loaded assemblies and try to call the LoadPlugin method /// Loads and initializes custom plugin types from the assemblies in the plugin folder.
/// </summary> /// </summary>
static void LoadCustomPluginTypes() /// <remarks>This method iterates through all loaded plugin assemblies, identifies types that implement
/// the <see cref="IPluginDeviceFactory"/> 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.</remarks>
private static void LoadCustomPluginTypes()
{ {
Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types...");
@@ -576,7 +662,7 @@ namespace PepperDash.Essentials
try try
{ {
var assy = loadedAssembly.Assembly; var assy = loadedAssembly.Assembly;
Type[] types = {}; Type[] types = [];
try try
{ {
types = assy.GetTypes(); types = assy.GetTypes();
@@ -682,8 +768,13 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Updates incompatible plugins with information about which plugins depend on them /// Updates the triggering plugin information for incompatible plugins based on their dependencies.
/// </summary> /// </summary>
/// <remarks>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.</remarks>
/// <param name="pluginDependencies">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.</param>
private static void UpdateIncompatiblePluginDependencies(Dictionary<string, List<string>> pluginDependencies) private static void UpdateIncompatiblePluginDependencies(Dictionary<string, List<string>> pluginDependencies)
{ {
// For each incompatible plugin // For each incompatible plugin
@@ -709,16 +800,21 @@ namespace PepperDash.Essentials
} }
} }
} }
/// <summary>
/// Loads a
/// </summary>
/// <param name="plugin"></param>
/// <param name="loadedAssembly"></param>
static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly)
{
var developmentPlugin = plugin as IPluginDevelopmentDeviceFactory;
var passed = developmentPlugin != null ? Global.IsRunningDevelopmentVersion /// <summary>
/// Loads a custom plugin and performs a dependency check to ensure compatibility with the required Essentials
/// framework version.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="plugin">The plugin to be loaded, implementing the <see cref="IPluginDeviceFactory"/> interface. If the plugin also
/// implements <see cref="IPluginDevelopmentDeviceFactory"/>, additional checks for development versions are
/// performed.</param>
/// <param name="loadedAssembly">The assembly associated with the plugin being loaded. This is used for logging and tracking purposes.</param>
private static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly)
{
var passed = plugin is IPluginDevelopmentDeviceFactory developmentPlugin ? Global.IsRunningDevelopmentVersion
(developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion)
: Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion); : Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion);
@@ -742,59 +838,20 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// Loads a a custom plugin via the legacy method /// Loads plugins from the designated plugin directory, processes them, and integrates them into the application.
/// </summary>
/// <param name="type"></param>
/// <param name="loadPlugin"></param>
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))
{
var passed = Global.IsRunningMinimumVersionOrHigher(minimumVersionString);
if (!passed)
{
Debug.LogMessage(LogEventLevel.Information, "Plugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin", minimumVersionString);
return;
}
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);
}
/// <summary>
/// Loads plugins
/// </summary> /// </summary>
/// <remarks>This method performs the following steps: <list type="bullet"> <item><description>Checks if
/// the plugin directory exists.</description></item> <item><description>Processes any plugin files, including .dll
/// and .cplz files, by moving or extracting them as needed.</description></item> <item><description>Loads
/// assemblies from the processed plugins into the application domain.</description></item>
/// <item><description>Identifies and reports any incompatible plugins, including the reason for
/// incompatibility.</description></item> </list> Plugins that are successfully loaded are made available for use,
/// while incompatible plugins are logged for review.</remarks>
public static void LoadPlugins() public static void LoadPlugins()
{ {
Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory); Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", PluginDirectory);
if (Directory.Exists(_pluginDirectory)) if (Directory.Exists(PluginDirectory))
{ {
Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins");
@@ -804,7 +861,7 @@ namespace PepperDash.Essentials
// Deal with any .cplz files // Deal with any .cplz files
UnzipAndMoveCplzArchives(); UnzipAndMoveCplzArchives();
if (Directory.Exists(_loadedPluginsDirectoryPath)) if (Directory.Exists(LoadedPluginsDirectoryPath))
{ {
// Load the assemblies from the loadedPlugins folder into the AppDomain // Load the assemblies from the loadedPlugins folder into the AppDomain
LoadPluginAssemblies(); LoadPluginAssemblies();
@@ -833,78 +890,4 @@ namespace PepperDash.Essentials
} }
} }
} }
}
/// <summary>
/// Represents an assembly loaded at runtime and it's associated metadata
/// </summary>
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;
}
}
/// <summary>
/// Represents a plugin that was found to be incompatible with .NET 8
/// </summary>
public class IncompatiblePlugin
{
[JsonProperty("name")]
public string Name { get; private set; }
[JsonProperty("reason")]
public string Reason { get; private set; }
[JsonProperty("triggeredBy")]
public string TriggeredBy { get; private set; }
public IncompatiblePlugin(string name, string reason, string triggeredBy = null)
{
Name = name;
Reason = reason;
TriggeredBy = triggeredBy ?? "Direct load";
}
/// <summary>
/// Updates the plugin that triggered this incompatibility
/// </summary>
/// <param name="triggeringPlugin">Name of the plugin that requires this incompatible plugin</param>
public void UpdateTriggeringPlugin(string triggeringPlugin)
{
if (!string.IsNullOrEmpty(triggeringPlugin))
{
TriggeredBy = triggeringPlugin;
}
}
}
/// <summary>
/// Attribute to explicitly mark a plugin as .NET 8 compatible
/// </summary>
[AttributeUsage(AttributeTargets.Assembly)]
public class Net8CompatibleAttribute : Attribute
{
public bool IsCompatible { get; }
public Net8CompatibleAttribute(bool isCompatible = true)
{
IsCompatible = isCompatible;
}
}
} }