diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 319045eda..da854605b 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -1,485 +1,586 @@ using System.Reflection; using System.Runtime.Loader; +using System.Collections.Concurrent; using McMaster.NETCore.Plugins; using Microsoft.Extensions.Logging; +using SwiftlyS2.Shared; using SwiftlyS2.Core.Natives; -using SwiftlyS2.Core.Modules.Plugins; using SwiftlyS2.Core.Services; -using SwiftlyS2.Shared; using SwiftlyS2.Shared.Plugins; +using SwiftlyS2.Core.Modules.Plugins; namespace SwiftlyS2.Core.Plugins; internal class PluginManager { - private IServiceProvider _Provider { get; init; } - private RootDirService _RootDirService { get; init; } - private ILogger _Logger { get; init; } - private List _Plugins { get; } = new(); - private FileSystemWatcher? _Watcher { get; set; } - private InterfaceManager _InterfaceManager { get; set; } = new(); - private List _SharedTypes { get; set; } = new(); - private DataDirectoryService _DataDirectoryService { get; init; } - private DateTime lastRead = DateTime.MinValue; - private readonly HashSet reloadingPlugins = new(); + private readonly IServiceProvider rootProvider; + private readonly RootDirService rootDirService; + private readonly DataDirectoryService dataDirectoryService; + private readonly ILogger logger; + + private readonly InterfaceManager interfaceManager; + private readonly List sharedTypes; + private readonly List plugins; + private readonly ConcurrentDictionary fileLastChange; + private readonly ConcurrentDictionary fileReloadTokens; + + private readonly FileSystemWatcher? fileWatcher; public PluginManager( - IServiceProvider provider, - ILogger logger, - RootDirService rootDirService, - DataDirectoryService dataDirectoryService + IServiceProvider provider, + ILogger logger, + RootDirService rootDirService, + DataDirectoryService dataDirectoryService ) { - _Provider = provider; - _RootDirService = rootDirService; - _Logger = logger; - _DataDirectoryService = dataDirectoryService; - _Watcher = new FileSystemWatcher { + this.rootProvider = provider; + this.rootDirService = rootDirService; + this.dataDirectoryService = dataDirectoryService; + this.logger = logger; + + this.interfaceManager = new(); + this.sharedTypes = []; + this.plugins = []; + this.fileLastChange = new ConcurrentDictionary(); + this.fileReloadTokens = new ConcurrentDictionary(); + + this.fileWatcher = new FileSystemWatcher { Path = rootDirService.GetPluginsRoot(), Filter = "*.dll", IncludeSubdirectories = true, NotifyFilter = NotifyFilters.LastWrite, + EnableRaisingEvents = true }; + this.fileWatcher.Changed += ( sender, e ) => + { + static async Task WaitForFileAccess( CancellationToken token, string filePath, int maxRetries = 10, int initialDelayMs = 50 ) + { + for (var i = 1; i <= maxRetries && !token.IsCancellationRequested; i++) + { + try + { + using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + break; + } + catch (IOException) + { + if (i < maxRetries) + { + // 50ms, 100ms, 200ms, 400ms, 800ms... + await Task.Delay(initialDelayMs * (1 << (i - 1)), token); + } + continue; + } + catch (Exception) + { + break; + } + } + } - _Watcher.Changed += HandlePluginChange; + try + { + if (!NativeServerHelpers.UseAutoHotReload() || e.ChangeType != WatcherChangeTypes.Changed) + { + return; + } - _Watcher.EnableRaisingEvents = true; + var directoryName = Path.GetFileName(Path.GetDirectoryName(e.FullPath)) ?? string.Empty; + var fileName = Path.GetFileNameWithoutExtension(e.FullPath); + if (string.IsNullOrWhiteSpace(directoryName) || !fileName.Equals(directoryName)) + { + return; + } - Initialize(); - } + if ((DateTime.UtcNow - fileLastChange.GetValueOrDefault(directoryName, DateTime.MinValue)).TotalSeconds > 2) + { + _ = fileLastChange.AddOrUpdate(directoryName, DateTime.UtcNow, ( _, _ ) => DateTime.UtcNow); - public void Initialize() - { - AppDomain.CurrentDomain.AssemblyResolve += ( sender, e ) => - { - var loadingAssemblyName = new AssemblyName(e.Name).Name ?? ""; - if (string.IsNullOrWhiteSpace(loadingAssemblyName)) - { - return null; - } + if (fileReloadTokens.TryRemove(directoryName, out var oldCts)) + { + oldCts.Cancel(); + oldCts.Dispose(); + } - if (loadingAssemblyName == "SwiftlyS2.CS2") + var cts = new CancellationTokenSource(); + _ = fileReloadTokens.AddOrUpdate(directoryName, cts, ( _, _ ) => cts); + + // Wait for file to be accessible, then reload + _ = Task.Run(async () => + { + try + { + await WaitForFileAccess(cts.Token, e.FullPath); + Console.WriteLine("\n"); + if (ReloadPluginByDllName(directoryName, true)) + { + logger.LogInformation("Reloaded plugin: {Format}", directoryName); + } + else + { + logger.LogWarning("Failed to reload plugin: {Format}", directoryName); + } + Console.WriteLine("\n"); + } + catch (Exception) + { + } + }, cts.Token); + } + } + catch (Exception ex) { - return Assembly.GetExecutingAssembly(); + if (!GlobalExceptionHandler.Handle(ex)) + { + return; + } + logger.LogError(ex, "Failed to handle plugin change"); } + }; - var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => loadingAssemblyName == a.GetName().Name); - - return loadedAssembly ?? null; + AppDomain.CurrentDomain.AssemblyResolve += ( sender, e ) => + { + var loadingAssemblyName = new AssemblyName(e.Name).Name ?? string.Empty; + return loadingAssemblyName.Equals("SwiftlyS2.CS2", StringComparison.OrdinalIgnoreCase) + ? Assembly.GetExecutingAssembly() + : AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => loadingAssemblyName == a.GetName().Name); }; + LoadExports(); LoadPlugins(); } - public void HandlePluginChange( object sender, FileSystemEventArgs e ) + public IReadOnlyList GetPlugins() => plugins.AsReadOnly(); + + public string? FindPluginDirectoryByDllName( string dllName ) { - try + dllName = dllName.Trim(); + if (dllName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) { - if (!NativeServerHelpers.UseAutoHotReload()) + dllName = dllName[..^4]; + } + + var pluginDir = plugins + .FirstOrDefault(p => Path.GetFileName(p.PluginDirectory)?.Trim().Equals(dllName.Trim()) ?? false) + ?.PluginDirectory; + + if (!string.IsNullOrWhiteSpace(pluginDir)) + { + return pluginDir; + } + + string? foundDir = null; + EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), dir => + { + if (Path.GetFileName(dir).Equals(dllName)) { - return; + foundDir = dir; } + }); + + return foundDir; + } + + public bool UnloadPluginById( string id, bool silent = false ) + { + var context = plugins + .Where(p => p.Status != PluginStatus.Unloaded) + .FirstOrDefault(p => p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false); - // Windows FileSystemWatcher triggers multiple (open, write, close) events for a single file change - if (DateTime.Now - lastRead < TimeSpan.FromSeconds(1)) + try + { + context?.Dispose(); + context?.Loader?.Dispose(); + context?.Core?.Dispose(); + context!.Status = PluginStatus.Unloaded; + return true; + } + catch + { + if (!silent) { - return; + logger.LogWarning("Failed to unload plugin by id: {Id}", id); + } + if (context != null) + { + context.Status = PluginStatus.Indeterminate; } + return false; + } + finally + { + RebuildSharedServices(); + } + } - var directory = Path.GetDirectoryName(e.FullPath); - if (directory == null) + public bool UnloadPluginByDllName( string dllName, bool silent = false ) + { + var pluginDir = FindPluginDirectoryByDllName(dllName); + if (string.IsNullOrWhiteSpace(pluginDir)) + { + if (!silent) { - return; + logger.LogWarning("Failed to find plugin by name: {DllName}", dllName); } + return false; + } + + var context = plugins + .Where(p => p.Status != PluginStatus.Unloaded) + .FirstOrDefault(p => p.PluginDirectory?.Trim().Equals(pluginDir.Trim()) ?? false); - foreach (var plugin in _Plugins) + if (string.IsNullOrWhiteSpace(context?.Metadata?.Id)) + { + if (!silent) { - if (Path.GetFileName(plugin?.PluginDirectory) == Path.GetFileName(directory)) - { - var pluginId = plugin?.Metadata?.Id; - if (string.IsNullOrWhiteSpace(pluginId)) - { - break; - } + logger.LogWarning("Failed to find plugin by name: {DllName}", dllName); + } + return false; + } - lock (reloadingPlugins) - { - if (reloadingPlugins.Contains(pluginId)) - { - return; - } - _ = reloadingPlugins.Add(pluginId); - } + return UnloadPluginById(context.Metadata.Id, silent); + } - lastRead = DateTime.Now; + public bool LoadPluginById( string id, bool silent = false ) + { + var context = plugins + .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded) + .FirstOrDefault(p => p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false); - // meh, Idk why, but when using Mstsc to copy and overwrite files - // it sometimes triggers: "System.IO.IOException: The process cannot access the file because it is being used by another process." - // therefore, we use a retry mechanism - _ = Task.Run(async () => - { - try - { - await Task.Delay(500); + if (string.IsNullOrWhiteSpace(context?.PluginDirectory)) + { + if (!silent) + { + logger.LogWarning("Failed to load plugin by id: {Id}", id); + } + return false; + } - var fileLockSuccess = false; - for (var attempt = 0; attempt < 3; attempt++) - { - try - { - using (var stream = File.Open(e.FullPath, FileMode.Open, FileAccess.Read, FileShare.None)) - { - } - fileLockSuccess = true; - break; - } - catch (IOException) when (attempt < 1) - { - _Logger.LogWarning($"{Path.GetFileName(plugin?.PluginDirectory)} is locked, retrying in 500ms... (Attempt {attempt + 1}/3)"); - await Task.Delay(500); - } - catch (IOException) - { - _Logger.LogError($"Failed to reload {Path.GetFileName(plugin?.PluginDirectory)} after 3 attempts"); - } - } + return LoadPluginByDllName(Path.GetFileName(context.PluginDirectory), silent); + } - if (fileLockSuccess) - { - ReloadPlugin(pluginId); - } - } - finally - { - lock (reloadingPlugins) - { - _ = reloadingPlugins.Remove(pluginId); - } - } - }); + public bool LoadPluginByDllName( string dllName, bool silent = false ) + { + var pluginDir = FindPluginDirectoryByDllName(dllName); + if (string.IsNullOrWhiteSpace(pluginDir)) + { + if (!silent) + { + logger.LogWarning("Failed to load plugin by name: {DllName}", dllName); + } + return false; + } + + var oldContext = plugins + .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded) + .FirstOrDefault(p => p.PluginDirectory?.Trim().Equals(pluginDir.Trim()) ?? false); - break; + PluginContext? newContext = null; + try + { + if (oldContext != null && plugins.Remove(oldContext)) + { + newContext = LoadPlugin(pluginDir, true, silent); + if (newContext?.Status == PluginStatus.Loaded) + { + if (!silent) + { + logger.LogInformation("Loaded plugin: {Id}", newContext.Metadata!.Id); + } + return true; } } + throw new ArgumentException(string.Empty, string.Empty); } - catch (Exception ex) + catch (Exception e) { - if (!GlobalExceptionHandler.Handle(ex)) return; - _Logger.LogError(ex, "Error handling plugin change"); + if (!GlobalExceptionHandler.Handle(e)) + { + return false; + } + if (!silent) + { + logger.LogWarning("Failed to load plugin by name: {Path}", pluginDir); + } + if (newContext != null) + { + newContext.Status = PluginStatus.Indeterminate; + } + return false; + } + finally + { + RebuildSharedServices(); } } - private void PopulateSharedManually( string startDirectory ) + public bool ReloadPluginById( string id, bool silent = false ) { - var pluginDirs = Directory.GetDirectories(startDirectory); + _ = UnloadPluginById(id, silent); + return LoadPluginById(id, silent); + } - foreach (var pluginDir in pluginDirs) + public bool ReloadPluginByDllName( string dllName, bool silent = false ) + { + _ = UnloadPluginByDllName(dllName, silent); + return LoadPluginByDllName(dllName, silent); + } + + private void LoadExports() + { + void PopulateSharedManually( string startDirectory ) { - var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith("[") && dirName.EndsWith("]")) PopulateSharedManually(pluginDir); - else + EnumeratePluginDirectories(startDirectory, pluginDir => { - if (Directory.Exists(Path.Combine(pluginDir, "resources", "exports"))) + var exportsPath = Path.Combine(pluginDir, "resources", "exports"); + if (!Directory.Exists(exportsPath)) { - var exportFiles = Directory.GetFiles(Path.Combine(pluginDir, "resources", "exports"), "*.dll"); - foreach (var exportFile in exportFiles) + return; + } + + Directory.GetFiles(exportsPath, "*.dll") + .ToList() + .ForEach(exportFile => { try { var assembly = Assembly.LoadFrom(exportFile); - var exports = assembly.GetTypes(); - foreach (var export in exports) - { - _SharedTypes.Add(export); - } + assembly.GetTypes().ToList().ForEach(sharedTypes.Add); } catch (Exception innerEx) { - if (!GlobalExceptionHandler.Handle(innerEx)) return; - _Logger.LogWarning(innerEx, $"Failed to load export assembly: {exportFile}"); + if (!GlobalExceptionHandler.Handle(innerEx)) + { + return; + } + logger.LogWarning(innerEx, "Failed to load export assembly: {Path}", exportFile); } - } - } - } + }); + }); } - } - - private void LoadExports() - { - var resolver = new DependencyResolver(_Logger); try { - resolver.AnalyzeDependencies(_RootDirService.GetPluginsRoot()); - - _Logger.LogInformation(resolver.GetDependencyGraphVisualization()); - + var resolver = new DependencyResolver(logger); + resolver.AnalyzeDependencies(rootDirService.GetPluginsRoot()); + logger.LogInformation("{Graph}", resolver.GetDependencyGraphVisualization()); var loadOrder = resolver.GetLoadOrder(); + logger.LogInformation("Loading {Count} export assemblies in dependency order", loadOrder.Count); - _Logger.LogInformation($"Loading {loadOrder.Count} export assemblies in dependency order."); - - foreach (var exportFile in loadOrder) + loadOrder.ForEach(exportFile => { try { var assembly = Assembly.LoadFrom(exportFile); var exports = assembly.GetTypes(); - - _Logger.LogDebug($"Loaded {exports.Length} types from {Path.GetFileName(exportFile)}."); - - - foreach (var export in exports) - { - _SharedTypes.Add(export); - } + logger.LogDebug("Loaded {Count} types from {Path}", exports.Length, Path.GetFileName(exportFile)); + exports.ToList().ForEach(sharedTypes.Add); } catch (Exception ex) { - if (!GlobalExceptionHandler.Handle(ex)) return; - _Logger.LogWarning(ex, $"Failed to load export assembly: {exportFile}"); + if (!GlobalExceptionHandler.Handle(ex)) + { + return; + } + logger.LogWarning(ex, "Failed to load export assembly: {Path}", exportFile); } - } + }); - _Logger.LogInformation($"Successfully loaded {_SharedTypes.Count} shared types."); + logger.LogInformation("Loaded {Count} shared types", sharedTypes.Count); } - catch (InvalidOperationException ex) when (ex.Message.Contains("Circular dependency")) + catch (InvalidOperationException ex) when (ex.Message.Contains("circular dependency", StringComparison.OrdinalIgnoreCase)) { - _Logger.LogError(ex, "Circular dependency detected in plugin exports. Loading exports without dependency resolution."); - PopulateSharedManually(_RootDirService.GetPluginsRoot()); + logger.LogError(ex, "Circular dependency detected in plugin exports, loading manually"); + PopulateSharedManually(rootDirService.GetPluginsRoot()); } catch (Exception ex) { - if (!GlobalExceptionHandler.Handle(ex)) return; - _Logger.LogError(ex, "Unexpected error during export loading"); + if (!GlobalExceptionHandler.Handle(ex)) + { + return; + } + logger.LogError(ex, "Failed to load exports"); } } - - private void LoadPluginsFromFolder( string directory ) + private void LoadPlugins() { - var pluginDirs = Directory.GetDirectories(directory); - - foreach (var pluginDir in pluginDirs) + EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), pluginDir => { - var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith("[") && dirName.EndsWith("]")) LoadPluginsFromFolder(pluginDir); - else + var relativePath = Path.GetRelativePath(rootDirService.GetRoot(), pluginDir); + var displayPath = Path.Join("(swRoot)", relativePath); + var dllName = Path.GetFileName(pluginDir); + var fullDisplayPath = string.IsNullOrWhiteSpace(displayPath) ? string.Empty : $"{Path.Join(displayPath, dllName)}.dll"; + + Console.WriteLine(string.Empty); + logger.LogInformation("Loading plugin: {Path}", fullDisplayPath); + + try { - try + var context = LoadPlugin(pluginDir, true); + if (context?.Status == PluginStatus.Loaded) { - var context = LoadPlugin(pluginDir, false); - if (context != null && context.Status == PluginStatus.Loaded) - { - _Logger.LogInformation("Loaded plugin " + context.Metadata!.Id); - } + logger.LogInformation( + string.Join("\n", [ + "Loaded Plugin", + "├─ {Id} {Version}", + "├─ Author: {Author}", + "└─ Path: {RelativePath}" + ]), + context.Metadata!.Id, + context.Metadata!.Version, + context.Metadata!.Author, + displayPath + ); } - catch (Exception e) + else { - if (!GlobalExceptionHandler.Handle(e)) continue; - _Logger.LogWarning(e, "Error loading plugin: " + pluginDir); - continue; + logger.LogWarning("Failed to load plugin: {Path}", fullDisplayPath); } } - } - } + catch (Exception e) + { + if (!GlobalExceptionHandler.Handle(e)) + { + return; + } + logger.LogWarning(e, "Failed to load plugin: {Path}", fullDisplayPath); + } - private void LoadPlugins() - { - LoadPluginsFromFolder(_RootDirService.GetPluginsRoot()); + Console.WriteLine(string.Empty); + }); RebuildSharedServices(); - _Plugins - .Where(p => p.Status == PluginStatus.Loaded) - .ToList() - .ForEach(p => p.Plugin!.OnAllPluginsLoaded()); - } - - public List GetPlugins() - { - return _Plugins; - } - - private void RebuildSharedServices() - { - _InterfaceManager.Dispose(); - - _Plugins - .Where(p => p.Status == PluginStatus.Loaded) - .ToList() - .ForEach(p => p.Plugin!.ConfigureSharedInterface(_InterfaceManager)); - - - _InterfaceManager.Build(); - - _Plugins - .Where(p => p.Status == PluginStatus.Loaded) - .ToList() - .ForEach(p => p.Plugin!.UseSharedInterface(_InterfaceManager)); - - _Plugins + plugins .Where(p => p.Status == PluginStatus.Loaded) .ToList() - .ForEach(p => p.Plugin!.OnSharedInterfaceInjected(_InterfaceManager)); + .ForEach(p => p.Plugin?.OnAllPluginsLoaded()); } - - public PluginContext? LoadPlugin( string dir, bool hotReload ) + private PluginContext? LoadPlugin( string dir, bool hotReload, bool silent = false ) { + PluginContext? FailWithError( PluginContext context, string message ) + { + if (!silent) + { + logger.LogWarning("{Message}", message); + } + context.Status = PluginStatus.Error; + return null; + } - - PluginContext context = new() { - PluginDirectory = dir, - Status = PluginStatus.Loading, - }; - _Plugins.Add(context); + var context = new PluginContext { PluginDirectory = dir, Status = PluginStatus.Loading }; + plugins.Add(context); var entrypointDll = Path.Combine(dir, Path.GetFileName(dir) + ".dll"); - if (!File.Exists(entrypointDll)) { - _Logger.LogWarning("Plugin entrypoint DLL not found: " + entrypointDll); - context.Status = PluginStatus.Error; - return null; + return FailWithError(context, $"Failed to find plugin entrypoint DLL: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } + var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); var loader = PluginLoader.CreateFromAssemblyFile( - assemblyFile: entrypointDll, - sharedTypes: [typeof(BasePlugin), .. _SharedTypes], + entrypointDll, + [typeof(BasePlugin), .. sharedTypes], config => { - config.IsUnloadable = true; - config.LoadInMemory = true; - var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); + config.IsUnloadable = config.LoadInMemory = true; if (currentContext != null) { - config.DefaultContext = currentContext; - config.PreferSharedTypes = true; + (config.DefaultContext, config.PreferSharedTypes) = (currentContext, true); } } ); - var assembly = loader.LoadDefaultAssembly(); - - var pluginType = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin)))!; - + var pluginType = loader.LoadDefaultAssembly() + .GetTypes() + .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); if (pluginType == null) { - _Logger.LogWarning("Plugin type not found: " + entrypointDll); - context.Status = PluginStatus.Error; - return null; + return FailWithError(context, $"Failed to find plugin type: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } var metadata = pluginType.GetCustomAttribute(); if (metadata == null) { - _Logger.LogWarning("Plugin metadata not found: " + entrypointDll); - context.Status = PluginStatus.Error; - return null; + return FailWithError(context, $"Failed to find plugin metadata: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } context.Metadata = metadata; + dataDirectoryService.EnsurePluginDataDirectory(metadata.Id); - _DataDirectoryService.EnsurePluginDataDirectory(metadata.Id); - - var core = new SwiftlyCore(metadata.Id, Path.GetDirectoryName(entrypointDll)!, metadata, pluginType, _Provider, _DataDirectoryService.GetPluginDataDirectory(metadata.Id)); + var pluginDir = Path.GetDirectoryName(entrypointDll)!; + var dataDir = dataDirectoryService.GetPluginDataDirectory(metadata.Id); + var core = new SwiftlyCore(metadata.Id, pluginDir, metadata, pluginType, rootProvider, dataDir); core.InitializeType(pluginType); - var plugin = (BasePlugin)Activator.CreateInstance(pluginType, [core])!; - core.InitializeObject(plugin); - try { plugin.Load(hotReload); + context.Status = PluginStatus.Loaded; + context.Core = core; + context.Plugin = plugin; + context.Loader = loader; + return context; } catch (Exception e) { - if (!GlobalExceptionHandler.Handle(e)) + _ = GlobalExceptionHandler.Handle(e); + + try { - context.Status = PluginStatus.Error; - return null; + plugin.Unload(); + loader?.Dispose(); + core?.Dispose(); } - _Logger.LogWarning(e, $"Error loading plugin {entrypointDll}"); - context.Status = PluginStatus.Error; - return null; - } - - context.Status = PluginStatus.Loaded; - context.Core = core; - context.Plugin = plugin; - context.Loader = loader; + catch { } - return context; + return FailWithError(context, $"Failed to load plugin: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } } - public bool UnloadPlugin( string id ) + private void RebuildSharedServices() { - var context = _Plugins - .Where(p => p.Status == PluginStatus.Loaded) - .FirstOrDefault(p => p.Metadata?.Id == id); - if (context == null) - { - _Logger.LogWarning("Plugin not found or not loaded: " + id); - return false; - } + interfaceManager.Dispose(); - context.Dispose(); - context.Status = PluginStatus.Unloaded; - return true; + var loadedPlugins = plugins + .Where(p => p.Status == PluginStatus.Loaded) + .ToList(); + + loadedPlugins.ForEach(p => p.Plugin?.ConfigureSharedInterface(interfaceManager)); + interfaceManager.Build(); + + loadedPlugins.ForEach(p => p.Plugin?.UseSharedInterface(interfaceManager)); + loadedPlugins.ForEach(p => p.Plugin?.OnSharedInterfaceInjected(interfaceManager)); } - public bool LoadPluginById( string id ) + private static void EnumeratePluginDirectories( string directory, Action action ) { - var context = _Plugins - .Where(p => p.Status == PluginStatus.Unloaded) - .FirstOrDefault(p => p.Metadata?.Id == id); - if (context == null) + var pluginDirs = Directory.GetDirectories(directory); + + foreach (var pluginDir in pluginDirs) { - // try to find new plugins - var root = _RootDirService.GetPluginsRoot(); - var pluginDirs = Directory.GetDirectories(root); - foreach (var pluginDir in pluginDirs) + var dirName = Path.GetFileName(pluginDir); + if (dirName.Trim().StartsWith('[') && dirName.EndsWith(']')) { - if (Path.GetFileName(pluginDir) == id) - { - context = LoadPlugin(pluginDir, false); - break; - } + EnumeratePluginDirectories(pluginDir, action); + continue; } - if (context == null) + + if (dirName.Trim().Equals("disable", StringComparison.OrdinalIgnoreCase) || dirName.Trim().Equals("_", StringComparison.OrdinalIgnoreCase)) { - _Logger.LogWarning("Plugin not found: " + id); - return false; + continue; } - } - else - { - var directory = context.PluginDirectory!; - _ = _Plugins.Remove(context); - _ = LoadPlugin(directory, true); - } - RebuildSharedServices(); - return true; - } - - public void ReloadPlugin( string id ) - { - _Logger.LogInformation("Reloading plugin " + id); - - if (!UnloadPlugin(id)) - { - return; - } + if (dirName.Trim().Length >= 2 && dirName.StartsWith('_')) + { + continue; + } - if (!LoadPluginById(id)) - { - RebuildSharedServices(); + action(pluginDir); } - - _Logger.LogInformation("Reloaded plugin " + id); } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginStatus.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginStatus.cs index 7c2681b07..0600d96ca 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginStatus.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginStatus.cs @@ -1,9 +1,10 @@ namespace SwiftlyS2.Core.Services; -internal enum PluginStatus { - - Loaded, - Unloaded, - Loading, - Error, +internal enum PluginStatus +{ + Loaded, + Unloaded, + Loading, + Error, + Indeterminate } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs index 4a18a4a65..a6d9373cd 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -1,290 +1,430 @@ -using System.Reflection; using System.Runtime; +using System.Reflection; using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; using Spectre.Console; -using SwiftlyS2.Core.Natives; -using SwiftlyS2.Core.Plugins; +using Microsoft.Extensions.Logging; using SwiftlyS2.Shared; +using SwiftlyS2.Core.Plugins; +using SwiftlyS2.Core.Natives; using SwiftlyS2.Shared.Commands; namespace SwiftlyS2.Core.Services; internal class CoreCommandService { - private ILogger _Logger { get; init; } - - private ISwiftlyCore _Core { get; init; } - - private ICommandService _CommandService { get; init; } - private PluginManager _PluginManager { get; init; } - private ProfileService _ProfileService { get; init; } - - public CoreCommandService( ILogger logger, ISwiftlyCore core, PluginManager pluginManager, ProfileService profileService ) - { - _Logger = logger; - _Core = core; - _CommandService = core.Command; - _PluginManager = pluginManager; - _ProfileService = profileService; - _CommandService.RegisterCommand("sw", OnCommand, true); - } - - private void OnCommand( ICommandContext context ) - { - try - { - if (context.IsSentByPlayer) return; - - var args = context.Args; - if (args.Length == 0) - { - ShowHelp(context); - return; - } - - switch (args[0]) - { - case "help": - ShowHelp(context); - break; - case "credits": - _Logger.LogInformation(@"SwiftlyS2 was created and developed by Swiftly Solution SRL and the contributors. -SwiftlyS2 is licensed under the GNU General Public License v3.0 or later. -Website: https://swiftlys2.net/ -GitHub: https://github.com/swiftly-solution/swiftlys2"); - break; - case "list": - var players = _Core.PlayerManager.GetAllPlayers(); - var outString = $"Connected players: {_Core.PlayerManager.PlayerCount}/{_Core.Engine.MaxPlayers}"; - foreach (var player in players) - { - outString += $"\n{player.PlayerID}. {player.Controller?.PlayerName}{(player.IsFakeClient ? " (BOT)" : "")} (steamid={player.SteamID})"; - } - _Logger.LogInformation(outString); - break; - case "status": - var uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime; - var outStrings = $"Uptime: {uptime.Days}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s"; - outStrings += $"\nManaged Heap Memory: {GC.GetTotalMemory(false) / 1024.0f / 1024.0f:0.00} MB"; - outStrings += $"\nLoaded Plugins: {_PluginManager.GetPlugins().Count()}"; - outStrings += $"\nPlayers: {_Core.PlayerManager.PlayerCount}/{_Core.Engine.GlobalVars.MaxClients}"; - outStrings += $"\nMap: {_Core.Engine.GlobalVars.MapName.Value}"; - _Logger.LogInformation(outStrings); - break; - case "version": - var outVersion = $"SwiftlyS2 Version: {NativeEngineHelpers.GetNativeVersion()}"; - outVersion += $"\nSwiftlyS2 Managed Version: {Assembly.GetExecutingAssembly().GetName().Version}"; - outVersion += $"\nSwiftlyS2 Runtime Version: {Environment.Version}"; - outVersion += $"\nSwiftlyS2 C++ Version: C++23"; - outVersion += $"\nSwiftlyS2 .NET Version: {RuntimeInformation.FrameworkDescription}"; - outVersion += $"\nGitHub URL: https://github.com/swiftly-solution/swiftlys2"; - _Logger.LogInformation(outVersion); - break; - case "gc": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } - var outGc = "Garbage Collection Information:"; - outGc += $"\n - Total Memory: {GC.GetTotalMemory(false) / 1024.0f / 1024.0f:0.00} MB"; - outGc += $"\n - Is Server GC: {GCSettings.IsServerGC}"; - outGc += $"\n - Max Generation: {GC.MaxGeneration}"; - for (int i = 0; i <= GC.MaxGeneration; i++) - { - outGc += $"\n - Generation {i} Collection Count: {GC.CollectionCount(i)}"; - } - outGc += $"\n - Latency Mode: {GCSettings.LatencyMode}"; - _Logger.LogInformation(outGc); - break; - case "plugins": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } - PluginCommand(context); - break; - case "profiler": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } - ProfilerCommand(context); - break; - case "confilter": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } - ConfilterCommand(context); - break; - default: - ShowHelp(context); - break; - } - } - catch (Exception e) - { - if (!GlobalExceptionHandler.Handle(e)) return; - _Logger.LogError(e, "Error executing command"); - } - } - - private static void ShowHelp( ICommandContext context ) - { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table.AddRow("credits", "List Swiftly credits"); - table.AddRow("help", "Show the help for Swiftly Commands"); - table.AddRow("list", "Show the list of online players"); - table.AddRow("status", "Show the status of the server"); - if (!context.IsSentByPlayer) - { - table.AddRow("confilter", "Console Filter Menu"); - table.AddRow("plugins", "Plugin Management Menu"); - table.AddRow("gc", "Show garbage collection information on managed"); - table.AddRow("profiler", "Profiler Menu"); - } - table.AddRow("version", "Display Swiftly version"); - AnsiConsole.Write(table); - } - - private void ConfilterCommand( ICommandContext context ) - { - var args = context.Args; - if (args.Length == 1) + private readonly ILogger logger; + private readonly ISwiftlyCore core; + private readonly PluginManager pluginManager; + private readonly RootDirService rootDirService; + private readonly ProfileService profileService; + + public CoreCommandService( ILogger logger, ISwiftlyCore core, PluginManager pluginManager, RootDirService rootDirService, ProfileService profileService ) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table.AddRow("enable", "Enable console filtering"); - table.AddRow("disable", "Disable console filtering"); - table.AddRow("status", "Show the status of the console filter"); - table.AddRow("reload", "Reload console filter configuration"); - AnsiConsole.Write(table); - return; + this.logger = logger; + this.core = core; + this.pluginManager = pluginManager; + this.rootDirService = rootDirService; + this.profileService = profileService; + _ = core.Command.RegisterCommand("sw", OnCommand, true); } - switch (args[1]) + private void OnCommand( ICommandContext context ) { - case "enable": - if (!_Core.ConsoleOutput.IsFilterEnabled()) _Core.ConsoleOutput.ToggleFilter(); - _Logger.LogInformation("Console filtering has been enabled."); - break; - case "disable": - if (_Core.ConsoleOutput.IsFilterEnabled()) _Core.ConsoleOutput.ToggleFilter(); - _Logger.LogInformation("Console filtering has been disabled."); - break; - case "status": - _Logger.LogInformation($"Console filtering is currently {(_Core.ConsoleOutput.IsFilterEnabled() ? "enabled" : "disabled")}.\nBelow are some statistics for the filtering process:\n{_Core.ConsoleOutput.GetCounterText()}"); - break; - case "reload": - _Core.ConsoleOutput.ReloadFilterConfiguration(); - _Logger.LogInformation("Console filter configuration reloaded."); - break; - default: - _Logger.LogWarning("Unknown command"); - break; + void ShowPlayerList() + { + var output = string.Join("\n", [ + $"Connected players: {core.PlayerManager.PlayerCount}/{core.Engine.GlobalVars.MaxClients}", + ..core.PlayerManager.GetAllPlayers().Select(player => $"{player.PlayerID}. {player.Controller?.PlayerName}{(player.IsFakeClient ? " (BOT)" : "")} (steamid={player.SteamID})") + ]); + logger.LogInformation("{Output}", output); + } + + void ShowServerStatus() + { + var uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime; + var output = string.Join("\n", [ + $"Uptime: {uptime.Days}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s", + $"Managed Heap Memory: {GC.GetTotalMemory(false) / 1024.0f / 1024.0f:0.00} MB", + $"Loaded Plugins: {pluginManager.GetPlugins().Count}", + $"Players: {core.PlayerManager.PlayerCount}/{core.Engine.GlobalVars.MaxClients}", + $"Map: {core.Engine.GlobalVars.MapName.Value}" + ]); + logger.LogInformation("{Output}", output); + } + + void ShowVersionInfo() + { + var output = string.Join("\n", [ + $"SwiftlyS2 Version: {NativeEngineHelpers.GetNativeVersion()}", + $"SwiftlyS2 Managed Version: {Assembly.GetExecutingAssembly().GetName().Version}", + $"SwiftlyS2 Runtime Version: {Environment.Version}", + $"SwiftlyS2 C++ Version: C++23", + $"SwiftlyS2 .NET Version: {RuntimeInformation.FrameworkDescription}", + $"GitHub URL: https://github.com/swiftly-solution/swiftlys2" + ]); + logger.LogInformation("{Output}", output); + } + + void ShowGarbageCollectionInfo() + { + var output = string.Join("\n", [ + $"Garbage Collection Information:", + $" - Total Memory: {GC.GetTotalMemory(false) / 1024.0f / 1024.0f:0.00} MB", + $" - Is Server GC: {GCSettings.IsServerGC}", + $" - Max Generation: {GC.MaxGeneration}", + ..Enumerable.Range(0, GC.MaxGeneration + 1).Select(i => $" - Generation {i} Collection Count: {GC.CollectionCount(i)}"), + $" - Latency Mode: {GCSettings.LatencyMode}" + ]); + logger.LogInformation("{Output}", output); + } + + void ShowCredits() + { + var output = string.Join("\n", [ + "SwiftlyS2 was created and developed by Swiftly Solution SRL and the contributors.", + "SwiftlyS2 is licensed under the GNU General Public License v3.0 or later.", + "Website: https://swiftlys2.net/", + "GitHub: https://github.com/swiftly-solution/swiftlys2" + ]); + logger.LogInformation("{Output}", output); + } + + bool RequireConsoleAccess() + { + if (context.IsSentByPlayer) + { + context.Reply("This command can only be executed from the server console."); + return false; + } + return true; + } + + try + { + if (context.IsSentByPlayer) + { + return; + } + + var args = context.Args; + if (args.Length == 0) + { + ShowHelp(context); + return; + } + + switch (args[0].Trim().ToLower()) + { + case "help": + ShowHelp(context); + break; + case "credits": + ShowCredits(); + break; + case "list": + ShowPlayerList(); + break; + case "status": + ShowServerStatus(); + break; + case "version": + ShowVersionInfo(); + break; + case "gc" when RequireConsoleAccess(): + ShowGarbageCollectionInfo(); + break; + case "plugins" when RequireConsoleAccess(): + PluginCommand(context); + break; + case "profiler" when RequireConsoleAccess(): + ProfilerCommand(context); + break; + case "confilter" when RequireConsoleAccess(): + ConfilterCommand(context); + break; + default: + ShowHelp(context); + break; + } + } + catch (Exception e) + { + if (!GlobalExceptionHandler.Handle(e)) + { + return; + } + logger.LogError(e, "Failed to execute command"); + } } - } - private void ProfilerCommand( ICommandContext context ) - { - var args = context.Args; - if (args.Length == 1) + private static void ShowHelp( ICommandContext context ) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table.AddRow("enable", "Enable the profiler"); - table.AddRow("disable", "Disable the profiler"); - table.AddRow("status", "Show the status of the profiler"); - table.AddRow("save", "Save the profiler data to a file"); - AnsiConsole.Write(table); - return; + var table = new Table() + .AddColumn("Command").AddColumn("Description") + .AddRow("credits", "List Swiftly credits") + .AddRow("help", "Show the help for Swiftly Commands") + .AddRow("list", "Show the list of online players") + .AddRow("status", "Show the status of the server"); + if (!context.IsSentByPlayer) + { + _ = table + .AddRow("confilter", "Console Filter Menu") + .AddRow("plugins", "Plugin Management Menu") + .AddRow("gc", "Show garbage collection information on managed") + .AddRow("profiler", "Profiler Menu"); + } + _ = table.AddRow("version", "Display Swiftly version"); + AnsiConsole.Write(table); } - switch (args[1]) + private void ConfilterCommand( ICommandContext context ) { - case "enable": - _ProfileService.Enable(); - _Logger.LogInformation("The profiler has been enabled."); - break; - case "disable": - _ProfileService.Disable(); - _Logger.LogInformation("The profiler has been disabled."); - break; - case "status": - _Logger.LogInformation($"Profiler is currently {(_ProfileService.IsEnabled() ? "enabled" : "disabled")}."); - break; - case "save": - var pluginId = args.Length >= 3 ? args[2] : ""; - var basePath = Environment.GetEnvironmentVariable("SWIFTLY_MANAGED_ROOT")!; - if (!File.Exists(Path.Combine(basePath, "profilers"))) + void ShowConfilterHelp() + { + var table = new Table() + .AddColumn("Command") + .AddColumn("Description") + .AddRow("enable", "Enable console filtering") + .AddRow("disable", "Disable console filtering") + .AddRow("status", "Show the status of the console filter") + .AddRow("reload", "Reload console filter configuration"); + AnsiConsole.Write(table); + } + + void EnableFilter() { - Directory.CreateDirectory(Path.Combine(basePath, "profilers")); + if (!core.ConsoleOutput.IsFilterEnabled()) + { + core.ConsoleOutput.ToggleFilter(); + } + logger.LogInformation("Console filtering has been enabled."); } - Guid guid = Guid.NewGuid(); - File.WriteAllText(Path.Combine(basePath, "profilers", $"profiler.{guid}.{(pluginId == "" ? "core" : pluginId)}.json"), _ProfileService.GenerateJSONPerformance(pluginId)); - _Logger.LogInformation($"Profile saved to {Path.Combine(basePath, "profilers", $"profiler.{guid}.{(pluginId == "" ? "core" : pluginId)}.json")}"); - break; - default: - _Logger.LogWarning("Unknown command"); - break; + void DisableFilter() + { + if (core.ConsoleOutput.IsFilterEnabled()) + { + core.ConsoleOutput.ToggleFilter(); + } + logger.LogInformation("Console filtering has been disabled."); + } + + void ShowFilterStatus() + { + var status = core.ConsoleOutput.IsFilterEnabled() ? "enabled" : "disabled"; + var output = string.Join("\n", [ + $"Console filtering is currently {status}.", + "Below are some statistics for the filtering process:", + core.ConsoleOutput.GetCounterText() + ]); + logger.LogInformation("{Output}", output); + } + + void ReloadFilter() + { + core.ConsoleOutput.ReloadFilterConfiguration(); + logger.LogInformation("Console filter configuration reloaded."); + } + + var args = context.Args; + if (args.Length == 1) + { + ShowConfilterHelp(); + return; + } + + switch (args[1].Trim().ToLower()) + { + case "enable": + EnableFilter(); + break; + case "disable": + DisableFilter(); + break; + case "status": + ShowFilterStatus(); + break; + case "reload": + ReloadFilter(); + break; + default: + logger.LogWarning("Unknown command"); + break; + } } - } - private void PluginCommand( ICommandContext context ) - { - var args = context.Args; - if (args.Length == 1) + private void ProfilerCommand( ICommandContext context ) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table.AddRow("list", "List all plugins"); - table.AddRow("load", "Load a plugin"); - table.AddRow("unload", "Unload a plugin"); - table.AddRow("reload", "Reload a plugin"); - AnsiConsole.Write(table); - return; + var args = context.Args; + if (args.Length == 1) + { + var table = new Table().AddColumn("Command").AddColumn("Description") + .AddRow("enable", "Enable the profiler") + .AddRow("disable", "Disable the profiler") + .AddRow("status", "Show the status of the profiler") + .AddRow("save", "Save the profiler data to a file"); + AnsiConsole.Write(table); + return; + } + + switch (args[1].Trim().ToLower()) + { + case "enable": + profileService.Enable(); + logger.LogInformation("The profiler has been enabled."); + break; + case "disable": + profileService.Disable(); + logger.LogInformation("The profiler has been disabled."); + break; + case "status": + logger.LogInformation("Profiler is currently {Status}.", (profileService.IsEnabled() ? "enabled" : "disabled")); + break; + case "save": + var pluginId = args.Length >= 3 ? args[2] : "core"; + var profilerDir = Path.Combine(rootDirService.GetRoot(), "profilers"); + + if (!Directory.Exists(profilerDir)) + { + _ = Directory.CreateDirectory(profilerDir); + } + + var fileName = $"{DateTime.Now:yyyyMMdd}.{Guid.NewGuid()}.{pluginId}.json"; + var filePath = Path.Combine(profilerDir, fileName); + + File.WriteAllText(filePath, profileService.GenerateJSONPerformance(args.Length >= 3 ? args[2] : string.Empty)); + logger.LogInformation("Profile saved to {FilePath}.", filePath); + break; + default: + logger.LogWarning("Unknown command"); + break; + } } - switch (args[1]) + private void PluginCommand( ICommandContext context ) { - case "list": - var table = new Table().AddColumn("Name").AddColumn("Status").AddColumn("Version").AddColumn("Author").AddColumn("Website"); - foreach (var plugin in _PluginManager.GetPlugins()) + void ShowPluginList() { - table.AddRow(plugin.Metadata?.Id ?? "", plugin.Status?.ToString() ?? "Unknown", plugin.Metadata?.Version ?? "", plugin.Metadata?.Author ?? "", plugin.Metadata?.Website ?? ""); + var table = new Table() + .AddColumn("Status") + .AddColumn("PluginId (ver.)") + .AddColumn("Author") + .AddColumn("Website") + .AddColumn("Location"); + + foreach (var plugin in pluginManager.GetPlugins()) + { + var pluginId = plugin.Metadata?.Id ?? ""; + var version = plugin.Metadata?.Version is { } v ? $" {v}" : string.Empty; + var statusText = GetColoredStatus(plugin.Status); + + _ = table.AddRow( + statusText, + $"{pluginId}{version}", + plugin.Metadata?.Author ?? "Anonymous", + plugin.Metadata?.Website ?? string.Empty, + plugin.PluginDirectory is { } dir ? Path.Join("(swRoot)", Path.GetRelativePath(rootDirService.GetRoot(), dir)) : string.Empty); + } + + AnsiConsole.Write(table); } - AnsiConsole.Write(table); - break; - case "load": - if (args.Length < 3) + + void ShowPluginHelp() + { + var table = new Table() + .AddColumn("Command") + .AddColumn("Description") + .AddRow("list", "List all plugins") + .AddRow("load", "Load a plugin") + .AddRow("unload", "Unload a plugin") + .AddRow("reload", "Reload a plugin"); + AnsiConsole.Write(table); + } + + bool ValidatePluginId( string[] args, string command, string usage ) { - _Logger.LogWarning("Usage: sw plugins load "); - return; + if (args.Length >= 3) + { + return true; + } + logger.LogWarning("Usage: sw plugins {Command} {Usage}", command, usage); + return false; } - _PluginManager.LoadPluginById(args[2]); - break; - case "unload": - if (args.Length < 3) + + string GetColoredStatus( PluginStatus? status ) => status switch { + // PluginStatus.Loaded => "[green]Loaded[/]", + // PluginStatus.Error => "[red]Error[/]", + // PluginStatus.Loading => "[yellow]Loading[/]", + // PluginStatus.Unloaded => "[grey]Unloaded[/]", + // _ => "[grey]Unknown[/]" + PluginStatus.Loaded => "Loaded", + PluginStatus.Error => "Error", + PluginStatus.Loading => "Loading", + PluginStatus.Unloaded => "Unloaded", + PluginStatus.Indeterminate => "Indeterminate", + _ => "Unknown" + }; + + var args = context.Args; + if (args.Length == 1) { - _Logger.LogWarning("Usage: sw plugins unload "); - return; + ShowPluginHelp(); + return; } - _PluginManager.UnloadPlugin(args[2]); - break; - case "reload": - if (args.Length < 3) + + switch (args[1].Trim().ToLower()) { - _Logger.LogWarning("Usage: sw plugins reload "); - return; + case "list": + ShowPluginList(); + break; + case "load": + if (ValidatePluginId(args, "load", "")) + { + Console.WriteLine("\n"); + if (pluginManager.LoadPluginByDllName(args[2], true)) + { + logger.LogInformation("Loaded plugin: {Format}", args[2]); + } + else + { + logger.LogWarning("Failed to load plugin: {Format}", args[2]); + } + Console.WriteLine("\n"); + } + break; + case "unload": + if (ValidatePluginId(args, "unload", "")) + { + Console.WriteLine("\n"); + if (pluginManager.UnloadPluginById(args[2], true) || pluginManager.UnloadPluginByDllName(args[2], true)) + { + logger.LogInformation("Unloaded plugin: {Format}", args[2]); + } + else + { + logger.LogWarning("Failed to unload plugin: {Format}", args[2]); + } + Console.WriteLine("\n"); + } + break; + case "reload": + if (ValidatePluginId(args, "reload", "")) + { + Console.WriteLine("\n"); + if (pluginManager.ReloadPluginById(args[2], true) || pluginManager.ReloadPluginByDllName(args[2], true)) + { + logger.LogInformation("Reloaded plugin: {Format}", args[2]); + } + else + { + logger.LogWarning("Failed to reload plugin: {Format}", args[2]); + } + Console.WriteLine("\n"); + } + break; + default: + logger.LogWarning("Unknown command"); + break; } - _PluginManager.ReloadPlugin(args[2]); - break; - default: - _Logger.LogWarning("Unknown command"); - break; } - } } \ No newline at end of file diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index d29722c08..e55882408 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -56,7 +56,7 @@ public InProcessConfig() } } -[PluginMetadata(Id = "testplugin", Version = "1.0.0")] +[PluginMetadata(Id = "sw2.testplugin", Version = "1.0.0")] public class TestPlugin : BasePlugin { public TestPlugin( ISwiftlyCore core ) : base(core)