diff --git a/src/OpenPuppet.SDK/IPlugin.cs b/src/OpenPuppet.SDK/IPlugin.cs index dbf48d5..26d255d 100644 --- a/src/OpenPuppet.SDK/IPlugin.cs +++ b/src/OpenPuppet.SDK/IPlugin.cs @@ -1,273 +1,538 @@ -using ImGuiNET; -using Newtonsoft.Json; -using OpenPuppet.Plugins; +using Newtonsoft.Json; using OpenPuppet.SDK.Events; +using OpenPuppet.SDK.Plugin; using OpenPuppet.SDK.Plugins; -using System.Collections.Generic; 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 + */ + + public static Dictionary RegisteredPlugins { get; } = []; static readonly object _pluginLock = new(); - const int MaxUnloadGcAttempts = 10; - - public static void RegisterPlugin(PluginMetadata metadata, string path, IPlugin? plugin) + static int MaxUnloadGCAttempts = 10; + public static string? InstallPath { - 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); + get + { + return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + } + static string PluginPath + { + get + { + return Path.Combine(InstallPath!, "Plugins"); + } } - public static void EnablePlugin(string registry) + static string PluginListPath { - if (RegisteredPlugins.ContainsKey(registry)) + get { - if (RegisteredPlugins[registry].Plugin != null) return; + return Path.Combine(PluginPath!, "plugins.json"); + } + } - string item = RegisteredPlugins[registry].Path; + public const string PluginID = ""; + public PluginLogger Logger { get; } - var anycpu = Path.Combine(item, $"anycpu.dll"); - var specific = Path.Combine(item, $"{RuntimeInformation.ProcessArchitecture}.dll"); + 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}\" has already 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}'"); - } - else throw new ArgumentException($"Plugin with the ID of\"{registry}\" has not been registered"); + RegisteredPlugins.Add(metadata.ID, new(metadata, path, enabled)); } - public static void DisablePlugin(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(); - //RegisteredPlugins[registry].Assembly!.OnShutdown(); - RegisteredPlugins[registry].Enabled = false; // Temporary, move to unloading method - UnloadPlugin(registry); + LoadPlugin(id, plugin.Path); } - else throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + else throw new ArgumentException($"Plugin with the ID of \"{id}\" has not been registered"); } + + 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 \"{id}\" has not been registered"); + } + + 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) { - if (RegisteredPlugins.ContainsKey(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 { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Warn, - "Removing plugins has not yet been fully implemented" - ); - // Uninstall - RegisteredPlugins.Remove(registry); + // 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 bool UnloadPlugin(string registry, bool soft = false) + public static void UnloadPlugin(string id, bool soft = false) { - lock (_pluginLock) + lock(_pluginLock) { - if (!RegisteredPlugins.TryGetValue(registry, out var plugin)) - throw new ArgumentException($"Plugin with the ID of \"{registry}\" has not been registered"); + if(!RegisteredPlugins.TryGetValue(id, out var plugin)) + throw new ArgumentException($"Plugin with the ID of \"{id}\" has not been registered"); - if (plugin.Plugin == null || - plugin.LoadContext == null || - plugin.WeakReference == null) + if(plugin.State == PluginState.Loaded && + 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" + 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" ); - - RegisteredPlugins.Remove(registry); - return true; } + } + } - var weakRef = plugin.WeakReference; - var loadContext = plugin.LoadContext; + static void LoadAssembly(string path, string id) + { + if (!RegisteredPlugins.TryGetValue(id, out var plugin)) + throw new ArgumentException($"Plugin with the ID of \"{id}\" hsa not been registered"); - try - { - plugin.Plugin.OnShutdown(); - } - catch (Exception ex) + 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 => { - SDK.SDK.logger.WriteLine( - SDK.Logger.ILogger.Level.Error, - $"Plugin \"{registry}\" threw during OnShutdown: {ex}" - ); - } - finally + var p = (IPlugin)Activator.CreateInstance(t.AsType())!; + //PluginManager.LoadedPlugins.Add(plugin, path); + plugin.Plugin = p; + plugin.State = PluginState.Loaded; + + plugin.OnInitialized(); + });*/ + } catch(Exception ex) { + SDK.logger.WriteLine( + ILogger.Level.Warn, + $"Failed to load plugin with ID \"{id}\": {ex.Message}" + ); + UnloadPlugin(id); + } + } + + 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)) + ?? 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) EnablePlugin(metadata.ID); + } + + public static void InstallPlugin(LocalInstallSource source) + { + // TODO + } + + public static void InstallPlugin(InternetInstallSource source) + { + // TODO + } + + 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(plugin.Path); + } 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 \"{id}\" 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); + foreach (var plugin in plugins) + LoadPluginDirectory(plugin.Value.Path, plugin.Value.Enabled); + } - 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; + Enabled = enabled; + 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 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 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.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/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.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.Tests/Plugins/Path.cs b/src/OpenPuppet.Tests/Plugins/Path.cs index fb939c3..52df212 100644 --- a/src/OpenPuppet.Tests/Plugins/Path.cs +++ b/src/OpenPuppet.Tests/Plugins/Path.cs @@ -1,18 +1,23 @@ -using OpenPuppet.Plugins; -using System.Xml.Linq; +using OpenPuppet.SDK; namespace OpenPuppet.Tests.Plugins { public class Path { + [Fact] + public void TestIPluginPluginPath() + { + Assert.NotNull(IPlugin.PluginPath); + } + [Fact] public void TestPluginName() { Assert.NotEqual( "a-b-c-d", - PluginsPath.SafePluginName( + IPlugin.SafePluginName( @"a B -`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,./<>?\| C d" +`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,/<>?\| C d" ) ); } @@ -21,12 +26,25 @@ 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" +`¦¬!" + '"' + @"@£$%^&*()_+-=[]{};:'@#~,/<>?\| C d" ) ); } + + [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 diff --git a/src/OpenPuppet/Program.cs b/src/OpenPuppet/Program.cs index 039870b..10957a4 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; @@ -11,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; @@ -62,6 +60,9 @@ static void Main(string[] args) } } + if (IPlugin.InstallPath == null) + throw new Exception("\"Plugin.InstallPath\" is null"); + startupSW = new(); startupSW.Start(); @@ -343,7 +344,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 @@ -360,17 +361,18 @@ 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() { - PluginManager.LoadPluginList(); + IPlugin.LoadPluginList(); + PluginEvents.InvokeFinishedLoading(null); + + // 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 +383,7 @@ static void LoadPlugins() errors.Add(ex.Message); logger.WriteLine(ex.Message); } - }*/ + }*\ foreach(var item in PluginManager.Plugins) { @@ -398,6 +400,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..32f93f4 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); @@ -101,17 +101,37 @@ 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) { - 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)