initial commit
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using Pulsar.Server.Networking;
|
||||
|
||||
namespace Pulsar.Server.Plugins
|
||||
{
|
||||
public interface IServerContext
|
||||
{
|
||||
Form MainForm { get; }
|
||||
PulsarServer Server { get; }
|
||||
void Log(string message);
|
||||
void AddClientContextMenuItem(string text, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItem(string text, Icon icon, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItem(string section, string text, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItem(string section, string text, Icon icon, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItem(string[] sections, string text, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItem(string[] sections, string text, Icon icon, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItemPath(string path, string text, Action<IReadOnlyList<Client>> onClick);
|
||||
void AddClientContextMenuItemPath(string path, string text, Icon icon, Action<IReadOnlyList<Client>> onClick);
|
||||
void ApplyTheme(Action<Form> apply);
|
||||
void ClearPluginMenuItems();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Pulsar.Server.Plugins
|
||||
{
|
||||
public interface IServerPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
Version Version { get; }
|
||||
string Description { get; }
|
||||
string Type { get; }
|
||||
void Initialize(IServerContext context);
|
||||
bool AutoLoadToClients => false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Server.Plugins
|
||||
{
|
||||
public interface IUIExtensionPlugin : IServerPlugin
|
||||
{
|
||||
TabPage[] CreateCustomTabs();
|
||||
ToolStripItem[] CreateToolbarItems();
|
||||
ToolStripMenuItem[] CreateMenuItems();
|
||||
void CustomizeForm(Form form);
|
||||
void CustomizeControl(Control control);
|
||||
Form CreateCustomMainForm();
|
||||
bool ShouldReplaceMainForm { get; }
|
||||
int UIPriority { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Pulsar.Server.Plugins
|
||||
{
|
||||
public sealed class PluginManager : IDisposable
|
||||
{
|
||||
private readonly IServerContext _context;
|
||||
private readonly List<IServerPlugin> _plugins = new List<IServerPlugin>();
|
||||
private FileSystemWatcher _watcher;
|
||||
private readonly object _lock = new object();
|
||||
public event EventHandler PluginsChanged;
|
||||
|
||||
public PluginManager(IServerContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IServerPlugin> Plugins => _plugins;
|
||||
|
||||
public void LoadFrom(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
_context.Log("Created plugin directory: " + folder);
|
||||
}
|
||||
|
||||
var enabledDlls = Directory.EnumerateFiles(folder, "*.dll", SearchOption.TopDirectoryOnly)
|
||||
.Where(f => !f.EndsWith(".disabled", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => !IsClientPluginFile(f))
|
||||
.OrderBy(Path.GetFileName)
|
||||
.ToList();
|
||||
|
||||
_context.Log($"Found {enabledDlls.Count} enabled DLL files in: {folder}");
|
||||
|
||||
foreach (var dll in enabledDlls)
|
||||
{
|
||||
_context.Log("Attempting to load: " + Path.GetFileName(dll));
|
||||
TryLoadDll(dll);
|
||||
}
|
||||
|
||||
_context.Log($"Loaded {_plugins.Count} plugins successfully");
|
||||
StartWatcher(folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log("PluginManager error: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartWatcher(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
_watcher = new FileSystemWatcher(folder);
|
||||
_watcher.Filter = "*.dll*";
|
||||
_watcher.IncludeSubdirectories = false;
|
||||
_watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime;
|
||||
_watcher.Created += OnFileChanged;
|
||||
_watcher.Changed += OnFileChanged;
|
||||
_watcher.Deleted += OnFileChanged;
|
||||
_watcher.Renamed += OnFileRenamed;
|
||||
_watcher.Error += OnWatcherError;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
_context.Log("Plugin watcher started for: " + folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log("Plugin watcher error: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (IsClientPluginFile(e.FullPath))
|
||||
{
|
||||
_context.Log($"Ignoring client plugin file {e.ChangeType}: {e.Name}");
|
||||
return;
|
||||
}
|
||||
_context.Log($"File {e.ChangeType}: {e.Name}");
|
||||
Task.Delay(500).ContinueWith(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ReloadPlugins();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log($"Plugin reload error: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnFileRenamed(object sender, RenamedEventArgs e)
|
||||
{
|
||||
if (IsClientPluginFile(e.FullPath) && IsClientPluginFile(e.OldFullPath))
|
||||
{
|
||||
_context.Log($"Ignoring client plugin rename: {e.OldName} -> {e.Name}");
|
||||
return;
|
||||
}
|
||||
_context.Log($"File renamed: {e.OldName} -> {e.Name}");
|
||||
Task.Delay(500).ContinueWith(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ReloadPlugins();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log($"Plugin reload error: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnWatcherError(object sender, ErrorEventArgs e)
|
||||
{
|
||||
_context.Log($"Plugin watcher error: {e.GetException().Message}");
|
||||
try
|
||||
{
|
||||
_watcher?.Dispose();
|
||||
var folder = Path.GetDirectoryName(_watcher?.Path ?? "");
|
||||
if (!string.IsNullOrEmpty(folder))
|
||||
{
|
||||
StartWatcher(folder);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log($"Failed to restart watcher: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadPlugins()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
var folder = Path.GetDirectoryName(_watcher.Path);
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
_context.Log("Plugin directory does not exist: " + folder);
|
||||
return;
|
||||
}
|
||||
|
||||
var enabledDlls = Directory.EnumerateFiles(folder, "*.dll", SearchOption.TopDirectoryOnly)
|
||||
.Where(f => !f.EndsWith(".disabled", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => !IsClientPluginFile(f))
|
||||
.OrderBy(Path.GetFileName)
|
||||
.ToList();
|
||||
|
||||
var currentPluginNames = _plugins.Select(p => p.Name).ToHashSet();
|
||||
var currentDllNames = enabledDlls.Select(Path.GetFileName).ToHashSet();
|
||||
var newPlugins = new List<string>();
|
||||
var removedPlugins = new List<string>();
|
||||
|
||||
var pluginsToRemove = _plugins.Where(p =>
|
||||
{
|
||||
var dllName = p.GetType().Assembly.GetName().Name + ".dll";
|
||||
return !currentDllNames.Contains(dllName);
|
||||
}).ToList();
|
||||
|
||||
foreach (var plugin in pluginsToRemove)
|
||||
{
|
||||
removedPlugins.Add(plugin.Name);
|
||||
|
||||
if (plugin is IUIExtensionPlugin uiPlugin)
|
||||
{
|
||||
UIExtensionManager.UnregisterUIExtension(uiPlugin);
|
||||
_context.Log("Unregistered UI extension: " + plugin.Name);
|
||||
}
|
||||
|
||||
_plugins.Remove(plugin);
|
||||
_context.Log($"Plugin removed: {plugin.Name}");
|
||||
}
|
||||
|
||||
foreach (var dll in enabledDlls)
|
||||
{
|
||||
var pluginName = TryLoadDll(dll);
|
||||
if (!string.IsNullOrEmpty(pluginName) && !currentPluginNames.Contains(pluginName))
|
||||
{
|
||||
newPlugins.Add(pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
if (newPlugins.Count > 0 || removedPlugins.Count > 0)
|
||||
{
|
||||
var changes = new List<string>();
|
||||
|
||||
if (newPlugins.Count > 0)
|
||||
{
|
||||
var pluginList = string.Join(", ", newPlugins.Take(5));
|
||||
var moreText = newPlugins.Count > 5 ? $" and {newPlugins.Count - 5} more" : "";
|
||||
changes.Add($"Added {newPlugins.Count} plugin{(newPlugins.Count > 1 ? "s" : "")}: {pluginList}{moreText}");
|
||||
}
|
||||
|
||||
if (removedPlugins.Count > 0)
|
||||
{
|
||||
var removedList = string.Join(", ", removedPlugins.Take(5));
|
||||
var moreRemoved = removedPlugins.Count > 5 ? $" and {removedPlugins.Count - 5} more" : "";
|
||||
changes.Add($"Removed {removedPlugins.Count} plugin{(removedPlugins.Count > 1 ? "s" : "")}: {removedList}{moreRemoved}");
|
||||
}
|
||||
|
||||
_context.Log(string.Join("; ", changes));
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.Log($"Plugin scan complete: {_plugins.Count} plugins loaded");
|
||||
}
|
||||
|
||||
PluginsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log("Plugin reload error: " + ex.Message);
|
||||
PluginsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string TryLoadDll(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsClientPluginFile(path))
|
||||
{
|
||||
_context.Log($"Skipping client plugin assembly for server load: {Path.GetFileName(path)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var dllName = Path.GetFileName(path);
|
||||
var alreadyLoaded = _plugins.Any(p =>
|
||||
{
|
||||
var loadedDllName = p.GetType().Assembly.GetName().Name + ".dll";
|
||||
return string.Equals(loadedDllName, dllName, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
if (alreadyLoaded)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var asm = Assembly.LoadFrom(path);
|
||||
var types = asm.GetTypes().Where(t => !t.IsAbstract && typeof(IServerPlugin).IsAssignableFrom(t));
|
||||
string loadedPlugin = null;
|
||||
foreach (var t in types)
|
||||
{
|
||||
var pluginName = TryInit(t, Path.GetFileName(path));
|
||||
if (!string.IsNullOrEmpty(pluginName))
|
||||
loadedPlugin = pluginName;
|
||||
}
|
||||
return loadedPlugin;
|
||||
}
|
||||
catch (ReflectionTypeLoadException rtle)
|
||||
{
|
||||
_context.Log("Plugin load error: " + rtle.Message);
|
||||
foreach (var e in rtle.LoaderExceptions)
|
||||
_context.Log(" " + e?.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log("Plugin load error: " + ex.Message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string TryInit(Type t, string source)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Activator.CreateInstance(t) is IServerPlugin p)
|
||||
{
|
||||
p.Initialize(_context);
|
||||
_plugins.Add(p);
|
||||
_context.Log("Loaded plugin '" + p.Name + "' " + p.Version + " from " + source);
|
||||
|
||||
if (p is IUIExtensionPlugin uiPlugin)
|
||||
{
|
||||
UIExtensionManager.RegisterUIExtension(uiPlugin);
|
||||
_context.Log("Registered UI extension: " + p.Name);
|
||||
}
|
||||
|
||||
PluginsChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
return p.Name;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_context.Log("Plugin init failed: " + ex.Message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var plugin in _plugins)
|
||||
{
|
||||
if (plugin is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
_plugins.Clear();
|
||||
_watcher?.Dispose();
|
||||
}
|
||||
|
||||
private static bool IsClientPluginFile(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return false;
|
||||
var fileName = Path.GetFileName(path);
|
||||
return fileName != null && fileName.EndsWith(".Client.dll", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Server.Plugins
|
||||
{
|
||||
public static class UIExtensionManager
|
||||
{
|
||||
private static readonly List<IUIExtensionPlugin> _uiPlugins = new List<IUIExtensionPlugin>();
|
||||
private static Form _customMainForm;
|
||||
|
||||
public static void RegisterUIExtension(IUIExtensionPlugin plugin)
|
||||
{
|
||||
if (plugin != null && !_uiPlugins.Contains(plugin))
|
||||
{
|
||||
_uiPlugins.Add(plugin);
|
||||
_uiPlugins.Sort((a, b) => a.UIPriority.CompareTo(b.UIPriority));
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnregisterUIExtension(IUIExtensionPlugin plugin)
|
||||
{
|
||||
_uiPlugins.Remove(plugin);
|
||||
}
|
||||
|
||||
public static Form GetCustomMainForm()
|
||||
{
|
||||
if (_customMainForm != null)
|
||||
return _customMainForm;
|
||||
|
||||
var replacementPlugin = _uiPlugins
|
||||
.Where(p => p.ShouldReplaceMainForm)
|
||||
.OrderByDescending(p => p.UIPriority)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (replacementPlugin != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_customMainForm = replacementPlugin.CreateCustomMainForm();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error creating custom main form: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return _customMainForm;
|
||||
}
|
||||
|
||||
public static void ApplyFormCustomizations(Form form)
|
||||
{
|
||||
if (form == null) return;
|
||||
|
||||
foreach (var plugin in _uiPlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.CustomizeForm(form);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error applying form customizations: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyControlCustomizations(Control control)
|
||||
{
|
||||
if (control == null) return;
|
||||
|
||||
foreach (var plugin in _uiPlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.CustomizeControl(control);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error applying control customizations: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Control child in control.Controls)
|
||||
{
|
||||
ApplyControlCustomizations(child);
|
||||
}
|
||||
}
|
||||
|
||||
public static TabPage[] GetCustomTabs()
|
||||
{
|
||||
var tabs = new List<TabPage>();
|
||||
|
||||
foreach (var plugin in _uiPlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pluginTabs = plugin.CreateCustomTabs();
|
||||
if (pluginTabs != null)
|
||||
tabs.AddRange(pluginTabs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error getting custom tabs: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return tabs.ToArray();
|
||||
}
|
||||
|
||||
public static ToolStripItem[] GetCustomToolbarItems()
|
||||
{
|
||||
var items = new List<ToolStripItem>();
|
||||
|
||||
foreach (var plugin in _uiPlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pluginItems = plugin.CreateToolbarItems();
|
||||
if (pluginItems != null)
|
||||
items.AddRange(pluginItems);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error getting custom toolbar items: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
public static ToolStripMenuItem[] GetCustomMenuItems()
|
||||
{
|
||||
var items = new List<ToolStripMenuItem>();
|
||||
|
||||
foreach (var plugin in _uiPlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pluginItems = plugin.CreateMenuItems();
|
||||
if (pluginItems != null)
|
||||
items.AddRange(pluginItems);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error getting custom menu items: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
public static void ClearExtensions()
|
||||
{
|
||||
_uiPlugins.Clear();
|
||||
_customMainForm?.Dispose();
|
||||
_customMainForm = null;
|
||||
}
|
||||
|
||||
public static IUIExtensionPlugin[] GetUIPlugins()
|
||||
{
|
||||
return _uiPlugins.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user