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)