From cdcb0ec8c95d2d7da6c4846e2945fc8165011f19 Mon Sep 17 00:00:00 2001 From: William Date: Sun, 19 Jul 2026 21:46:30 +0100 Subject: [PATCH 01/15] Rewrote a large chunk of the plugin stuff --- src/OpenPuppet.SDK/IPlugin.cs | 528 ++++++++++++------ src/OpenPuppet.SDK/IPlugin.old.cs | 273 +++++++++ src/OpenPuppet.SDK/Logger.cs | 6 +- src/OpenPuppet.SDK/OpenPuppet.SDK.csproj | 10 + src/OpenPuppet.SDK/Plugins/Path.cs | 48 -- src/OpenPuppet.SDK/Plugins/PluginMetadata.cs | 18 + src/OpenPuppet/Program.cs | 17 +- .../OpenPuppet.Core/Settings/Plugins.cs | 12 +- 8 files changed, 675 insertions(+), 237 deletions(-) create mode 100644 src/OpenPuppet.SDK/IPlugin.old.cs delete mode 100644 src/OpenPuppet.SDK/Plugins/Path.cs create mode 100644 src/OpenPuppet.SDK/Plugins/PluginMetadata.cs diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index dbf48d5..f59d6ec 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -1,273 +1,451 @@ -using ImGuiNET; +using Microsoft.Win32; using Newtonsoft.Json; -using OpenPuppet.Plugins; -using OpenPuppet.SDK.Events; +using OpenPuppet.SDK.Plugin; using OpenPuppet.SDK.Plugins; -using System.Collections.Generic; +using System.ComponentModel.Design; +using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using static OpenPuppet.SDK.Logger; -namespace OpenPuppet +namespace OpenPuppet.SDK { - /// - /// Plugin interface - /// public interface IPlugin : IDisposable { - public static Dictionary RegisteredPlugins { get; } = new(); + /* + Plugin loading: + - Attempt to load plugin list + If this doesn't exist, scan the plugin + directory, and add all of the plugins + within that directory to the list, + and enable all of them + - Load the plugin list into RegisteredPlugins + - For each plugin registered: + - Load the plugin assembly + - Call the OnInitialized method + - Check for updates (if possible) + - Mark as loaded + + Plugin unloading: + - Call OnShutdown + - Call Dispose + - Unload and clean up + - Cycle through GC cleaning + (Warn if reference leaks occur) + - Mark as EITHER: + - Ready Meaning, ready to be loaded + - Unloaded Meaning unloaded, but + has reference leaks, so should + not be loaded again + */ + + static Dictionary RegisteredPlugins { get; } = new(); static readonly object _pluginLock = new(); - const int MaxUnloadGcAttempts = 10; + static int MaxUnloadGCAttempts = 10; + public static string? InstallPath + { + get + { + return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + } + static string PluginPath + { + get + { + return Path.Combine(InstallPath!, "Plugins"); + } + } + static string PluginListPath + { + get + { + return Path.Combine(PluginPath!, "plugins.json"); + } + } - public static void RegisterPlugin(PluginMetadata metadata, string path, IPlugin? plugin) + public const string PluginID = ""; + public PluginLogger Logger { get; } + + public static void RegisterPlugin(PluginMetadata metadata, string path, IPlugin? plugin = null, bool enabled = false) { if (RegisteredPlugins.ContainsKey(metadata.ID)) - throw new ArgumentException($"Plugin with the ID of \"{metadata.ID}\" is already registered"); - RegisteredPlugins[metadata.ID] = new(metadata, path, null, null, plugin, false); + throw new ArgumentException($"Plugin with the ID of \"{metadata.ID}\" has already been registered"); + + RegisteredPlugins.Add(metadata.ID, new(metadata, path, enabled)); } - public static void EnablePlugin(string registry) + + public static void EnablePlugin(string id) { - if (RegisteredPlugins.ContainsKey(registry)) + if (RegisteredPlugins.TryGetValue(id, out var plugin)) { - if (RegisteredPlugins[registry].Plugin != null) return; + plugin.Enabled = true; + SavePluginList(); - string item = RegisteredPlugins[registry].Path; + LoadPlugin(id, plugin.Path); + } + else throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + } - var anycpu = Path.Combine(item, $"anycpu.dll"); - var specific = Path.Combine(item, $"{RuntimeInformation.ProcessArchitecture}.dll"); + public static void DisablePlugin(string id) + { + if (RegisteredPlugins.TryGetValue(id, out var plugin)) + { + plugin.Enabled = false; + SavePluginList(); + UnloadPlugin(id); + } + else throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + } - if (File.Exists(specific)) LoadAssembly(specific, registry); - else if (File.Exists(anycpu)) LoadAssembly(anycpu, registry); - else Console.WriteLine($"Warning: could not load {Path.GetFileName(item)}: " + - $"no dll found for architecture '{RuntimeInformation.ProcessArchitecture}'"); + public static void SetPluginEnabled(string ID, bool enabled) + { + if (enabled) EnablePlugin(ID); + else DisablePlugin(ID); + } + + [Obsolete("Please use UninstallPlugin instead", true)] + public static void RemovePlugin(string registry) + { + UninstallPlugin(registry); + } + + public static void LoadPlugin(string id, string path) + { + string anycpu = Path.Combine(path, "anycpu.dll"), + specific = Path.Combine(path, $"{RuntimeInformation.ProcessArchitecture}.dll"); + + if (File.Exists(specific)) + LoadAssembly(specific, id); + else if (File.Exists(anycpu)) + LoadAssembly(anycpu, id); + else + { + // TODO: Find if there's a more appropriate exception for this + throw new DllNotFoundException($"Plugin with the ID \"{id}\" does not contain " + + $"a DLL for architecture \"{RuntimeInformation.ProcessArchitecture}\""); } - else throw new ArgumentException($"Plugin with the ID of\"{registry}\" has not been registered"); } - public static void DisablePlugin(string registry) + + public static void UnloadPlugin(string id, bool soft = false) { - if (RegisteredPlugins.ContainsKey(registry)) + lock(_pluginLock) { - if (RegisteredPlugins[registry].Plugin == null) return; - //RegisteredPlugins[registry].Assembly!.OnShutdown(); - RegisteredPlugins[registry].Enabled = false; // Temporary, move to unloading method - UnloadPlugin(registry); } - else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); } - public static void RemovePlugin(string registry) + + static void LoadAssembly(string path, string id) { - if (RegisteredPlugins.ContainsKey(registry)) + if (!RegisteredPlugins.TryGetValue(id, out var plugin)) + throw new ArgumentException($"Plugin with the ID of \"{plugin}\" hsa not been registered"); + + plugin.LoadContext = new PluginLoadContext(path); + plugin.WeakReference = new(plugin.LoadContext); + + try + { + Assembly asm = plugin.LoadContext.LoadFromAssemblyPath(path); + var t = asm.DefinedTypes.Single(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsAnsiClass); + plugin.Plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; + plugin.State = PluginState.Loaded; + plugin.Plugin.OnInitialized(); + /*asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => + { + var p = (IPlugin)Activator.CreateInstance(t.AsType())!; + //PluginManager.LoadedPlugins.Add(plugin, path); + plugin.Plugin = p; + plugin.State = PluginState.Loaded; + + plugin.OnInitialized(); + });*/ + } catch { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Warn, - "Removing plugins has not yet been fully implemented" + SDK.logger.WriteLine( + ILogger.Level.Warn, + $"Failed to load plugin with ID \"{id}\"" ); - // Uninstall - RegisteredPlugins.Remove(registry); + UnloadPlugin(id); } - else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); } - public static bool UnloadPlugin(string registry, bool soft = false) + public static void LoadPluginDirectory(string path, bool enabled = true) { - lock (_pluginLock) - { - if (!RegisteredPlugins.TryGetValue(registry, out var plugin)) - throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + if (!Directory.Exists(path)) + throw new ArgumentException($"\"{path}\" does not exist"); - if (plugin.Plugin == null || - plugin.LoadContext == null || - plugin.WeakReference == null) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Log, - $"Plugin with the ID of \"{registry}\" cannot be unloaded, as it is not loaded" - ); + string meta = Path.Combine(path, "meta.inf"); + if (!File.Exists(meta)) + throw new ArgumentException($"No metadata present in \"{path}\" (\"{meta}\" does not exist)"); - RegisteredPlugins.Remove(registry); - return true; - } + PluginMetadata? metadata = JsonConvert.DeserializeObject(File.ReadAllText(meta)) + ?? throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); + RegisterPlugin(metadata, path, enabled: enabled); + // TODO: Add further check to see if the plugin is invalid or not + if (enabled) LoadPlugin(metadata.ID, path); + } - var weakRef = plugin.WeakReference; - var loadContext = plugin.LoadContext; + public static void InstallPlugin(Plugin.LocalInstallSource source) + { - try - { - plugin.Plugin.OnShutdown(); - } - catch (Exception ex) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Error, - $"Plugin \"{registry}\" threw during OnShutdown: {ex}" - ); - } - finally + } + + public static void InstallPlugin(Plugin.InternetInstallSource source) + { + + } + + public static void UninstallPlugin(string id) + { + if(RegisteredPlugins.TryGetValue(id, out var plugin)) + { + switch(plugin.State) { - plugin.Plugin = null; + case PluginState.Unknown: + case PluginState.Invalid: + case PluginState.Unloaded: + // TODO: Find appropriate exception + throw new Exception($"Plugin with the ID \"{id}\" is not ready to be uninstalled"); + case PluginState.Loaded: + UnloadPlugin(id); + UninstallPlugin(id); + return; } try { - loadContext.Unload(); - } - finally + Directory.Delete(id); + } catch(Exception ex) { - plugin.LoadContext = null; - } - - for (int i = 0; weakRef.IsAlive && i < MaxUnloadGcAttempts; i++) + SDK.logger.WriteLine( + ILogger.Level.Warn, + $"Could not fully uninstall plugin with the ID \"{id}\": {ex.Message}" + ); + } finally { - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); + RegisteredPlugins.Remove(id); + SavePluginList(); } + } + else throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + } - bool leaked = weakRef.IsAlive; - - if (leaked) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Warn, - $"Could not unload Plugin \"{registry}\", possible reference leak" - ); - - if (soft) - IEvent.Invoke("openpuppet.restart", null, false); - else - IEvent.Invoke("openpuppet.unstable", null, EventArgs.Empty); - } + static string SafePluginName(string name) + { + name = name.ToLower().Replace(' ', '-'); + name = Regex.Replace(name, @"[^a-zA-Z0-9.]", ""); - //RegisteredPlugins.Remove(registry); - plugin.Enabled = false; - return !leaked; - } + return name; } - static void LoadAssembly(string path, string registry) + static string GetPluginPath(string name) { - if (!RegisteredPlugins.ContainsKey(registry)) - throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + if (PluginPath == null) return null!; + return Path.Combine(PluginPath, SafePluginName(name)); + } - // Remove the logs once fixed + static string GetPluginPath(PluginMetadata metadata) + { + return GetPluginPath(metadata.Name); + } - RegisteredPlugins[registry].LoadContext = new PluginLoadContext(path); - RegisteredPlugins[registry].WeakReference = new( - RegisteredPlugins[registry].LoadContext//, - //trackResurrection: true - ); - Assembly asm = RegisteredPlugins[registry].LoadContext!.LoadFromAssemblyPath(path); - asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => - { - RegisteredPlugins[registry].Plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; - RegisteredPlugins[registry].Plugin!.OnInitialized(); - SDK.SDK.logger.WriteLine($"Initialized Plugin {registry}"); - }); - /*foreach(Type t in asm.GetTypes()) + public static void LoadPluginList() + { + static void GenerateDefault() { - if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass) + Dictionary plugins = new(); + + foreach (var item in Directory.GetDirectories(PluginPath)) { - SDK.SDK.logger.WriteLine($"Found IPlugin in Plugin {registry}"); - RegisteredPlugins[registry].Assembly = (IPlugin)Activator.CreateInstance(t)!; - RegisteredPlugins[registry].Assembly!.OnInitialized(); - SDK.SDK.logger.WriteLine($"Initialised Plugin {registry}"); + string meta = Path.Combine(item, "meta.inf"); + if (!File.Exists(meta)) + throw new ArgumentException($"No metadata present in \"{item}\" (\"{meta}\" does not exist)"); + + PluginMetadata? metadata = JsonConvert.DeserializeObject(File.ReadAllText(meta)) + ?? throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); + + plugins.Add( + metadata.ID, + new PluginItem + { + Path = item, + Enabled = true + } + ); } - }*/ - /*var asm = Assembly.LoadFrom(path); - asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => - { - var plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; - //PluginManager.LoadedPlugins.Add(plugin, path); - RegisteredPlugins[registry].Assembly = plugin; - RegisteredPlugins[registry].Enabled = true; + SavePluginList(plugins); + } - plugin.OnInitialized(); - });*/ + if (File.Exists(PluginListPath)) + { + try + { + var plugins = JsonConvert.DeserializeObject>(File.ReadAllText(PluginListPath)); + if (plugins == null) + { + SDK.logger.WriteLine( + ILogger.Level.Warn, + "Plugins list is null, rewriting with default values. " + + "The user will need to reinstall plugins." + ); + + GenerateDefault(); + } + else + { + SDK.logger.WriteLine( + ILogger.Level.OK, + $"Found {plugins.Count} plugins" + ); + + foreach (var plugin in plugins) + LoadPluginDirectory(plugin.Value.Path, plugin.Value.Enabled); + } + } catch (Exception ex) + { + SDK.logger.WriteLine( + ILogger.Level.Error, + $"Failed to load plugins list: " + ex.Message + ); + SDK.logger.WriteLine( + ILogger.Level.Warn, + "Rewriting plugin list with default values. " + + "The user will need to reinstall plugins." + ); - RegisteredPlugins[registry].Enabled = true; + GenerateDefault(); + } + } + else GenerateDefault(); } - /// - /// Load a plugin from a directory. - /// This was previously in Program.cs, but has been moved here so that - /// plugins can be loaded later on. - /// - /// The path of the directory - public static void LoadPluginDirectory(string path, bool enabled = true) + public static Dictionary GetPluginList() { - if (!Directory.Exists(path)) - throw new ArgumentException($"\"{path}\" does not exist"); - - string meta = Path.Combine(path, "meta.inf"); - if (!File.Exists(meta)) - throw new ArgumentException($"No metadata present in \"{path}\" (\"{meta}\" does not exist)"); - - PluginMetadata? metadata = JsonConvert.DeserializeObject(File.ReadAllText(meta)); - if(metadata == null) - throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); + Dictionary plugins = []; + foreach (var plugin in RegisteredPlugins) + { + plugins.Add(plugin.Key, new PluginItem + { + Path = plugin.Value.Path, + Enabled = plugin.Value.Enabled, + }); + } + return plugins; + } - RegisterPlugin(metadata, path, null); - if(enabled) EnablePlugin(metadata.ID); + public static void SavePluginList(Dictionary? list = null) + { + File.WriteAllText( + PluginListPath, + JsonConvert.SerializeObject(list ?? GetPluginList(), Formatting.Indented) + ); } - /// - /// The plugin ID - /// - public const string PluginID = "com.unknown.unknown"; + /* + Plugin method definitions + */ - public SDK.Logger.PluginLogger Logger { get; } + void IDisposable.Dispose() { } /// - /// OnInitialized - /// This is called when the plugin is loaded, use this to prepare the plugin + /// OnInitialized is called when the plugin is loaded. + /// This should be used to prepare the plugin, and + /// doing things such as:
+ /// - Subscribing to events
+ /// - Registering windows
+ /// - Registering dialogs
+ /// - Registering context menu items ///
abstract void OnInitialized(); /// - /// OnShutdown - /// This is called when the plugin is being unloaded, use this to save any data or configuration + /// OnShutdown is called when the plugin is unloaded. + /// This should be used to save any data, and + /// doing things such as:
+ /// - Unsubscribing from events
+ /// - Deregistering windows
+ /// - Deregistering dialogs
+ /// - Deregistering context menu items ///
abstract void OnShutdown(); } - /// - /// Plugin metadata file - /// - public class PluginMetadata + public enum PluginState { - public string ID { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string Version { get; set; } = string.Empty; - public string Icon { get; set; } = string.Empty; + // Needs to be checked + Unknown, + // Ready to be loaded + Ready, + // Loaded, and in use + Loaded, + // Unloaded, but has reference leaks + Unloaded, + // Cannot be loaded, no DLL + Invalid } public class RegisteredPlugin { + /// + /// Plugin metadata, loaded from meta.inf + /// public PluginMetadata Metadata { get; set; } + + /// + /// Plugin path, the root directory of the plugin. + /// This should have the following files:
+ /// - meta.inf
+ /// - anycpu.dll/pluginid.dll + ///
public string Path { get; set; } + public PluginLoadContext? LoadContext { get; set; } public WeakReference? WeakReference { get; set; } public IPlugin? Plugin { get; set; } + + public PluginState State { get; set; } public bool Enabled { get; set; } public RegisteredPlugin( PluginMetadata metadata, string path, - PluginLoadContext? loadContext, - WeakReference? weakReference, - IPlugin? assembly, - bool enabled) + bool enabled, + + PluginLoadContext? loadContext = null, + WeakReference? weakReference = null, + IPlugin? plugin = null + ) { Metadata = metadata; Path = path; + State = PluginState.Unknown; + LoadContext = loadContext; WeakReference = weakReference; - Plugin = assembly; - Enabled = enabled; + Plugin = plugin; + } + } + + namespace Plugin + { + public class PluginItem + { + public string Path { get; set; } + + public bool Enabled { get; set; } + } + + public class LocalInstallSource + { + + } + + public class InternetInstallSource + { + } } } \ No newline at end of file diff --git a/src/OpenPuppet.SDK/IPlugin.old.cs b/src/OpenPuppet.SDK/IPlugin.old.cs new file mode 100644 index 0000000..393d7f5 --- /dev/null +++ b/src/OpenPuppet.SDK/IPlugin.old.cs @@ -0,0 +1,273 @@ +using ImGuiNET; +using Newtonsoft.Json; +using OpenPuppet.Plugins; +using OpenPuppet.SDK.Events; +using OpenPuppet.SDK.Plugins; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace OpenPuppet +{ + /// + /// Plugin interface + /// + public interface OldIPlugin : IDisposable + { + public static Dictionary RegisteredPlugins { get; } = new(); + + static readonly object _pluginLock = new(); + const int MaxUnloadGcAttempts = 10; + + public static void RegisterPlugin(PluginMetadata metadata, string path, IPlugin? plugin) + { + if (RegisteredPlugins.ContainsKey(metadata.ID)) + throw new ArgumentException($"Plugin with the ID of \"{metadata.ID}\" is already registered"); + RegisteredPlugins[metadata.ID] = new(metadata, path, null, null, plugin, false); + } + public static void EnablePlugin(string registry) + { + if (RegisteredPlugins.ContainsKey(registry)) + { + if (RegisteredPlugins[registry].Plugin != null) return; + + string item = RegisteredPlugins[registry].Path; + + var anycpu = Path.Combine(item, $"anycpu.dll"); + var specific = Path.Combine(item, $"{RuntimeInformation.ProcessArchitecture}.dll"); + + if (File.Exists(specific)) LoadAssembly(specific, registry); + else if (File.Exists(anycpu)) LoadAssembly(anycpu, registry); + else Console.WriteLine($"Warning: could not load {Path.GetFileName(item)}: " + + $"no dll found for architecture '{RuntimeInformation.ProcessArchitecture}'"); + } + else throw new ArgumentException($"Plugin with the ID of\"{registry}\" has not been registered"); + } + public static void DisablePlugin(string registry) + { + if (RegisteredPlugins.ContainsKey(registry)) + { + if (RegisteredPlugins[registry].Plugin == null) return; + + //RegisteredPlugins[registry].Assembly!.OnShutdown(); + RegisteredPlugins[registry].Enabled = false; // Temporary, move to unloading method + UnloadPlugin(registry); + } + else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + } + public static void RemovePlugin(string registry) + { + if (RegisteredPlugins.ContainsKey(registry)) + { + SDK.SDK.logger.WriteLine( + SDK.Logger.ILogger.Level.Warn, + "Removing plugins has not yet been fully implemented" + ); + // Uninstall + RegisteredPlugins.Remove(registry); + } + else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + } + + public static bool UnloadPlugin(string registry, bool soft = false) + { + lock (_pluginLock) + { + if (!RegisteredPlugins.TryGetValue(registry, out var plugin)) + throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + + if (plugin.Plugin == null || + plugin.LoadContext == null || + plugin.WeakReference == null) + { + SDK.SDK.logger.WriteLine( + SDK.Logger.ILogger.Level.Log, + $"Plugin with the ID of \"{registry}\" cannot be unloaded, as it is not loaded" + ); + + RegisteredPlugins.Remove(registry); + return true; + } + + var weakRef = plugin.WeakReference; + var loadContext = plugin.LoadContext; + + try + { + plugin.Plugin.OnShutdown(); + } + catch (Exception ex) + { + SDK.SDK.logger.WriteLine( + SDK.Logger.ILogger.Level.Error, + $"Plugin \"{registry}\" threw during OnShutdown: {ex}" + ); + } + finally + { + plugin.Plugin = null; + } + + try + { + loadContext.Unload(); + } + finally + { + plugin.LoadContext = null; + } + + for (int i = 0; weakRef.IsAlive && i < MaxUnloadGcAttempts; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + bool leaked = weakRef.IsAlive; + + if (leaked) + { + SDK.SDK.logger.WriteLine( + SDK.Logger.ILogger.Level.Warn, + $"Could not unload Plugin \"{registry}\", possible reference leak" + ); + + if (soft) + IEvent.Invoke("openpuppet.restart", null, false); + else + IEvent.Invoke("openpuppet.unstable", null, EventArgs.Empty); + } + + //RegisteredPlugins.Remove(registry); + plugin.Enabled = false; + return !leaked; + } + } + + static void LoadAssembly(string path, string registry) + { + if (!RegisteredPlugins.ContainsKey(registry)) + throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + + // Remove the logs once fixed + + RegisteredPlugins[registry].LoadContext = new PluginLoadContext(path); + RegisteredPlugins[registry].WeakReference = new( + RegisteredPlugins[registry].LoadContext//, + //trackResurrection: true + ); + Assembly asm = RegisteredPlugins[registry].LoadContext!.LoadFromAssemblyPath(path); + asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => + { + RegisteredPlugins[registry].Plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; + RegisteredPlugins[registry].Plugin!.OnInitialized(); + SDK.SDK.logger.WriteLine($"Initialized Plugin {registry}"); + }); + /*foreach(Type t in asm.GetTypes()) + { + if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass) + { + SDK.SDK.logger.WriteLine($"Found IPlugin in Plugin {registry}"); + RegisteredPlugins[registry].Assembly = (IPlugin)Activator.CreateInstance(t)!; + RegisteredPlugins[registry].Assembly!.OnInitialized(); + SDK.SDK.logger.WriteLine($"Initialised Plugin {registry}"); + } + }*/ + + /*var asm = Assembly.LoadFrom(path); + asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => + { + var plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; + //PluginManager.LoadedPlugins.Add(plugin, path); + RegisteredPlugins[registry].Assembly = plugin; + RegisteredPlugins[registry].Enabled = true; + + plugin.OnInitialized(); + });*/ + + RegisteredPlugins[registry].Enabled = true; + } + + /// + /// Load a plugin from a directory. + /// This was previously in Program.cs, but has been moved here so that + /// plugins can be loaded later on. + /// + /// The path of the directory + public static void LoadPluginDirectory(string path, bool enabled = true) + { + if (!Directory.Exists(path)) + throw new ArgumentException($"\"{path}\" does not exist"); + + string meta = Path.Combine(path, "meta.inf"); + if (!File.Exists(meta)) + throw new ArgumentException($"No metadata present in \"{path}\" (\"{meta}\" does not exist)"); + + PluginMetadata? metadata = JsonConvert.DeserializeObject(File.ReadAllText(meta)); + if(metadata == null) + throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); + + RegisterPlugin(metadata, path, null); + if(enabled) EnablePlugin(metadata.ID); + } + + /// + /// The plugin ID + /// + public const string PluginID = "com.unknown.unknown"; + + public SDK.Logger.PluginLogger Logger { get; } + + /// + /// OnInitialized + /// This is called when the plugin is loaded, use this to prepare the plugin + /// + abstract void OnInitialized(); + + /// + /// OnShutdown + /// This is called when the plugin is being unloaded, use this to save any data or configuration + /// + abstract void OnShutdown(); + } + + /// + /// Plugin metadata file + /// + public class PluginMetadata + { + public string ID { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; + } + + public class RegisteredPlugin + { + public PluginMetadata Metadata { get; set; } + public string Path { get; set; } + public PluginLoadContext? LoadContext { get; set; } + public WeakReference? WeakReference { get; set; } + public IPlugin? Plugin { get; set; } + public bool Enabled { get; set; } + + public RegisteredPlugin( + PluginMetadata metadata, + string path, + PluginLoadContext? loadContext, + WeakReference? weakReference, + IPlugin? assembly, + bool enabled) + { + Metadata = metadata; + Path = path; + LoadContext = loadContext; + WeakReference = weakReference; + Plugin = assembly; + Enabled = enabled; + } + } +} \ No newline at end of file diff --git a/src/OpenPuppet.SDK/Logger.cs b/src/OpenPuppet.SDK/Logger.cs index 0428345..156a8fa 100644 --- a/src/OpenPuppet.SDK/Logger.cs +++ b/src/OpenPuppet.SDK/Logger.cs @@ -25,13 +25,13 @@ public static class LogManager { public static string RequestPluginLogFile(string pluginName) { - if(!Directory.Exists(Path.Combine(LogPath!, OpenPuppet.Plugins.PluginsPath.SafePluginName(pluginName)))) { - Directory.CreateDirectory(Path.Combine(LogPath!, OpenPuppet.Plugins.PluginsPath.SafePluginName(pluginName))); + if(!Directory.Exists(Path.Combine(LogPath!, IPlugin.SafePluginName(pluginName)))) { + Directory.CreateDirectory(Path.Combine(LogPath!, IPlugin.SafePluginName(pluginName))); } string path = Path.Combine( LogPath!, - OpenPuppet.Plugins.PluginsPath.SafePluginName(pluginName), + IPlugin.SafePluginName(pluginName), DateTime.Now.ToString("dd'-'MM'-'yyyy'.txt'") ); diff --git a/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj b/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj index 488e755..1d5c79c 100644 --- a/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj +++ b/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj @@ -7,6 +7,16 @@ true + + + + + + + + + + diff --git a/src/OpenPuppet.SDK/Plugins/Path.cs b/src/OpenPuppet.SDK/Plugins/Path.cs deleted file mode 100644 index cad3b79..0000000 --- a/src/OpenPuppet.SDK/Plugins/Path.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace OpenPuppet.Plugins -{ - public static class PluginsPath - { - internal static string? InstallPath - { - get - { - return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - } - } - - public static string? PluginPath - { - get - { - return InstallPath != null ? Path.Combine(InstallPath, "Plugins") : null; - } - } - - public static string SafePluginName(string name) - { - name = name.ToLower().Replace(' ', '-'); - name = Regex.Replace(name, @"[^a-zA-Z0-9.]", ""); - - return name; - } - - public static string GetPluginPath(string name) - { - if (PluginPath == null) return null!; - return System.IO.Path.Combine(PluginPath, SafePluginName(name)); - } - - public static string GetPluginPath(PluginMetadata metadata) - { - return GetPluginPath(metadata.Name); - } - } -} \ No newline at end of file diff --git a/src/OpenPuppet.SDK/Plugins/PluginMetadata.cs b/src/OpenPuppet.SDK/Plugins/PluginMetadata.cs new file mode 100644 index 0000000..00dfe19 --- /dev/null +++ b/src/OpenPuppet.SDK/Plugins/PluginMetadata.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenPuppet.SDK +{ + public class PluginMetadata + { + public string ID { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; + } +} diff --git a/src/OpenPuppet/Program.cs b/src/OpenPuppet/Program.cs index 039870b..9c86bf4 100644 --- a/src/OpenPuppet/Program.cs +++ b/src/OpenPuppet/Program.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using OpenPuppet.Plugins; using OpenPuppet.rendering; using OpenPuppet.rendering.VertexTypes; using OpenPuppet.SDK; @@ -62,6 +61,9 @@ static void Main(string[] args) } } + if (IPlugin.InstallPath == null) + throw new Exception("\"Plugin.InstallPath\" is null"); + startupSW = new(); startupSW.Start(); @@ -343,7 +345,7 @@ static void Closing() //for (int i = 0; i < PluginManager.LoadedPlugins.Count; i++) // PluginManager.LoadedPlugins[i].OnShutdown(); - PluginManager.SavePluginList(); + IPlugin.SavePluginList(); foreach (var plugin in IPlugin.RegisteredPlugins) { try @@ -368,9 +370,13 @@ static void Closing() static void LoadPlugins() { - PluginManager.LoadPluginList(); + IPlugin.LoadPluginList(); + + // TODO: Clean up below, make sure that nothing important is left + /* + List errors = new(); - /*foreach (var item in Directory.GetDirectories(PluginsPath.PluginPath!)) + \*foreach (var item in Directory.GetDirectories(PluginsPath.PluginPath!)) { try { @@ -381,7 +387,7 @@ static void LoadPlugins() errors.Add(ex.Message); logger.WriteLine(ex.Message); } - }*/ + }*\ foreach(var item in PluginManager.Plugins) { @@ -398,6 +404,7 @@ static void LoadPlugins() logger.WriteLine(Logger.ILogger.Level.Warn, $"{errors.Count} plugins failed to load"); PluginEvents.InvokeFinishedLoading(null); + */ } static void SoftRestart() diff --git a/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs b/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs index 9ca364e..cdf3585 100644 --- a/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs +++ b/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using OpenPuppet.Plugins; using OpenPuppet.SDK; using OpenPuppet.SDK.Events; using System; @@ -30,7 +29,7 @@ public void OnRender(double deltaTime) foreach (var plugin in IPlugin.RegisteredPlugins) { if (plugin.Key == "com.openpuppet.core") continue; - PluginManager.SetPluginEnabled(plugin.Key, true); + IPlugin.SetPluginEnabled(plugin.Key, true); } } ImGui.SameLine(); @@ -39,7 +38,7 @@ public void OnRender(double deltaTime) foreach (var plugin in IPlugin.RegisteredPlugins) { if (plugin.Key == "com.openpuppet.core") continue; - PluginManager.SetPluginEnabled(plugin.Key, false); + IPlugin.SetPluginEnabled(plugin.Key, false); WarnRestart = true; } } @@ -49,7 +48,7 @@ public void OnRender(double deltaTime) foreach (var plugin in IPlugin.RegisteredPlugins) { if (plugin.Key == "com.openpuppet.core") continue; - PluginManager.UninstallPlugin(plugin.Key); + IPlugin.UninstallPlugin(plugin.Key); WarnRestart = true; } } @@ -90,6 +89,7 @@ public void OnRender(double deltaTime) foreach (var plugin in IPlugin.RegisteredPlugins) { bool disabled = plugin.Key == "com.openpuppet.core"; + PluginState state = plugin.Value.State; bool enabled = plugin.Value.Enabled; //ImGui.BeginChild("Plugins-" + plugin.Key); ImGui.Columns(2); @@ -105,13 +105,13 @@ public void OnRender(double deltaTime) ImGui.BeginDisabled(true); if(ImGui.Button((enabled ? "Disable" : "Enable") + $"##{plugin.Key}") && !disabled) { - PluginManager.SetPluginEnabled(plugin.Key, !enabled); + IPlugin.SetPluginEnabled(plugin.Key, !enabled); if(enabled) WarnRestart = true; } ImGui.SameLine(); if(ImGui.Button("Uninstall##" + plugin.Key) && !disabled) { - PluginManager.UninstallPlugin(plugin.Key); + IPlugin.UninstallPlugin(plugin.Key); WarnRestart = true; } if(disabled) From 68e0bba5624185fa9c3c7989520223d5c0f8d188 Mon Sep 17 00:00:00 2001 From: William Date: Sun, 19 Jul 2026 22:15:26 +0100 Subject: [PATCH 02/15] Hmm --- src/OpenPuppet.SDK/IPlugin.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index f59d6ec..b399f60 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -40,7 +40,7 @@ and enable all of them not be loaded again */ - static Dictionary RegisteredPlugins { get; } = new(); + public static Dictionary RegisteredPlugins { get; } = []; static readonly object _pluginLock = new(); static int MaxUnloadGCAttempts = 10; @@ -184,7 +184,7 @@ public static void LoadPluginDirectory(string path, bool enabled = true) ?? throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); RegisterPlugin(metadata, path, enabled: enabled); // TODO: Add further check to see if the plugin is invalid or not - if (enabled) LoadPlugin(metadata.ID, path); + if (enabled) EnablePlugin(metadata.ID); } public static void InstallPlugin(Plugin.LocalInstallSource source) @@ -277,6 +277,8 @@ static void GenerateDefault() } SavePluginList(plugins); + foreach (var plugin in plugins) + LoadPluginDirectory(plugin.Value.Path, plugin.Value.Enabled); } if (File.Exists(PluginListPath)) From 2e1255293cfa38596cd3ff0677193dee0b08b597 Mon Sep 17 00:00:00 2001 From: William Date: Sun, 19 Jul 2026 22:30:47 +0100 Subject: [PATCH 03/15] Still doesnt unload properly :rage1: --- src/OpenPuppet.SDK/IPlugin.cs | 69 +++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index b399f60..36b339f 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -1,5 +1,6 @@ using Microsoft.Win32; using Newtonsoft.Json; +using OpenPuppet.SDK.Events; using OpenPuppet.SDK.Plugin; using OpenPuppet.SDK.Plugins; using System.ComponentModel.Design; @@ -133,7 +134,75 @@ public static void UnloadPlugin(string id, bool soft = false) { lock(_pluginLock) { + if(!RegisteredPlugins.TryGetValue(id, out var plugin)) + throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + if(plugin.State == PluginState.Loaded && + plugin.Plugin != null && + plugin.LoadContext != null && + plugin.WeakReference != null) + { + var weakRef = plugin.WeakReference; + var loadContext = plugin.LoadContext; + + try + { + plugin.Plugin.OnShutdown(); + plugin.Plugin.Dispose(); + } + catch (Exception ex) + { + SDK.logger.WriteLine( + ILogger.Level.Error, + $"Plugin \"{id}\" threw an exception during OnShutdown: {ex}" + ); + } + finally + { + plugin.Plugin = null; + } + + try + { + loadContext.Unload(); + } + finally + { + plugin.LoadContext = null; + loadContext = null; + } + + for (int i = 0; weakRef.IsAlive && i < MaxUnloadGCAttempts; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + bool leaked = weakRef.IsAlive; + weakRef = null; + + if (leaked) + { + SDK.logger.WriteLine( + ILogger.Level.Warn, + $"Could not unload Plugin \"{id}\", possible reference leak" + ); + + if (soft) + IEvent.Invoke("openpuppet.restart", null, false); + else + IEvent.Invoke("openpuppet.unstable", null, EventArgs.Empty); + } + + plugin.State = leaked ? PluginState.Unloaded : PluginState.Ready; + } else + { + SDK.logger.WriteLine( + ILogger.Level.Log, + $"Plugin with the ID of \"{id}\" cannot be unloaded, as it is not in an unloadable state" + ); + } } } From 893eab203d90192fae3c5929895e6fefd199868f Mon Sep 17 00:00:00 2001 From: William Date: Mon, 20 Jul 2026 20:33:29 +0100 Subject: [PATCH 04/15] Idk --- .../OpenPuppet.Core/Settings/Plugins.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs b/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs index cdf3585..32f93f4 100644 --- a/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs +++ b/src/Plugins/OpenPuppet.Core/Settings/Plugins.cs @@ -101,6 +101,26 @@ public void OnRender(double deltaTime) ImGui.TextWrapped($"Description: {plugin.Value.Metadata.Description}"); ImGui.Text($"Version: {plugin.Value.Metadata.Version}"); ImGui.Text($"Author: {plugin.Value.Metadata.Author}"); + ImGui.Text("State:"); + ImGui.SameLine(); + switch (state) + { + case PluginState.Unknown: + ImGui.TextDisabled("Unknown"); + break; + case PluginState.Ready: + ImGui.TextColored(new(0, 0.5f, 0, 1), "Ready"); + break; + case PluginState.Loaded: + ImGui.TextColored(new(0, 1, 0, 1), "Loaded"); + break; + case PluginState.Unloaded: + ImGui.TextColored(new(1, 1, 0, 1), "Failed to unload"); + break; + case PluginState.Invalid: + ImGui.TextColored(new(1, 0, 0, 1), "Invalid plugin"); + break; + } if (disabled) ImGui.BeginDisabled(true); if(ImGui.Button((enabled ? "Disable" : "Enable") + $"##{plugin.Key}") && !disabled) From 47f24f517c898b3b20be76b900d4fb7532bc64d6 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 21:23:03 +0100 Subject: [PATCH 05/15] Some stuff --- src/OpenPuppet.SDK/IPlugin.cs | 4 +- src/OpenPuppet.SDK/IPlugin.old.cs | 273 ----------------------- src/OpenPuppet.SDK/OpenPuppet.SDK.csproj | 10 - src/OpenPuppet.SDK/Plugins/Manager.cs | 192 ---------------- src/OpenPuppet/Program.cs | 1 + 5 files changed, 3 insertions(+), 477 deletions(-) delete mode 100644 src/OpenPuppet.SDK/IPlugin.old.cs delete mode 100644 src/OpenPuppet.SDK/Plugins/Manager.cs diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index 36b339f..eff389f 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -258,12 +258,12 @@ public static void LoadPluginDirectory(string path, bool enabled = true) public static void InstallPlugin(Plugin.LocalInstallSource source) { - + // TODO } public static void InstallPlugin(Plugin.InternetInstallSource source) { - + // TODO } public static void UninstallPlugin(string id) diff --git a/src/OpenPuppet.SDK/IPlugin.old.cs b/src/OpenPuppet.SDK/IPlugin.old.cs deleted file mode 100644 index 393d7f5..0000000 --- a/src/OpenPuppet.SDK/IPlugin.old.cs +++ /dev/null @@ -1,273 +0,0 @@ -using ImGuiNET; -using Newtonsoft.Json; -using OpenPuppet.Plugins; -using OpenPuppet.SDK.Events; -using OpenPuppet.SDK.Plugins; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace OpenPuppet -{ - /// - /// Plugin interface - /// - public interface OldIPlugin : IDisposable - { - public static Dictionary RegisteredPlugins { get; } = new(); - - static readonly object _pluginLock = new(); - const int MaxUnloadGcAttempts = 10; - - public static void RegisterPlugin(PluginMetadata metadata, string path, IPlugin? plugin) - { - if (RegisteredPlugins.ContainsKey(metadata.ID)) - throw new ArgumentException($"Plugin with the ID of \"{metadata.ID}\" is already registered"); - RegisteredPlugins[metadata.ID] = new(metadata, path, null, null, plugin, false); - } - public static void EnablePlugin(string registry) - { - if (RegisteredPlugins.ContainsKey(registry)) - { - if (RegisteredPlugins[registry].Plugin != null) return; - - string item = RegisteredPlugins[registry].Path; - - var anycpu = Path.Combine(item, $"anycpu.dll"); - var specific = Path.Combine(item, $"{RuntimeInformation.ProcessArchitecture}.dll"); - - if (File.Exists(specific)) LoadAssembly(specific, registry); - else if (File.Exists(anycpu)) LoadAssembly(anycpu, registry); - else Console.WriteLine($"Warning: could not load {Path.GetFileName(item)}: " + - $"no dll found for architecture '{RuntimeInformation.ProcessArchitecture}'"); - } - else throw new ArgumentException($"Plugin with the ID of\"{registry}\" has not been registered"); - } - public static void DisablePlugin(string registry) - { - if (RegisteredPlugins.ContainsKey(registry)) - { - if (RegisteredPlugins[registry].Plugin == null) return; - - //RegisteredPlugins[registry].Assembly!.OnShutdown(); - RegisteredPlugins[registry].Enabled = false; // Temporary, move to unloading method - UnloadPlugin(registry); - } - else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); - } - public static void RemovePlugin(string registry) - { - if (RegisteredPlugins.ContainsKey(registry)) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Warn, - "Removing plugins has not yet been fully implemented" - ); - // Uninstall - RegisteredPlugins.Remove(registry); - } - else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); - } - - public static bool UnloadPlugin(string registry, bool soft = false) - { - lock (_pluginLock) - { - if (!RegisteredPlugins.TryGetValue(registry, out var plugin)) - throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); - - if (plugin.Plugin == null || - plugin.LoadContext == null || - plugin.WeakReference == null) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Log, - $"Plugin with the ID of \"{registry}\" cannot be unloaded, as it is not loaded" - ); - - RegisteredPlugins.Remove(registry); - return true; - } - - var weakRef = plugin.WeakReference; - var loadContext = plugin.LoadContext; - - try - { - plugin.Plugin.OnShutdown(); - } - catch (Exception ex) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Error, - $"Plugin \"{registry}\" threw during OnShutdown: {ex}" - ); - } - finally - { - plugin.Plugin = null; - } - - try - { - loadContext.Unload(); - } - finally - { - plugin.LoadContext = null; - } - - for (int i = 0; weakRef.IsAlive && i < MaxUnloadGcAttempts; i++) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - } - - bool leaked = weakRef.IsAlive; - - if (leaked) - { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Warn, - $"Could not unload Plugin \"{registry}\", possible reference leak" - ); - - if (soft) - IEvent.Invoke("openpuppet.restart", null, false); - else - IEvent.Invoke("openpuppet.unstable", null, EventArgs.Empty); - } - - //RegisteredPlugins.Remove(registry); - plugin.Enabled = false; - return !leaked; - } - } - - static void LoadAssembly(string path, string registry) - { - if (!RegisteredPlugins.ContainsKey(registry)) - throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); - - // Remove the logs once fixed - - RegisteredPlugins[registry].LoadContext = new PluginLoadContext(path); - RegisteredPlugins[registry].WeakReference = new( - RegisteredPlugins[registry].LoadContext//, - //trackResurrection: true - ); - Assembly asm = RegisteredPlugins[registry].LoadContext!.LoadFromAssemblyPath(path); - asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => - { - RegisteredPlugins[registry].Plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; - RegisteredPlugins[registry].Plugin!.OnInitialized(); - SDK.SDK.logger.WriteLine($"Initialized Plugin {registry}"); - }); - /*foreach(Type t in asm.GetTypes()) - { - if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass) - { - SDK.SDK.logger.WriteLine($"Found IPlugin in Plugin {registry}"); - RegisteredPlugins[registry].Assembly = (IPlugin)Activator.CreateInstance(t)!; - RegisteredPlugins[registry].Assembly!.OnInitialized(); - SDK.SDK.logger.WriteLine($"Initialised Plugin {registry}"); - } - }*/ - - /*var asm = Assembly.LoadFrom(path); - asm.DefinedTypes.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList().ForEach(t => - { - var plugin = (IPlugin)Activator.CreateInstance(t.AsType())!; - //PluginManager.LoadedPlugins.Add(plugin, path); - RegisteredPlugins[registry].Assembly = plugin; - RegisteredPlugins[registry].Enabled = true; - - plugin.OnInitialized(); - });*/ - - RegisteredPlugins[registry].Enabled = true; - } - - /// - /// Load a plugin from a directory. - /// This was previously in Program.cs, but has been moved here so that - /// plugins can be loaded later on. - /// - /// The path of the directory - public static void LoadPluginDirectory(string path, bool enabled = true) - { - if (!Directory.Exists(path)) - throw new ArgumentException($"\"{path}\" does not exist"); - - string meta = Path.Combine(path, "meta.inf"); - if (!File.Exists(meta)) - throw new ArgumentException($"No metadata present in \"{path}\" (\"{meta}\" does not exist)"); - - PluginMetadata? metadata = JsonConvert.DeserializeObject(File.ReadAllText(meta)); - if(metadata == null) - throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); - - RegisterPlugin(metadata, path, null); - if(enabled) EnablePlugin(metadata.ID); - } - - /// - /// The plugin ID - /// - public const string PluginID = "com.unknown.unknown"; - - public SDK.Logger.PluginLogger Logger { get; } - - /// - /// OnInitialized - /// This is called when the plugin is loaded, use this to prepare the plugin - /// - abstract void OnInitialized(); - - /// - /// OnShutdown - /// This is called when the plugin is being unloaded, use this to save any data or configuration - /// - abstract void OnShutdown(); - } - - /// - /// Plugin metadata file - /// - public class PluginMetadata - { - public string ID { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string Version { get; set; } = string.Empty; - public string Icon { get; set; } = string.Empty; - } - - public class RegisteredPlugin - { - public PluginMetadata Metadata { get; set; } - public string Path { get; set; } - public PluginLoadContext? LoadContext { get; set; } - public WeakReference? WeakReference { get; set; } - public IPlugin? Plugin { get; set; } - public bool Enabled { get; set; } - - public RegisteredPlugin( - PluginMetadata metadata, - string path, - PluginLoadContext? loadContext, - WeakReference? weakReference, - IPlugin? assembly, - bool enabled) - { - Metadata = metadata; - Path = path; - LoadContext = loadContext; - WeakReference = weakReference; - Plugin = assembly; - Enabled = enabled; - } - } -} \ No newline at end of file diff --git a/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj b/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj index 1d5c79c..488e755 100644 --- a/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj +++ b/src/OpenPuppet.SDK/OpenPuppet.SDK.csproj @@ -7,16 +7,6 @@ true - - - - - - - - - - diff --git a/src/OpenPuppet.SDK/Plugins/Manager.cs b/src/OpenPuppet.SDK/Plugins/Manager.cs deleted file mode 100644 index fce056b..0000000 --- a/src/OpenPuppet.SDK/Plugins/Manager.cs +++ /dev/null @@ -1,192 +0,0 @@ -using Newtonsoft.Json; -using OpenPuppet.SDK; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace OpenPuppet.Plugins -{ - public static class PluginManager - { - //public static List LoadedPlugins { get; internal set; } = new(); - public static Dictionary Plugins { get; set; } = new(); - public static string? PluginListPath - { - get - { - return Path.Combine(PluginsPath.PluginPath, "plugins.json"); - } - } - - public class PluginItem - { - public string Path { get; set; } - - public bool Enabled { get; set; } - } - - public static void LoadPluginList() - { - if (PluginListPath == null) - throw new Exception("\"PluginListPath\" is null"); - if (File.Exists(PluginListPath)) - { - try - { - var plugins = JsonConvert.DeserializeObject>(File.ReadAllText(PluginListPath)); - if (plugins == null) - { - SDK.SDK.logger.WriteLine( - Logger.ILogger.Level.Warn, - "Plugins list is null, rewriting with default values. " + - "The user will need to reinstall plugins." - ); - - GenerateDefaultPluginList(); - SavePluginList(); - } - else - { - foreach (var plugin in plugins) - Plugins[plugin.Key] = plugin.Value; - } - } - catch (Exception ex) - { - SDK.SDK.logger.WriteLine( - Logger.ILogger.Level.Error, - $"Failed to load plugins list: " + ex.Message - ); - SDK.SDK.logger.WriteLine( - Logger.ILogger.Level.Warn, - "Rewriting plugin list with default values. " + - "The user will need to reinstall plugins." - ); - - GenerateDefaultPluginList(); - SavePluginList(); - } - } - else - { - GenerateDefaultPluginList(); - SavePluginList(); - } - } - - public static void SavePluginList() - { - if (PluginListPath == null) - throw new Exception("\"PluginListPath\" is null"); - File.WriteAllText( - PluginListPath, - JsonConvert.SerializeObject(Plugins, Formatting.Indented) - ); - } - - public static void GenerateDefaultPluginList() - { - if (Plugins.Count != 0) - { - // For now just print a warning, and then end - // In the future, this should probably throw an exception. - // This seems to get triggered at some point during the loading, even - // when the file is in fact valid. - - SDK.SDK.logger.WriteLine( - Logger.ILogger.Level.Warn, - "Refusing to generate default plugin list: Plugins already exist in the list" - ); - - return; - } - - // This just puts the currently available/loaded plugins into the list - foreach (var item in Directory.GetDirectories(PluginsPath.PluginPath!)) - { - string meta = Path.Combine(item, "meta.inf"); - if (!File.Exists(meta)) - throw new ArgumentException($"No metadata present in \"{item}\" (\"{meta}\" does not exist)"); - - PluginMetadata? metadata = JsonConvert.DeserializeObject(File.ReadAllText(meta)); - if (metadata == null) - throw new ArgumentException($"Metadata in \"{meta}\" is invalid"); - - Plugins.Add( - metadata.ID, - new PluginItem - { - Path = item, - Enabled = true - } - ); - } - } - - /// - /// Installs a plugin from a path, the path can either be a directory - /// or a ZIP file. - /// - /// The path to directory/ZIP file - public static InstallPluginResult InstallPlugin(string path) - { - if (Directory.Exists(path)) return InstallPluginDir(path); - else return InstallPluginArchive(path); - } - - static internal InstallPluginResult InstallPluginDir(string path) - { - throw new NotImplementedException(); - } - - static internal InstallPluginResult InstallPluginArchive(string path) - { - throw new NotImplementedException(); - } - - public class InstallPluginResult - { - public int InstalledPlugins; - public int FailedPlugins; - public int TotalPlugins; - - public string[] Logs = []; - } - - public static void SetPluginEnabled(string ID, bool enabled) - { - if (enabled) IPlugin.EnablePlugin(ID); - else IPlugin.DisablePlugin(ID); - - if (Plugins.ContainsKey(ID)) - { - Plugins[ID].Enabled = enabled; - } - else - { - SDK.SDK.logger.WriteLine( - Logger.ILogger.Level.Warn, - "Plugin with ID \"{ID}\" is not present in the plugin list. " + - "The plugin list, and the currently loaded plugins may be desynced." - ); - } - } - - public static void UninstallPlugin(string ID) - { - IPlugin.DisablePlugin(ID); - //IPlugin.RemovePlugin(ID); - if (Plugins.ContainsKey(ID)) - { - Directory.Delete(Plugins[ID].Path, true); - Plugins.Remove(ID); - } - } - } -} \ No newline at end of file diff --git a/src/OpenPuppet/Program.cs b/src/OpenPuppet/Program.cs index 9c86bf4..5e2675a 100644 --- a/src/OpenPuppet/Program.cs +++ b/src/OpenPuppet/Program.cs @@ -371,6 +371,7 @@ static void Closing() static void LoadPlugins() { IPlugin.LoadPluginList(); + PluginEvents.InvokeFinishedLoading(null); // TODO: Clean up below, make sure that nothing important is left /* From ba5a0933b7ae4ad13396862a60fc1a93ee119e2b Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 21:29:54 +0100 Subject: [PATCH 06/15] Various fixes --- src/OpenPuppet.SDK/IPlugin.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index eff389f..b057aa9 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -87,7 +87,7 @@ public static void EnablePlugin(string id) LoadPlugin(id, plugin.Path); } - else throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + else throw new ArgumentException($"Plugin with the ID of \"{id}\" has not been registered"); } public static void DisablePlugin(string id) @@ -98,7 +98,7 @@ public static void DisablePlugin(string id) SavePluginList(); UnloadPlugin(id); } - else throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + else throw new ArgumentException($"Plugin with the ID of \"{id}\" has not been registered"); } public static void SetPluginEnabled(string ID, bool enabled) @@ -135,7 +135,7 @@ public static void UnloadPlugin(string id, bool soft = false) lock(_pluginLock) { if(!RegisteredPlugins.TryGetValue(id, out var plugin)) - throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + throw new ArgumentException($"Plugin with the ID of \"{id}\" has not been registered"); if(plugin.State == PluginState.Loaded && plugin.Plugin != null && @@ -209,7 +209,7 @@ public static void UnloadPlugin(string id, bool soft = false) static void LoadAssembly(string path, string id) { if (!RegisteredPlugins.TryGetValue(id, out var plugin)) - throw new ArgumentException($"Plugin with the ID of \"{plugin}\" hsa not been registered"); + throw new ArgumentException($"Plugin with the ID of \"{id}\" hsa not been registered"); plugin.LoadContext = new PluginLoadContext(path); plugin.WeakReference = new(plugin.LoadContext); From e6f48e8812f28f4c6b3ce7c08d40f0835681b72e Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 21:39:37 +0100 Subject: [PATCH 07/15] :fishsticks: --- src/OpenPuppet.SDK/IPlugin.cs | 7 ++----- src/OpenPuppet/Program.cs | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index b057aa9..924d3d4 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -1,10 +1,7 @@ -using Microsoft.Win32; -using Newtonsoft.Json; +using Newtonsoft.Json; using OpenPuppet.SDK.Events; using OpenPuppet.SDK.Plugin; using OpenPuppet.SDK.Plugins; -using System.ComponentModel.Design; -using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -298,7 +295,7 @@ public static void UninstallPlugin(string id) SavePluginList(); } } - else throw new ArgumentException($"Plugin with the ID of \"{plugin}\" has not been registered"); + else throw new ArgumentException($"Plugin with the ID of \"{id}\" has not been registered"); } static string SafePluginName(string name) diff --git a/src/OpenPuppet/Program.cs b/src/OpenPuppet/Program.cs index 5e2675a..029daf7 100644 --- a/src/OpenPuppet/Program.cs +++ b/src/OpenPuppet/Program.cs @@ -10,7 +10,6 @@ using Silk.NET.OpenGL.Extensions.ImGui; using Silk.NET.Windowing; using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; From 15a9564d3b81e8621e863e37d01779d0e83dbd31 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 21:52:25 +0100 Subject: [PATCH 08/15] Fixed some tests --- src/OpenPuppet.SDK/IPlugin.cs | 4 ++-- src/OpenPuppet.Tests/Plugins/Path.cs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index 924d3d4..e5c146e 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -508,12 +508,12 @@ public class PluginItem public class LocalInstallSource { - + public static string Path { get; set; } } public class InternetInstallSource { - + public static string URL { get; set; } } } } \ No newline at end of file diff --git a/src/OpenPuppet.Tests/Plugins/Path.cs b/src/OpenPuppet.Tests/Plugins/Path.cs index fb939c3..e360d52 100644 --- a/src/OpenPuppet.Tests/Plugins/Path.cs +++ b/src/OpenPuppet.Tests/Plugins/Path.cs @@ -1,5 +1,4 @@ -using OpenPuppet.Plugins; -using System.Xml.Linq; +using OpenPuppet.SDK; namespace OpenPuppet.Tests.Plugins { @@ -10,7 +9,7 @@ public void TestPluginName() { Assert.NotEqual( "a-b-c-d", - PluginsPath.SafePluginName( + IPlugin.SafePluginName( @"a B `¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,./<>?\| C d" ) @@ -21,8 +20,8 @@ public void TestPluginName() public void TestPluginPath() { Assert.Equal( - System.IO.Path.Combine(PluginsPath.PluginPath!, PluginsPath.SafePluginName("a-b-c-d")), - PluginsPath.GetPluginPath( + System.IO.Path.Combine(IPlugin.PluginPath!, IPlugin.SafePluginName("a-b-c-d")), + IPlugin.GetPluginPath( @"a B `¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,./<>?\| C d" ) From 6889c52d0dc60f0c8620d9c9937976ebb1c270ef Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 21:55:40 +0100 Subject: [PATCH 09/15] More test fixes --- src/OpenPuppet.Tests/Plugins/Path.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenPuppet.Tests/Plugins/Path.cs b/src/OpenPuppet.Tests/Plugins/Path.cs index e360d52..0792af6 100644 --- a/src/OpenPuppet.Tests/Plugins/Path.cs +++ b/src/OpenPuppet.Tests/Plugins/Path.cs @@ -11,7 +11,7 @@ public void TestPluginName() "a-b-c-d", IPlugin.SafePluginName( @"a B -`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,./<>?\| C d" +`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,/<>?\| C d" ) ); } @@ -23,7 +23,7 @@ public void TestPluginPath() System.IO.Path.Combine(IPlugin.PluginPath!, IPlugin.SafePluginName("a-b-c-d")), IPlugin.GetPluginPath( @"a B -`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,./<>?\| C d" +`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,/<>?\| C d" ) ); } From 2b6d0e12e79b4902b5ac56958bd6d0c924a5aeef Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 21:59:58 +0100 Subject: [PATCH 10/15] Added an extra test because why not --- src/OpenPuppet.Tests/Plugins/Path.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/OpenPuppet.Tests/Plugins/Path.cs b/src/OpenPuppet.Tests/Plugins/Path.cs index 0792af6..51e3f89 100644 --- a/src/OpenPuppet.Tests/Plugins/Path.cs +++ b/src/OpenPuppet.Tests/Plugins/Path.cs @@ -4,6 +4,12 @@ namespace OpenPuppet.Tests.Plugins { public class Path { + [Fact] + public void TestIPluginPluginPath() + { + Assert.NotNull(IPlugin.PluginPath); + } + [Fact] public void TestPluginName() { From 54882504de533058fb61917f287e2493c843c1fb Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 22:10:51 +0100 Subject: [PATCH 11/15] Data structure and even more tests --- src/OpenPuppet.SDK/IPlugin.cs | 29 +++++++++++++++++++++++----- src/OpenPuppet.Tests/Plugins/Path.cs | 13 +++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index e5c146e..ec7c073 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -253,12 +253,12 @@ public static void LoadPluginDirectory(string path, bool enabled = true) if (enabled) EnablePlugin(metadata.ID); } - public static void InstallPlugin(Plugin.LocalInstallSource source) + public static void InstallPlugin(LocalInstallSource source) { // TODO } - public static void InstallPlugin(Plugin.InternetInstallSource source) + public static void InstallPlugin(InternetInstallSource source) { // TODO } @@ -502,18 +502,37 @@ namespace Plugin public class PluginItem { public string Path { get; set; } - public bool Enabled { get; set; } } public class LocalInstallSource { - public static string Path { get; set; } + public string Path { get; set; } + public bool IsArchive { get; set; } + + public LocalInstallSource(string path, bool isArchive = false) + { + Path = path; + IsArchive = isArchive; + } + + public LocalInstallSource(InternetInstallSource IIS) + { + Path = IIS.Path; + IsArchive = true; + } } public class InternetInstallSource { - public static string URL { get; set; } + public string URL { get; set; } + public string Path { get; set; } + + public InternetInstallSource(string url, string path) + { + URL = url; + Path = path; + } } } } \ No newline at end of file diff --git a/src/OpenPuppet.Tests/Plugins/Path.cs b/src/OpenPuppet.Tests/Plugins/Path.cs index 51e3f89..52df212 100644 --- a/src/OpenPuppet.Tests/Plugins/Path.cs +++ b/src/OpenPuppet.Tests/Plugins/Path.cs @@ -33,5 +33,18 @@ public void TestPluginPath() ) ); } + + [Fact] + public void TestInternetSourceToLocalSourceConversion() + { + SDK.Plugin.InternetInstallSource internet = new( + "https://example.com/plugin.zip", + "plugin.zip" + ); + + SDK.Plugin.LocalInstallSource local = new(internet); + + Assert.Equal(internet.Path, local.Path); + } } } \ No newline at end of file From a0fa885b208732988639bdcf521e499fb103933c Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jul 2026 22:25:42 +0100 Subject: [PATCH 12/15] Removed forceful exit in Closing --- src/OpenPuppet/Program.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/OpenPuppet/Program.cs b/src/OpenPuppet/Program.cs index 029daf7..10957a4 100644 --- a/src/OpenPuppet/Program.cs +++ b/src/OpenPuppet/Program.cs @@ -361,10 +361,6 @@ static void Closing() SDK.SDK.DestroyLogger(); logger.Dispose(); - - // Some plugins may still be loaded if there were reference leaks, - // so just forcefully exit - Environment.Exit(0); } static void LoadPlugins() From 40f3592ca88e242e19ad6ad3d3ab8d0b0f7f1d36 Mon Sep 17 00:00:00 2001 From: William Dawson Date: Fri, 24 Jul 2026 15:57:09 +0100 Subject: [PATCH 13/15] Fixed small bug --- src/OpenPuppet.SDK/IPlugin.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index ec7c073..85e00ec 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -282,7 +282,7 @@ public static void UninstallPlugin(string id) try { - Directory.Delete(id); + Directory.Delete(plugin.Path); } catch(Exception ex) { SDK.logger.WriteLine( @@ -490,6 +490,7 @@ public RegisteredPlugin( Metadata = metadata; Path = path; State = PluginState.Unknown; + Enabled = enabled; LoadContext = loadContext; WeakReference = weakReference; From 724d5d2cd191bb000437f9f16e8bbbd00fad22fa Mon Sep 17 00:00:00 2001 From: William Dawson Date: Fri, 24 Jul 2026 16:03:21 +0100 Subject: [PATCH 14/15] Added a message to one of the errors --- src/OpenPuppet.SDK/IPlugin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index 85e00ec..ce15617 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -227,11 +227,11 @@ static void LoadAssembly(string path, string id) plugin.OnInitialized(); });*/ - } catch + } catch(Exception ex) { { SDK.logger.WriteLine( ILogger.Level.Warn, - $"Failed to load plugin with ID \"{id}\"" + $"Failed to load plugin with ID \"{id}\": {ex.Message}" ); UnloadPlugin(id); } From c38244c7da60813fc030a2482f07c5d050dca061 Mon Sep 17 00:00:00 2001 From: William Dawson Date: Fri, 24 Jul 2026 16:04:58 +0100 Subject: [PATCH 15/15] :rage1: --- src/OpenPuppet.SDK/IPlugin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index ce15617..26d255d 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -228,7 +228,6 @@ static void LoadAssembly(string path, string id) plugin.OnInitialized(); });*/ } catch(Exception ex) { - { SDK.logger.WriteLine( ILogger.Level.Warn, $"Failed to load plugin with ID \"{id}\": {ex.Message}"