From 74d55774c72a138dca813305b1563504e95936e4 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Mon, 17 Nov 2025 01:15:01 +0800 Subject: [PATCH 01/12] refactor: Implement more reasonable plugin management --- .../Modules/Plugins/PluginManager.cs | 238 ++++++------------ 1 file changed, 83 insertions(+), 155 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index a2e9c3f5f..6a7521df5 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -2,11 +2,11 @@ using System.Runtime.Loader; 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; @@ -21,13 +21,12 @@ internal class PluginManager private List _SharedTypes { get; set; } = new(); private DataDirectoryService _DataDirectoryService { get; init; } private DateTime lastRead = DateTime.MinValue; - private readonly HashSet reloadingPlugins = new(); public PluginManager( - IServiceProvider provider, - ILogger logger, - RootDirService rootDirService, - DataDirectoryService dataDirectoryService + IServiceProvider provider, + ILogger logger, + RootDirService rootDirService, + DataDirectoryService dataDirectoryService ) { _Provider = provider; @@ -42,7 +41,6 @@ DataDirectoryService dataDirectoryService }; _Watcher.Changed += HandlePluginChange; - _Watcher.EnableRaisingEvents = true; Initialize(); @@ -52,21 +50,10 @@ public void Initialize() { AppDomain.CurrentDomain.AssemblyResolve += ( sender, e ) => { - var loadingAssemblyName = new AssemblyName(e.Name).Name ?? ""; - if (string.IsNullOrWhiteSpace(loadingAssemblyName)) - { - return null; - } - - if (loadingAssemblyName == "SwiftlyS2.CS2") - { - return Assembly.GetExecutingAssembly(); - } - - var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => loadingAssemblyName == a.GetName().Name); - - return loadedAssembly ?? null; + 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(); @@ -87,85 +74,32 @@ public void HandlePluginChange( object sender, FileSystemEventArgs e ) return; } - var directory = Path.GetDirectoryName(e.FullPath); - if (directory == null) + if (string.IsNullOrWhiteSpace(Path.GetDirectoryName(e.FullPath))) { return; } foreach (var plugin in _Plugins) { - if (Path.GetFileName(plugin?.PluginDirectory) == Path.GetFileName(directory)) + if (Path.GetFileName(plugin?.PluginDirectory) == Path.GetFileName(Path.GetDirectoryName(e.FullPath))) { var pluginId = plugin?.Metadata?.Id; - if (string.IsNullOrWhiteSpace(pluginId)) - { - break; - } - - lock (reloadingPlugins) + if (!string.IsNullOrWhiteSpace(pluginId)) { - if (reloadingPlugins.Contains(pluginId)) - { - return; - } - _ = reloadingPlugins.Add(pluginId); + lastRead = DateTime.Now; + ReloadPlugin(pluginId); } - lastRead = DateTime.Now; - - // 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); - - 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"); - } - } - - if (fileLockSuccess) - { - ReloadPlugin(pluginId); - } - } - finally - { - lock (reloadingPlugins) - { - _ = reloadingPlugins.Remove(pluginId); - } - } - }); - break; } } } catch (Exception ex) { - if (!GlobalExceptionHandler.Handle(ex)) return; + if (!GlobalExceptionHandler.Handle(ex)) + { + return; + } _Logger.LogError(ex, "Error handling plugin change"); } } @@ -177,7 +111,10 @@ private void PopulateSharedManually( string startDirectory ) foreach (var pluginDir in pluginDirs) { var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith("[") && dirName.EndsWith("]")) PopulateSharedManually(pluginDir); + if (dirName.StartsWith('[') && dirName.EndsWith(']')) + { + PopulateSharedManually(pluginDir); + } else { if (Directory.Exists(Path.Combine(pluginDir, "resources", "exports"))) @@ -196,8 +133,11 @@ private void PopulateSharedManually( string startDirectory ) } catch (Exception innerEx) { - if (!GlobalExceptionHandler.Handle(innerEx)) return; - _Logger.LogWarning(innerEx, $"Failed to load export assembly: {exportFile}"); + if (!GlobalExceptionHandler.Handle(innerEx)) + { + continue; + } + _Logger.LogWarning(innerEx, "Failed to load export assembly: {Path}", exportFile); } } } @@ -212,12 +152,10 @@ private void LoadExports() try { resolver.AnalyzeDependencies(_RootDirService.GetPluginsRoot()); - - _Logger.LogInformation(resolver.GetDependencyGraphVisualization()); - + _Logger.LogInformation("{Graph}", resolver.GetDependencyGraphVisualization()); var loadOrder = resolver.GetLoadOrder(); - _Logger.LogInformation($"Loading {loadOrder.Count} export assemblies in dependency order."); + _Logger.LogInformation("Loading {Count} export assemblies in dependency order.", loadOrder.Count); foreach (var exportFile in loadOrder) { @@ -225,9 +163,7 @@ private void LoadExports() { var assembly = Assembly.LoadFrom(exportFile); var exports = assembly.GetTypes(); - - _Logger.LogDebug($"Loaded {exports.Length} types from {Path.GetFileName(exportFile)}."); - + _Logger.LogDebug("Loaded {Count} types from {Path}", exports.Length, Path.GetFileName(exportFile)); foreach (var export in exports) { @@ -236,12 +172,15 @@ private void LoadExports() } catch (Exception ex) { - if (!GlobalExceptionHandler.Handle(ex)) return; - _Logger.LogWarning(ex, $"Failed to load export assembly: {exportFile}"); + if (!GlobalExceptionHandler.Handle(ex)) + { + continue; + } + _Logger.LogWarning(ex, "Failed to load export assembly: {Path}", exportFile); } } - _Logger.LogInformation($"Successfully loaded {_SharedTypes.Count} shared types."); + _Logger.LogInformation("Successfully loaded {Count} shared types.", _SharedTypes.Count); } catch (InvalidOperationException ex) when (ex.Message.Contains("Circular dependency")) { @@ -262,7 +201,10 @@ private void LoadPluginsFromFolder( string directory ) foreach (var pluginDir in pluginDirs) { var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith("[") && dirName.EndsWith("]")) LoadPluginsFromFolder(pluginDir); + if (dirName.StartsWith('[') && dirName.EndsWith(']')) + { + LoadPluginsFromFolder(pluginDir); + } else { try @@ -270,13 +212,16 @@ private void LoadPluginsFromFolder( string directory ) var context = LoadPlugin(pluginDir, false); if (context != null && context.Status == PluginStatus.Loaded) { - _Logger.LogInformation("Loaded plugin " + context.Metadata!.Id); + _Logger.LogInformation("Loaded plugin {Id}", context.Metadata!.Id); } } catch (Exception e) { - if (!GlobalExceptionHandler.Handle(e)) continue; - _Logger.LogWarning(e, "Error loading plugin: " + pluginDir); + if (!GlobalExceptionHandler.Handle(e)) + { + continue; + } + _Logger.LogWarning(e, "Error loading plugin: {Path}", pluginDir); continue; } } @@ -286,7 +231,6 @@ private void LoadPluginsFromFolder( string directory ) private void LoadPlugins() { LoadPluginsFromFolder(_RootDirService.GetPluginsRoot()); - RebuildSharedServices(); } @@ -300,24 +244,20 @@ private void RebuildSharedServices() _InterfaceManager.Dispose(); _Plugins - .Where(p => p.Status == PluginStatus.Loaded) - .ToList() - .ForEach(p => p.Plugin!.ConfigureSharedInterface(_InterfaceManager)); - + .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)); + .Where(p => p.Status == PluginStatus.Loaded) + .ToList() + .ForEach(p => p.Plugin!.UseSharedInterface(_InterfaceManager)); } - public PluginContext? LoadPlugin( string dir, bool hotReload ) { - - PluginContext context = new() { PluginDirectory = dir, Status = PluginStatus.Loading, @@ -328,7 +268,7 @@ private void RebuildSharedServices() if (!File.Exists(entrypointDll)) { - _Logger.LogWarning("Plugin entrypoint DLL not found: " + entrypointDll); + _Logger.LogWarning("Plugin entrypoint DLL not found: {Path}", entrypointDll); context.Status = PluginStatus.Error; return null; } @@ -350,12 +290,10 @@ private void RebuildSharedServices() ); var assembly = loader.LoadDefaultAssembly(); - - var pluginType = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin)))!; - + var pluginType = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); if (pluginType == null) { - _Logger.LogWarning("Plugin type not found: " + entrypointDll); + _Logger.LogWarning("Plugin type not found: {Path}", entrypointDll); context.Status = PluginStatus.Error; return null; } @@ -363,24 +301,19 @@ private void RebuildSharedServices() var metadata = pluginType.GetCustomAttribute(); if (metadata == null) { - _Logger.LogWarning("Plugin metadata not found: " + entrypointDll); + _Logger.LogWarning("Plugin metadata not found: {Path}", entrypointDll); context.Status = PluginStatus.Error; return null; } context.Metadata = metadata; - _DataDirectoryService.EnsurePluginDataDirectory(metadata.Id); var core = new SwiftlyCore(metadata.Id, Path.GetDirectoryName(entrypointDll)!, metadata, pluginType, _Provider, _DataDirectoryService.GetPluginDataDirectory(metadata.Id)); - core.InitializeType(pluginType); - var plugin = (BasePlugin)Activator.CreateInstance(pluginType, [core])!; - core.InitializeObject(plugin); - try { plugin.Load(hotReload); @@ -392,7 +325,16 @@ private void RebuildSharedServices() context.Status = PluginStatus.Error; return null; } - _Logger.LogWarning(e, $"Error loading plugin {entrypointDll}"); + _Logger.LogWarning(e, "Error loading plugin {Path}", entrypointDll); + try + { + plugin.Unload(); + loader?.Dispose(); + core?.Dispose(); + } + catch (Exception) + { + } context.Status = PluginStatus.Error; return null; } @@ -401,18 +343,18 @@ private void RebuildSharedServices() context.Core = core; context.Plugin = plugin; context.Loader = loader; - return context; } public bool UnloadPlugin( string id ) { var context = _Plugins - .Where(p => p.Status == PluginStatus.Loaded) - .FirstOrDefault(p => p.Metadata?.Id == id); + .Where(p => p.Status == PluginStatus.Loaded) + .FirstOrDefault(p => p.Metadata?.Id == id); + if (context == null) { - _Logger.LogWarning("Plugin not found or not loaded: " + id); + _Logger.LogWarning("Plugin not found or not loaded: {Id}", id); return false; } @@ -424,52 +366,38 @@ public bool UnloadPlugin( string id ) public bool LoadPluginById( string id ) { var context = _Plugins - .Where(p => p.Status == PluginStatus.Unloaded) - .FirstOrDefault(p => p.Metadata?.Id == id); - if (context == null) - { - // try to find new plugins - var root = _RootDirService.GetPluginsRoot(); - var pluginDirs = Directory.GetDirectories(root); - foreach (var pluginDir in pluginDirs) - { - if (Path.GetFileName(pluginDir) == id) - { - context = LoadPlugin(pluginDir, false); - break; - } - } - if (context == null) - { - _Logger.LogWarning("Plugin not found: " + id); - return false; - } - } - else + .Where(p => p.Status == PluginStatus.Unloaded) + .FirstOrDefault(p => p.Metadata?.Id == id); + + var result = false; + if (context != null) { var directory = context.PluginDirectory!; _ = _Plugins.Remove(context); _ = LoadPlugin(directory, true); + result = true; } RebuildSharedServices(); - return true; + return result; } public void ReloadPlugin( string id ) { - _Logger.LogInformation("Reloading plugin " + id); + _Logger.LogInformation("Reloading plugin {Id}", id); if (!UnloadPlugin(id)) { + _Logger.LogWarning("Plugin not found or not loaded: {Id}", id); return; } if (!LoadPluginById(id)) { - RebuildSharedServices(); + _Logger.LogWarning("Failed to load plugin {Id}", id); + return; } - _Logger.LogInformation("Reloaded plugin " + id); + _Logger.LogInformation("Reloaded plugin {Id}", id); } } \ No newline at end of file From 6a4be71e6f3006476f56701737beb8193db8e8b3 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Mon, 17 Nov 2025 12:44:52 +0800 Subject: [PATCH 02/12] refactor: Eliminate redundancy and improve code organization in PluginManager#1 --- .../Modules/Plugins/PluginManager.cs | 275 ++++++++---------- 1 file changed, 127 insertions(+), 148 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 613e55d63..b697bf4d2 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -12,15 +12,16 @@ 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 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 FileSystemWatcher? fileWatcher; public PluginManager( IServiceProvider provider, @@ -29,25 +30,56 @@ public PluginManager( 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.fileWatcher = new FileSystemWatcher { Path = rootDirService.GetPluginsRoot(), Filter = "*.dll", IncludeSubdirectories = true, NotifyFilter = NotifyFilters.LastWrite, }; - _Watcher.Changed += HandlePluginChange; - _Watcher.EnableRaisingEvents = true; + this.fileWatcher.EnableRaisingEvents = true; + this.fileWatcher.Changed += ( sender, e ) => + { + try + { + if (!NativeServerHelpers.UseAutoHotReload() || e.ChangeType != WatcherChangeTypes.Changed) + { + return; + } - Initialize(); - } + var directoryName = Path.GetDirectoryName(e.FullPath) ?? string.Empty; + if (string.IsNullOrWhiteSpace(directoryName)) + { + return; + } + + var pluginId = plugins + .FirstOrDefault(p => Path.GetFileName(p?.PluginDirectory) == Path.GetFileName(directoryName)) + ?.Metadata?.Id; + if (!string.IsNullOrWhiteSpace(pluginId)) + { + ReloadPlugin(pluginId); + } + } + catch (Exception ex) + { + if (!GlobalExceptionHandler.Handle(ex)) + { + return; + } + logger.LogError(ex, "Error handling plugin change"); + } + }; - public void Initialize() - { AppDomain.CurrentDomain.AssemblyResolve += ( sender, e ) => { var loadingAssemblyName = new AssemblyName(e.Name).Name ?? string.Empty; @@ -55,164 +87,115 @@ public void Initialize() ? Assembly.GetExecutingAssembly() : AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => loadingAssemblyName == a.GetName().Name); }; + LoadExports(); LoadPlugins(); } - public void HandlePluginChange( object sender, FileSystemEventArgs e ) + private void LoadExports() { - try + void PopulateSharedManually( string startDirectory ) { - if (!NativeServerHelpers.UseAutoHotReload()) - { - return; - } - - // Windows FileSystemWatcher triggers multiple (open, write, close) events for a single file change - if (DateTime.Now - lastRead < TimeSpan.FromSeconds(1)) - { - return; - } - - if (string.IsNullOrWhiteSpace(Path.GetDirectoryName(e.FullPath))) - { - return; - } + var pluginDirs = Directory.GetDirectories(startDirectory); - foreach (var plugin in _Plugins) + foreach (var pluginDir in pluginDirs) { - if (Path.GetFileName(plugin?.PluginDirectory) == Path.GetFileName(Path.GetDirectoryName(e.FullPath))) + var dirName = Path.GetFileName(pluginDir); + if (dirName.StartsWith('[') && dirName.EndsWith(']')) { - var pluginId = plugin?.Metadata?.Id; - if (!string.IsNullOrWhiteSpace(pluginId)) - { - lastRead = DateTime.Now; - ReloadPlugin(pluginId); - } - - break; + PopulateSharedManually(pluginDir); + continue; } - } - } - catch (Exception ex) - { - if (!GlobalExceptionHandler.Handle(ex)) - { - return; - } - _Logger.LogError(ex, "Error handling plugin change"); - } - } - - private void PopulateSharedManually( string startDirectory ) - { - var pluginDirs = Directory.GetDirectories(startDirectory); - foreach (var pluginDir in pluginDirs) - { - var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith('[') && dirName.EndsWith(']')) - { - PopulateSharedManually(pluginDir); - } - else - { - 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) + continue; + } + + 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)) { - continue; + return; } - _Logger.LogWarning(innerEx, "Failed to load export assembly: {Path}", exportFile); + 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("{Graph}", 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 {Count} export assemblies in dependency order.", loadOrder.Count); - - foreach (var exportFile in loadOrder) + loadOrder.ForEach(exportFile => { try { var assembly = Assembly.LoadFrom(exportFile); var exports = assembly.GetTypes(); - _Logger.LogDebug("Loaded {Count} types from {Path}", exports.Length, 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)) { - continue; + return; } - _Logger.LogWarning(ex, "Failed to load export assembly: {Path}", exportFile); + logger.LogWarning(ex, "Failed to load export assembly: {Path}", exportFile); } - } + }); - _Logger.LogInformation("Successfully loaded {Count} shared types.", _SharedTypes.Count); + logger.LogInformation("Successfully loaded {Count} shared types.", sharedTypes.Count); } catch (InvalidOperationException ex) when (ex.Message.Contains("Circular dependency")) { - _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 exports without dependency resolution."); + PopulateSharedManually(rootDirService.GetPluginsRoot()); } catch (Exception ex) { if (!GlobalExceptionHandler.Handle(ex)) return; - _Logger.LogError(ex, "Unexpected error during export loading"); + logger.LogError(ex, "Unexpected error during export loading"); } } - private void LoadPluginsFromFolder( string directory ) + private void LoadPlugins() { - var pluginDirs = Directory.GetDirectories(directory); - - foreach (var pluginDir in pluginDirs) + void LoadPluginsFromFolder( string directory ) { - var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith('[') && dirName.EndsWith(']')) - { - LoadPluginsFromFolder(pluginDir); - } - else + var pluginDirs = Directory.GetDirectories(directory); + + foreach (var pluginDir in pluginDirs) { + var dirName = Path.GetFileName(pluginDir); + if (dirName.StartsWith('[') && dirName.EndsWith(']')) + { + LoadPluginsFromFolder(pluginDir); + continue; + } + try { var context = LoadPlugin(pluginDir, false); - if (context != null && context.Status == PluginStatus.Loaded) + if (context?.Status == PluginStatus.Loaded) { - _Logger.LogInformation("Loaded plugin {Id}", context.Metadata!.Id); + logger.LogInformation("Loaded plugin {Id}", context.Metadata!.Id); } } catch (Exception e) @@ -221,19 +204,15 @@ private void LoadPluginsFromFolder( string directory ) { continue; } - _Logger.LogWarning(e, "Error loading plugin: {Path}", pluginDir); - continue; + logger.LogWarning(e, "Error loading plugin: {Path}", pluginDir); } } } - } - private void LoadPlugins() - { - LoadPluginsFromFolder(_RootDirService.GetPluginsRoot()); + LoadPluginsFromFolder(rootDirService.GetPluginsRoot()); RebuildSharedServices(); - _Plugins + plugins .Where(p => p.Status == PluginStatus.Loaded) .ToList() .ForEach(p => p.Plugin!.OnAllPluginsLoaded()); @@ -241,22 +220,22 @@ private void LoadPlugins() public List GetPlugins() { - return _Plugins; + return plugins; } private void RebuildSharedServices() { - _InterfaceManager.Dispose(); + interfaceManager.Dispose(); - var loadedPlugins = _Plugins + var loadedPlugins = plugins .Where(p => p.Status == PluginStatus.Loaded) .ToList(); - loadedPlugins.ForEach(p => p.Plugin?.ConfigureSharedInterface(_InterfaceManager)); - _InterfaceManager.Build(); + loadedPlugins.ForEach(p => p.Plugin?.ConfigureSharedInterface(interfaceManager)); + interfaceManager.Build(); - loadedPlugins.ForEach(p => p.Plugin?.UseSharedInterface(_InterfaceManager)); - loadedPlugins.ForEach(p => p.Plugin?.OnSharedInterfaceInjected(_InterfaceManager)); + loadedPlugins.ForEach(p => p.Plugin?.UseSharedInterface(interfaceManager)); + loadedPlugins.ForEach(p => p.Plugin?.OnSharedInterfaceInjected(interfaceManager)); } public PluginContext? LoadPlugin( string dir, bool hotReload ) @@ -265,20 +244,20 @@ private void RebuildSharedServices() PluginDirectory = dir, Status = PluginStatus.Loading, }; - _Plugins.Add(context); + plugins.Add(context); var entrypointDll = Path.Combine(dir, Path.GetFileName(dir) + ".dll"); if (!File.Exists(entrypointDll)) { - _Logger.LogWarning("Plugin entrypoint DLL not found: {Path}", entrypointDll); + logger.LogWarning("Plugin entrypoint DLL not found: {Path}", entrypointDll); context.Status = PluginStatus.Error; return null; } var loader = PluginLoader.CreateFromAssemblyFile( assemblyFile: entrypointDll, - sharedTypes: [typeof(BasePlugin), .. _SharedTypes], + sharedTypes: [typeof(BasePlugin), .. sharedTypes], config => { config.IsUnloadable = true; @@ -296,7 +275,7 @@ private void RebuildSharedServices() var pluginType = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); if (pluginType == null) { - _Logger.LogWarning("Plugin type not found: {Path}", entrypointDll); + logger.LogWarning("Plugin type not found: {Path}", entrypointDll); context.Status = PluginStatus.Error; return null; } @@ -304,15 +283,15 @@ private void RebuildSharedServices() var metadata = pluginType.GetCustomAttribute(); if (metadata == null) { - _Logger.LogWarning("Plugin metadata not found: {Path}", entrypointDll); + logger.LogWarning("Plugin metadata not found: {Path}", entrypointDll); context.Status = PluginStatus.Error; return null; } 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 core = new SwiftlyCore(metadata.Id, Path.GetDirectoryName(entrypointDll)!, metadata, pluginType, rootProvider, dataDirectoryService.GetPluginDataDirectory(metadata.Id)); core.InitializeType(pluginType); var plugin = (BasePlugin)Activator.CreateInstance(pluginType, [core])!; core.InitializeObject(plugin); @@ -328,7 +307,7 @@ private void RebuildSharedServices() context.Status = PluginStatus.Error; return null; } - _Logger.LogWarning(e, "Error loading plugin {Path}", entrypointDll); + logger.LogWarning(e, "Error loading plugin {Path}", entrypointDll); try { plugin.Unload(); @@ -351,13 +330,13 @@ private void RebuildSharedServices() public bool UnloadPlugin( string id ) { - var context = _Plugins + 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}", id); + logger.LogWarning("Plugin not found or not loaded: {Id}", id); return false; } @@ -368,7 +347,7 @@ public bool UnloadPlugin( string id ) public bool LoadPluginById( string id ) { - var context = _Plugins + var context = plugins .Where(p => p.Status == PluginStatus.Unloaded) .FirstOrDefault(p => p.Metadata?.Id == id); @@ -376,7 +355,7 @@ public bool LoadPluginById( string id ) if (context != null) { var directory = context.PluginDirectory!; - _ = _Plugins.Remove(context); + _ = plugins.Remove(context); _ = LoadPlugin(directory, true); result = true; } @@ -387,20 +366,20 @@ public bool LoadPluginById( string id ) public void ReloadPlugin( string id ) { - _Logger.LogInformation("Reloading plugin {Id}", id); + logger.LogInformation("Reloading plugin {Id}", id); if (!UnloadPlugin(id)) { - _Logger.LogWarning("Plugin not found or not loaded: {Id}", id); + logger.LogWarning("Plugin not found or not loaded: {Id}", id); return; } if (!LoadPluginById(id)) { - _Logger.LogWarning("Failed to load plugin {Id}", id); + logger.LogWarning("Failed to load plugin {Id}", id); return; } - _Logger.LogInformation("Reloaded plugin {Id}", id); + logger.LogInformation("Reloaded plugin {Id}", id); } } \ No newline at end of file From 50d858e795d1dadde3db5bd4bb0ee8828f0eb041 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Mon, 17 Nov 2025 18:59:49 +0800 Subject: [PATCH 03/12] refactor: Eliminate redundancy and improve code organization in PluginManager#2 --- .../Modules/Plugins/PluginManager.cs | 299 +++++++++--------- .../Services/CoreCommandService.cs | 4 +- 2 files changed, 150 insertions(+), 153 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index b697bf4d2..85868a368 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -67,7 +67,7 @@ DataDirectoryService dataDirectoryService ?.Metadata?.Id; if (!string.IsNullOrWhiteSpace(pluginId)) { - ReloadPlugin(pluginId); + ReloadPlugin(pluginId, true); } } catch (Exception ex) @@ -92,6 +92,153 @@ DataDirectoryService dataDirectoryService LoadPlugins(); } + public IReadOnlyList GetPlugins() => plugins.AsReadOnly(); + + public 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; + } + + 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)) + { + return FailWithError(context, $"Plugin entrypoint DLL not found: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + + var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); + var loader = PluginLoader.CreateFromAssemblyFile( + entrypointDll, + [typeof(BasePlugin), .. sharedTypes], + config => + { + config.IsUnloadable = config.LoadInMemory = true; + if (currentContext != null) + { + (config.DefaultContext, config.PreferSharedTypes) = (currentContext, true); + } + } + ); + + var pluginType = loader.LoadDefaultAssembly() + .GetTypes() + .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); + if (pluginType == null) + { + return FailWithError(context, $"Plugin type not found: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + + var metadata = pluginType.GetCustomAttribute(); + if (metadata == null) + { + return FailWithError(context, $"Plugin metadata not found: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + + context.Metadata = metadata; + dataDirectoryService.EnsurePluginDataDirectory(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) + { + _ = GlobalExceptionHandler.Handle(e); + + try + { + plugin.Unload(); + loader?.Dispose(); + core?.Dispose(); + } + catch { } + + return FailWithError(context, $"Error loading plugin {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + } + + public bool UnloadPluginById( string id, bool silent = false ) + { + var context = plugins + .Where(p => p.Status != PluginStatus.Unloaded) + .FirstOrDefault(p => p.Metadata?.Id == id); + + try + { + context?.Dispose(); + context?.Loader?.Dispose(); + context?.Core?.Dispose(); + context!.Status = PluginStatus.Unloaded; + return true; + } + catch + { + if (!silent) + { + logger.LogWarning("Error unloading plugin: {Id}", id); + } + return false; + } + } + + public bool LoadPluginById( string id, bool silent = false ) + { + var context = plugins + .Where(p => p.Status == PluginStatus.Unloaded || p.Status == PluginStatus.Error) + .FirstOrDefault(p => p.Metadata?.Id == id); + + if (context != null) + { + _ = plugins.Remove(context); + _ = LoadPlugin(context.PluginDirectory!, true, silent); + RebuildSharedServices(); + return true; + } + else + { + RebuildSharedServices(); + return false; + } + } + + public void ReloadPlugin( string id, bool silent = false ) + { + logger.LogInformation("Reloading plugin {Id}", id); + + _ = UnloadPluginById(id, silent); + + if (!LoadPluginById(id, silent)) + { + logger.LogError("Failed to reload plugin {Id}", id); + } + else + { + logger.LogInformation("Reloaded plugin {Id}", id); + } + } + private void LoadExports() { void PopulateSharedManually( string startDirectory ) @@ -218,11 +365,6 @@ void LoadPluginsFromFolder( string directory ) .ForEach(p => p.Plugin!.OnAllPluginsLoaded()); } - public List GetPlugins() - { - return plugins; - } - private void RebuildSharedServices() { interfaceManager.Dispose(); @@ -237,149 +379,4 @@ private void RebuildSharedServices() loadedPlugins.ForEach(p => p.Plugin?.UseSharedInterface(interfaceManager)); loadedPlugins.ForEach(p => p.Plugin?.OnSharedInterfaceInjected(interfaceManager)); } - - public PluginContext? LoadPlugin( string dir, bool hotReload ) - { - PluginContext context = new() { - 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: {Path}", entrypointDll); - context.Status = PluginStatus.Error; - return null; - } - - var loader = PluginLoader.CreateFromAssemblyFile( - assemblyFile: entrypointDll, - sharedTypes: [typeof(BasePlugin), .. sharedTypes], - config => - { - config.IsUnloadable = true; - config.LoadInMemory = true; - var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); - if (currentContext != null) - { - config.DefaultContext = currentContext; - config.PreferSharedTypes = true; - } - } - ); - - var assembly = loader.LoadDefaultAssembly(); - var pluginType = assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); - if (pluginType == null) - { - logger.LogWarning("Plugin type not found: {Path}", entrypointDll); - context.Status = PluginStatus.Error; - return null; - } - - var metadata = pluginType.GetCustomAttribute(); - if (metadata == null) - { - logger.LogWarning("Plugin metadata not found: {Path}", entrypointDll); - context.Status = PluginStatus.Error; - return null; - } - - context.Metadata = metadata; - dataDirectoryService.EnsurePluginDataDirectory(metadata.Id); - - var core = new SwiftlyCore(metadata.Id, Path.GetDirectoryName(entrypointDll)!, metadata, pluginType, rootProvider, dataDirectoryService.GetPluginDataDirectory(metadata.Id)); - core.InitializeType(pluginType); - var plugin = (BasePlugin)Activator.CreateInstance(pluginType, [core])!; - core.InitializeObject(plugin); - - try - { - plugin.Load(hotReload); - } - catch (Exception e) - { - if (!GlobalExceptionHandler.Handle(e)) - { - context.Status = PluginStatus.Error; - return null; - } - logger.LogWarning(e, "Error loading plugin {Path}", entrypointDll); - try - { - plugin.Unload(); - loader?.Dispose(); - core?.Dispose(); - } - catch (Exception) - { - } - context.Status = PluginStatus.Error; - return null; - } - - context.Status = PluginStatus.Loaded; - context.Core = core; - context.Plugin = plugin; - context.Loader = loader; - return context; - } - - public bool UnloadPlugin( string id ) - { - 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}", id); - return false; - } - - context.Dispose(); - context.Status = PluginStatus.Unloaded; - return true; - } - - public bool LoadPluginById( string id ) - { - var context = plugins - .Where(p => p.Status == PluginStatus.Unloaded) - .FirstOrDefault(p => p.Metadata?.Id == id); - - var result = false; - if (context != null) - { - var directory = context.PluginDirectory!; - _ = plugins.Remove(context); - _ = LoadPlugin(directory, true); - result = true; - } - - RebuildSharedServices(); - return result; - } - - public void ReloadPlugin( string id ) - { - logger.LogInformation("Reloading plugin {Id}", id); - - if (!UnloadPlugin(id)) - { - logger.LogWarning("Plugin not found or not loaded: {Id}", id); - return; - } - - if (!LoadPluginById(id)) - { - logger.LogWarning("Failed to load plugin {Id}", id); - return; - } - - logger.LogInformation("Reloaded plugin {Id}", id); - } } \ 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..5f31ffcf9 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -272,7 +272,7 @@ private void PluginCommand( ICommandContext context ) _Logger.LogWarning("Usage: sw plugins unload "); return; } - _PluginManager.UnloadPlugin(args[2]); + _PluginManager.UnloadPluginById(args[2]); break; case "reload": if (args.Length < 3) @@ -280,7 +280,7 @@ private void PluginCommand( ICommandContext context ) _Logger.LogWarning("Usage: sw plugins reload "); return; } - _PluginManager.ReloadPlugin(args[2]); + _PluginManager.ReloadPlugin(args[2], true); break; default: _Logger.LogWarning("Unknown command"); From cf886a345c9d610c17e7c3ba4e477380755d2a53 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Mon, 17 Nov 2025 23:06:37 +0800 Subject: [PATCH 04/12] refactor: Eliminate redundancy and improve code organization in PluginManager#3 --- .../Modules/Plugins/PluginManager.cs | 68 ++- .../Services/CoreCommandService.cs | 502 +++++++++--------- 2 files changed, 292 insertions(+), 278 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 85868a368..ce6ed1e01 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -44,9 +44,8 @@ DataDirectoryService dataDirectoryService Filter = "*.dll", IncludeSubdirectories = true, NotifyFilter = NotifyFilters.LastWrite, + EnableRaisingEvents = true }; - - this.fileWatcher.EnableRaisingEvents = true; this.fileWatcher.Changed += ( sender, e ) => { try @@ -67,7 +66,7 @@ DataDirectoryService dataDirectoryService ?.Metadata?.Id; if (!string.IsNullOrWhiteSpace(pluginId)) { - ReloadPlugin(pluginId, true); + ReloadPluginById(pluginId, true); } } catch (Exception ex) @@ -76,7 +75,7 @@ DataDirectoryService dataDirectoryService { return; } - logger.LogError(ex, "Error handling plugin change"); + logger.LogError(ex, "Failed to handle plugin change"); } }; @@ -112,7 +111,7 @@ DataDirectoryService dataDirectoryService var entrypointDll = Path.Combine(dir, Path.GetFileName(dir) + ".dll"); if (!File.Exists(entrypointDll)) { - return FailWithError(context, $"Plugin entrypoint DLL not found: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + return FailWithError(context, $"Failed to find plugin entrypoint DLL: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); @@ -134,13 +133,13 @@ DataDirectoryService dataDirectoryService .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); if (pluginType == null) { - return FailWithError(context, $"Plugin type not found: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + return FailWithError(context, $"Failed to find plugin type: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } var metadata = pluginType.GetCustomAttribute(); if (metadata == null) { - return FailWithError(context, $"Plugin metadata not found: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + return FailWithError(context, $"Failed to find plugin metadata: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } context.Metadata = metadata; @@ -175,7 +174,7 @@ DataDirectoryService dataDirectoryService } catch { } - return FailWithError(context, $"Error loading plugin {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + return FailWithError(context, $"Failed to load plugin: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); } } @@ -197,10 +196,14 @@ public bool UnloadPluginById( string id, bool silent = false ) { if (!silent) { - logger.LogWarning("Error unloading plugin: {Id}", id); + logger.LogWarning("Failed to unload plugin by Id: {Id}", id); } return false; } + finally + { + RebuildSharedServices(); + } } public bool LoadPluginById( string id, bool silent = false ) @@ -209,33 +212,39 @@ public bool LoadPluginById( string id, bool silent = false ) .Where(p => p.Status == PluginStatus.Unloaded || p.Status == PluginStatus.Error) .FirstOrDefault(p => p.Metadata?.Id == id); - if (context != null) + try { - _ = plugins.Remove(context); - _ = LoadPlugin(context.PluginDirectory!, true, silent); - RebuildSharedServices(); + if (plugins.Remove(context!)) + { + _ = LoadPlugin(context!.PluginDirectory!, true, silent); + } return true; } - else + catch { - RebuildSharedServices(); + if (!silent) + { + logger.LogWarning("Failed to load plugin by Id: {Id}", id); + } return false; } + finally + { + RebuildSharedServices(); + } } - public void ReloadPlugin( string id, bool silent = false ) + public void ReloadPluginById( string id, bool silent = false ) { - logger.LogInformation("Reloading plugin {Id}", id); - _ = UnloadPluginById(id, silent); if (!LoadPluginById(id, silent)) { - logger.LogError("Failed to reload plugin {Id}", id); + logger.LogWarning("Failed to reload plugin by Id: {Id}", id); } else { - logger.LogInformation("Reloaded plugin {Id}", id); + logger.LogInformation("Reloaded plugin by Id: {Id}", id); } } @@ -285,9 +294,9 @@ void PopulateSharedManually( string startDirectory ) { var resolver = new DependencyResolver(logger); resolver.AnalyzeDependencies(rootDirService.GetPluginsRoot()); - logger.LogInformation("{Graph}", resolver.GetDependencyGraphVisualization()); + logger.LogInformation("{Graph}\n", resolver.GetDependencyGraphVisualization()); var loadOrder = resolver.GetLoadOrder(); - logger.LogInformation("Loading {Count} export assemblies in dependency order.", loadOrder.Count); + // logger.LogInformation("Loading {Count} export assemblies in dependency order", loadOrder.Count); loadOrder.ForEach(exportFile => { @@ -308,17 +317,20 @@ void PopulateSharedManually( string startDirectory ) } }); - logger.LogInformation("Successfully loaded {Count} shared types.", sharedTypes.Count); + logger.LogInformation("Loaded {Count} shared types", sharedTypes.Count); } catch (InvalidOperationException ex) when (ex.Message.Contains("Circular dependency")) { - logger.LogError(ex, "Circular dependency detected in plugin exports. Loading exports without dependency resolution."); + 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"); } } @@ -342,7 +354,7 @@ void LoadPluginsFromFolder( string directory ) var context = LoadPlugin(pluginDir, false); if (context?.Status == PluginStatus.Loaded) { - logger.LogInformation("Loaded plugin {Id}", context.Metadata!.Id); + logger.LogInformation("Loaded plugin: {Id}", context.Metadata!.Id); } } catch (Exception e) @@ -351,7 +363,7 @@ void LoadPluginsFromFolder( string directory ) { continue; } - logger.LogWarning(e, "Error loading plugin: {Path}", pluginDir); + logger.LogWarning(e, "Failed to load plugin: {Path}", pluginDir); } } } diff --git a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs index 5f31ffcf9..d5250aace 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -1,290 +1,292 @@ -using System.Reflection; using System.Runtime; +using System.Reflection; using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; using Spectre.Console; +using Microsoft.Extensions.Logging; +using SwiftlyS2.Shared; using SwiftlyS2.Core.Natives; using SwiftlyS2.Core.Plugins; -using SwiftlyS2.Shared; 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; + private readonly ILogger logger; + private readonly ISwiftlyCore core; + private readonly PluginManager pluginManager; + private readonly ProfileService profileService; - 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) + public CoreCommandService( ILogger logger, ISwiftlyCore core, PluginManager pluginManager, ProfileService profileService ) { - if (!GlobalExceptionHandler.Handle(e)) return; - _Logger.LogError(e, "Error executing command"); + this.logger = logger; + this.core = core; + this.pluginManager = pluginManager; + this.profileService = profileService; + _ = core.Command.RegisterCommand("sw", OnCommand, true); } - } - 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) + private void OnCommand( ICommandContext context ) { - 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); - } + try + { + if (context.IsSentByPlayer) + { + return; + } - private void ConfilterCommand( ICommandContext context ) - { - var args = context.Args; - if (args.Length == 1) - { - 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; - } + var args = context.Args; + if (args.Length == 0) + { + ShowHelp(context); + return; + } - switch (args[1]) - { - 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; + 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.GlobalVars.MaxClients}"; + 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 (var 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 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"); + table = table.AddRow("credits", "List Swiftly credits"); + table = table.AddRow("help", "Show the help for Swiftly Commands"); + table = table.AddRow("list", "Show the list of online players"); + table = table.AddRow("status", "Show the status of the server"); + if (!context.IsSentByPlayer) + { + table = table.AddRow("confilter", "Console Filter Menu"); + table = table.AddRow("plugins", "Plugin Management Menu"); + table = table.AddRow("gc", "Show garbage collection information on managed"); + table = table.AddRow("profiler", "Profiler Menu"); + } + table = 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"))) + var args = context.Args; + if (args.Length == 1) { - Directory.CreateDirectory(Path.Combine(basePath, "profilers")); + var table = new Table().AddColumn("Command").AddColumn("Description"); + table = table.AddRow("enable", "Enable console filtering"); + table = table.AddRow("disable", "Disable console filtering"); + table = table.AddRow("status", "Show the status of the console filter"); + table = table.AddRow("reload", "Reload console filter configuration"); + AnsiConsole.Write(table); + return; } - 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; - } - } - - private void PluginCommand( ICommandContext context ) - { - var args = context.Args; - if (args.Length == 1) - { - 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; + switch (args[1]) + { + 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; + } } - switch (args[1]) + private void ProfilerCommand( ICommandContext context ) { - case "list": - var table = new Table().AddColumn("Name").AddColumn("Status").AddColumn("Version").AddColumn("Author").AddColumn("Website"); - foreach (var plugin in _PluginManager.GetPlugins()) + var args = context.Args; + if (args.Length == 1) { - table.AddRow(plugin.Metadata?.Id ?? "", plugin.Status?.ToString() ?? "Unknown", plugin.Metadata?.Version ?? "", plugin.Metadata?.Author ?? "", plugin.Metadata?.Website ?? ""); + var table = new Table().AddColumn("Command").AddColumn("Description"); + table = table.AddRow("enable", "Enable the profiler"); + table = table.AddRow("disable", "Disable the profiler"); + table = table.AddRow("status", "Show the status of the profiler"); + table = table.AddRow("save", "Save the profiler data to a file"); + AnsiConsole.Write(table); + return; } - AnsiConsole.Write(table); - break; - case "load": - if (args.Length < 3) + + switch (args[1]) { - _Logger.LogWarning("Usage: sw plugins load "); - return; + 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"))) + { + _ = Directory.CreateDirectory(Path.Combine(basePath, "profilers")); + } + + var 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; } - _PluginManager.LoadPluginById(args[2]); - break; - case "unload": - if (args.Length < 3) + } + + private void PluginCommand( ICommandContext context ) + { + var args = context.Args; + if (args.Length == 1) { - _Logger.LogWarning("Usage: sw plugins unload "); - return; + var table = new Table().AddColumn("Command").AddColumn("Description"); + table = table.AddRow("list", "List all plugins"); + table = table.AddRow("load", "Load a plugin"); + table = table.AddRow("unload", "Unload a plugin"); + table = table.AddRow("reload", "Reload a plugin"); + AnsiConsole.Write(table); + return; } - _PluginManager.UnloadPluginById(args[2]); - break; - case "reload": - if (args.Length < 3) + + switch (args[1]) { - _Logger.LogWarning("Usage: sw plugins reload "); - return; + case "list": + var table = new Table().AddColumn("Name").AddColumn("Status").AddColumn("Version").AddColumn("Author").AddColumn("Website"); + foreach (var plugin in pluginManager.GetPlugins()) + { + table = table.AddRow(plugin.Metadata?.Id ?? "", plugin.Status?.ToString() ?? "Unknown", plugin.Metadata?.Version ?? "", plugin.Metadata?.Author ?? "", plugin.Metadata?.Website ?? ""); + } + AnsiConsole.Write(table); + break; + case "load": + if (args.Length < 3) + { + logger.LogWarning("Usage: sw plugins load "); + return; + } + _ = pluginManager.LoadPluginById(args[2]); + break; + case "unload": + if (args.Length < 3) + { + logger.LogWarning("Usage: sw plugins unload "); + return; + } + _ = pluginManager.UnloadPluginById(args[2]); + break; + case "reload": + if (args.Length < 3) + { + logger.LogWarning("Usage: sw plugins reload "); + return; + } + pluginManager.ReloadPluginById(args[2], true); + break; + default: + logger.LogWarning("Unknown command"); + break; } - _PluginManager.ReloadPlugin(args[2], true); - break; - default: - _Logger.LogWarning("Unknown command"); - break; } - } } \ No newline at end of file From eb58730dcc76f5a2667c4c55c2edb05dca9e2ae6 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 10:44:14 +0800 Subject: [PATCH 05/12] refactor: Improve CoreCommandService readability and code quality --- .../Services/CoreCommandService.cs | 352 ++++++++++++------ 1 file changed, 231 insertions(+), 121 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs index d5250aace..6e20ffa81 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -4,8 +4,8 @@ using Spectre.Console; using Microsoft.Extensions.Logging; using SwiftlyS2.Shared; -using SwiftlyS2.Core.Natives; using SwiftlyS2.Core.Plugins; +using SwiftlyS2.Core.Natives; using SwiftlyS2.Shared.Commands; namespace SwiftlyS2.Core.Services; @@ -15,19 +15,90 @@ internal class CoreCommandService 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, ProfileService profileService ) + public CoreCommandService( ILogger logger, ISwiftlyCore core, PluginManager pluginManager, RootDirService rootDirService, ProfileService profileService ) { this.logger = logger; this.core = core; this.pluginManager = pluginManager; + this.rootDirService = rootDirService; this.profileService = profileService; _ = core.Command.RegisterCommand("sw", OnCommand, true); } private void OnCommand( ICommandContext context ) { + 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) @@ -42,83 +113,33 @@ private void OnCommand( ICommandContext context ) return; } - switch (args[0]) + switch (args[0].Trim().ToLower()) { 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"); + ShowCredits(); break; case "list": - var players = core.PlayerManager.GetAllPlayers(); - var outString = $"Connected players: {core.PlayerManager.PlayerCount}/{core.Engine.GlobalVars.MaxClients}"; - foreach (var player in players) - { - outString += $"\n{player.PlayerID}. {player.Controller?.PlayerName}{(player.IsFakeClient ? " (BOT)" : "")} (steamid={player.SteamID})"; - } - logger.LogInformation(outString); + ShowPlayerList(); 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); + ShowServerStatus(); 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); + ShowVersionInfo(); 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 (var i = 0; i <= GC.MaxGeneration; i++) - { - outGc += $"\n - Generation {i} Collection Count: {GC.CollectionCount(i)}"; - } - outGc += $"\n - Latency Mode: {GCSettings.LatencyMode}"; - logger.LogInformation(outGc); + case "gc" when RequireConsoleAccess(): + ShowGarbageCollectionInfo(); break; - case "plugins": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } + case "plugins" when RequireConsoleAccess(): PluginCommand(context); break; - case "profiler": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } + case "profiler" when RequireConsoleAccess(): ProfilerCommand(context); break; - case "confilter": - if (context.IsSentByPlayer) - { - context.Reply("This command can only be executed from the server console."); - return; - } + case "confilter" when RequireConsoleAccess(): ConfilterCommand(context); break; default: @@ -132,58 +153,99 @@ SwiftlyS2 is licensed under the GNU General Public License v3.0 or later. { return; } - logger.LogError(e, "Error executing command"); + logger.LogError(e, "Failed to execute command"); } } private static void ShowHelp( ICommandContext context ) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table = table.AddRow("credits", "List Swiftly credits"); - table = table.AddRow("help", "Show the help for Swiftly Commands"); - table = table.AddRow("list", "Show the list of online players"); - table = table.AddRow("status", "Show the status of the server"); + 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 = table.AddRow("confilter", "Console Filter Menu"); - table = table.AddRow("plugins", "Plugin Management Menu"); - table = table.AddRow("gc", "Show garbage collection information on managed"); - table = table.AddRow("profiler", "Profiler Menu"); + _ = table + .AddRow("confilter", "Console Filter Menu") + .AddRow("plugins", "Plugin Management Menu") + .AddRow("gc", "Show garbage collection information on managed") + .AddRow("profiler", "Profiler Menu"); } - table = table.AddRow("version", "Display Swiftly version"); + _ = table.AddRow("version", "Display Swiftly version"); AnsiConsole.Write(table); } private void ConfilterCommand( ICommandContext context ) { + 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() + { + if (!core.ConsoleOutput.IsFilterEnabled()) + { + core.ConsoleOutput.ToggleFilter(); + } + logger.LogInformation("Console filtering has been enabled."); + } + + 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) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table = table.AddRow("enable", "Enable console filtering"); - table = table.AddRow("disable", "Disable console filtering"); - table = table.AddRow("status", "Show the status of the console filter"); - table = table.AddRow("reload", "Reload console filter configuration"); - AnsiConsole.Write(table); + ShowConfilterHelp(); return; } - switch (args[1]) + switch (args[1].Trim().ToLower()) { case "enable": - if (!core.ConsoleOutput.IsFilterEnabled()) core.ConsoleOutput.ToggleFilter(); - logger.LogInformation("Console filtering has been enabled."); + EnableFilter(); break; case "disable": - if (core.ConsoleOutput.IsFilterEnabled()) core.ConsoleOutput.ToggleFilter(); - logger.LogInformation("Console filtering has been disabled."); + DisableFilter(); 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()}"); + ShowFilterStatus(); break; case "reload": - core.ConsoleOutput.ReloadFilterConfiguration(); - logger.LogInformation("Console filter configuration reloaded."); + ReloadFilter(); break; default: logger.LogWarning("Unknown command"); @@ -205,7 +267,7 @@ private void ProfilerCommand( ICommandContext context ) return; } - switch (args[1]) + switch (args[1].Trim().ToLower()) { case "enable": profileService.Enable(); @@ -216,19 +278,22 @@ private void ProfilerCommand( ICommandContext context ) logger.LogInformation("The profiler has been disabled."); break; case "status": - logger.LogInformation($"Profiler is currently {(profileService.IsEnabled() ? "enabled" : "disabled")}."); + logger.LogInformation("Profiler is currently {Status}.", (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"))) + var pluginId = args.Length >= 3 ? args[2] : "core"; + var profilerDir = Path.Combine(rootDirService.GetRoot(), "profilers"); + + if (!Directory.Exists(profilerDir)) { - _ = Directory.CreateDirectory(Path.Combine(basePath, "profilers")); + _ = Directory.CreateDirectory(profilerDir); } - var 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")}"); + 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"); @@ -238,51 +303,96 @@ private void ProfilerCommand( ICommandContext context ) private void PluginCommand( ICommandContext context ) { + void ShowPluginList() + { + 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); + } + + 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 ) + { + if (args.Length >= 3) + { + return true; + } + logger.LogWarning("Usage: sw plugins {Command} {Usage}", command, usage); + return false; + } + + 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", + _ => "Unknown" + }; + var args = context.Args; if (args.Length == 1) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table = table.AddRow("list", "List all plugins"); - table = table.AddRow("load", "Load a plugin"); - table = table.AddRow("unload", "Unload a plugin"); - table = table.AddRow("reload", "Reload a plugin"); - AnsiConsole.Write(table); + ShowPluginHelp(); return; } - switch (args[1]) + switch (args[1].Trim().ToLower()) { case "list": - var table = new Table().AddColumn("Name").AddColumn("Status").AddColumn("Version").AddColumn("Author").AddColumn("Website"); - foreach (var plugin in pluginManager.GetPlugins()) - { - table = table.AddRow(plugin.Metadata?.Id ?? "", plugin.Status?.ToString() ?? "Unknown", plugin.Metadata?.Version ?? "", plugin.Metadata?.Author ?? "", plugin.Metadata?.Website ?? ""); - } - AnsiConsole.Write(table); + ShowPluginList(); break; case "load": - if (args.Length < 3) + if (ValidatePluginId(args, "load", "")) { - logger.LogWarning("Usage: sw plugins load "); - return; + _ = pluginManager.LoadPluginById(args[2]); } - _ = pluginManager.LoadPluginById(args[2]); break; case "unload": - if (args.Length < 3) + if (ValidatePluginId(args, "unload", "/")) { - logger.LogWarning("Usage: sw plugins unload "); - return; + _ = pluginManager.UnloadPluginById(args[2]); } - _ = pluginManager.UnloadPluginById(args[2]); break; case "reload": - if (args.Length < 3) + if (ValidatePluginId(args, "reload", "/")) { - logger.LogWarning("Usage: sw plugins reload "); - return; + pluginManager.ReloadPluginById(args[2], true); } - pluginManager.ReloadPluginById(args[2], true); break; default: logger.LogWarning("Unknown command"); From a417b2b2e2eb70db68225f4902c59f25e1f335f9 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 11:12:02 +0800 Subject: [PATCH 06/12] fix: Improve plugin status handling --- .../Modules/Plugins/PluginManager.cs | 24 ++++++++++++++----- .../Modules/Plugins/PluginStatus.cs | 13 +++++----- .../Services/CoreCommandService.cs | 1 + 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index ce6ed1e01..0ed2a57b7 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -196,7 +196,11 @@ public bool UnloadPluginById( string id, bool silent = false ) { if (!silent) { - logger.LogWarning("Failed to unload plugin by Id: {Id}", id); + logger.LogWarning("Failed to unload plugin by id: {Id}", id); + } + if (context != null) + { + context.Status = PluginStatus.Indeterminate; } return false; } @@ -209,7 +213,7 @@ public bool UnloadPluginById( string id, bool silent = false ) public bool LoadPluginById( string id, bool silent = false ) { var context = plugins - .Where(p => p.Status == PluginStatus.Unloaded || p.Status == PluginStatus.Error) + .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded) .FirstOrDefault(p => p.Metadata?.Id == id); try @@ -217,14 +221,22 @@ public bool LoadPluginById( string id, bool silent = false ) if (plugins.Remove(context!)) { _ = LoadPlugin(context!.PluginDirectory!, true, silent); + return true; + } + else + { + throw new ArgumentException(string.Empty, string.Empty); } - return true; } catch { if (!silent) { - logger.LogWarning("Failed to load plugin by Id: {Id}", id); + logger.LogWarning("Failed to load plugin by id: {Id}", id); + } + if (context != null) + { + context.Status = PluginStatus.Indeterminate; } return false; } @@ -240,11 +252,11 @@ public void ReloadPluginById( string id, bool silent = false ) if (!LoadPluginById(id, silent)) { - logger.LogWarning("Failed to reload plugin by Id: {Id}", id); + logger.LogWarning("Failed to reload plugin by id: {Id}", id); } else { - logger.LogInformation("Reloaded plugin by Id: {Id}", id); + logger.LogInformation("Reloaded plugin by id: {Id}", id); } } 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 6e20ffa81..6d8f43e68 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -361,6 +361,7 @@ bool ValidatePluginId( string[] args, string command, string usage ) PluginStatus.Error => "Error", PluginStatus.Loading => "Loading", PluginStatus.Unloaded => "Unloaded", + PluginStatus.Indeterminate => "Indeterminate", _ => "Unknown" }; From 4224b9266b1d7bcb3ad644901421f08a7cfcfaf0 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 14:54:22 +0800 Subject: [PATCH 07/12] refactor: Enhanced plugin management system with better adaptability --- .../Modules/Plugins/PluginManager.cs | 384 +++++++++++------- .../Services/CoreCommandService.cs | 43 +- managed/src/TestPlugin/TestPlugin.cs | 2 +- 3 files changed, 283 insertions(+), 146 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 0ed2a57b7..5bbc36252 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -55,18 +55,10 @@ DataDirectoryService dataDirectoryService return; } - var directoryName = Path.GetDirectoryName(e.FullPath) ?? string.Empty; + var directoryName = Path.GetFileName(Path.GetDirectoryName(e.FullPath)) ?? string.Empty; if (string.IsNullOrWhiteSpace(directoryName)) { - return; - } - - var pluginId = plugins - .FirstOrDefault(p => Path.GetFileName(p?.PluginDirectory) == Path.GetFileName(directoryName)) - ?.Metadata?.Id; - if (!string.IsNullOrWhiteSpace(pluginId)) - { - ReloadPluginById(pluginId, true); + _ = ReloadPluginByDllName(directoryName, true); } } catch (Exception ex) @@ -93,96 +85,40 @@ DataDirectoryService dataDirectoryService public IReadOnlyList GetPlugins() => plugins.AsReadOnly(); - public PluginContext? LoadPlugin( string dir, bool hotReload, bool silent = false ) + public string? FindPluginDirectoryByDllName( string dllName ) { - PluginContext? FailWithError( PluginContext context, string message ) - { - if (!silent) - { - logger.LogWarning("{Message}", message); - } - context.Status = PluginStatus.Error; - return null; - } - - 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)) + dllName = dllName.Trim(); + if (dllName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) { - return FailWithError(context, $"Failed to find plugin entrypoint DLL: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + dllName = dllName[..^4]; } - var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); - var loader = PluginLoader.CreateFromAssemblyFile( - entrypointDll, - [typeof(BasePlugin), .. sharedTypes], - config => - { - config.IsUnloadable = config.LoadInMemory = true; - if (currentContext != null) - { - (config.DefaultContext, config.PreferSharedTypes) = (currentContext, true); - } - } - ); - - var pluginType = loader.LoadDefaultAssembly() - .GetTypes() - .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); - if (pluginType == null) - { - return FailWithError(context, $"Failed to find plugin type: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); - } + var pluginDir = plugins + .FirstOrDefault(p => Path.GetFileName(p.PluginDirectory)?.Trim().Equals(dllName.Trim()) ?? false) + ?.PluginDirectory; - var metadata = pluginType.GetCustomAttribute(); - if (metadata == null) + if (!string.IsNullOrWhiteSpace(pluginDir)) { - return FailWithError(context, $"Failed to find plugin metadata: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + return pluginDir; } - context.Metadata = metadata; - dataDirectoryService.EnsurePluginDataDirectory(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 + string? foundDir = null; + EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), dir => { - plugin.Load(hotReload); - context.Status = PluginStatus.Loaded; - context.Core = core; - context.Plugin = plugin; - context.Loader = loader; - return context; - } - catch (Exception e) - { - _ = GlobalExceptionHandler.Handle(e); - - try + if (Path.GetFileName(dir).Equals(dllName)) { - plugin.Unload(); - loader?.Dispose(); - core?.Dispose(); + foundDir = dir; } - catch { } + }); - return FailWithError(context, $"Failed to load plugin: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); - } + return foundDir; } public bool UnloadPluginById( string id, bool silent = false ) { var context = plugins .Where(p => p.Status != PluginStatus.Unloaded) - .FirstOrDefault(p => p.Metadata?.Id == id); + .FirstOrDefault(p => p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false); try { @@ -210,29 +146,100 @@ public bool UnloadPluginById( string id, bool silent = false ) } } + public bool UnloadPluginByDllName( string dllName, bool silent = false ) + { + var pluginDir = FindPluginDirectoryByDllName(dllName); + if (string.IsNullOrWhiteSpace(pluginDir)) + { + if (!silent) + { + 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); + + if (string.IsNullOrWhiteSpace(context?.Metadata?.Id)) + { + if (!silent) + { + logger.LogWarning("Failed to find plugin by name: {DllName}", dllName); + } + return false; + } + + return UnloadPluginById(context.Metadata.Id, silent); + } + 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 == id); + .FirstOrDefault(p => p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false); + + if (string.IsNullOrWhiteSpace(context?.PluginDirectory)) + { + if (!silent) + { + logger.LogWarning("Failed to load plugin by id: {Id}", id); + } + return false; + } + + return LoadPluginByDllName(Path.GetFileName(context.PluginDirectory), silent); + } + + 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 context = plugins + .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded) + .FirstOrDefault(p => p.PluginDirectory?.Trim().Equals(pluginDir.Trim()) ?? false); try { - if (plugins.Remove(context!)) + if (context != null && plugins.Remove(context)) { - _ = LoadPlugin(context!.PluginDirectory!, true, silent); - return true; + var newContext = LoadPlugin(pluginDir, true, silent); + if (newContext?.Status == PluginStatus.Loaded) + { + if (!silent) + { + logger.LogInformation("Loaded plugin: {Id}", context.Metadata!.Id); + } + return true; + } + else + { + throw new ArgumentException(string.Empty, string.Empty); + } } else { throw new ArgumentException(string.Empty, string.Empty); } } - catch + catch (Exception e) { + if (!GlobalExceptionHandler.Handle(e)) + { + return false; + } if (!silent) { - logger.LogWarning("Failed to load plugin by id: {Id}", id); + logger.LogWarning(e, "Failed to load plugin by name: {Path}", pluginDir); } if (context != null) { @@ -246,39 +253,28 @@ public bool LoadPluginById( string id, bool silent = false ) } } - public void ReloadPluginById( string id, bool silent = false ) + public bool ReloadPluginById( string id, bool silent = false ) { _ = UnloadPluginById(id, silent); + return LoadPluginById(id, silent); + } - if (!LoadPluginById(id, silent)) - { - logger.LogWarning("Failed to reload plugin by id: {Id}", id); - } - else - { - logger.LogInformation("Reloaded plugin by id: {Id}", id); - } + public bool ReloadPluginByDllName( string dllName, bool silent = false ) + { + _ = UnloadPluginByDllName(dllName, silent); + return LoadPluginByDllName(dllName, silent); } private void LoadExports() { void PopulateSharedManually( string startDirectory ) { - var pluginDirs = Directory.GetDirectories(startDirectory); - - foreach (var pluginDir in pluginDirs) + EnumeratePluginDirectories(startDirectory, pluginDir => { - var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith('[') && dirName.EndsWith(']')) - { - PopulateSharedManually(pluginDir); - continue; - } - var exportsPath = Path.Combine(pluginDir, "resources", "exports"); if (!Directory.Exists(exportsPath)) { - continue; + return; } Directory.GetFiles(exportsPath, "*.dll") @@ -299,16 +295,16 @@ void PopulateSharedManually( string startDirectory ) logger.LogWarning(innerEx, "Failed to load export assembly: {Path}", exportFile); } }); - } + }); } try { var resolver = new DependencyResolver(logger); resolver.AnalyzeDependencies(rootDirService.GetPluginsRoot()); - logger.LogInformation("{Graph}\n", resolver.GetDependencyGraphVisualization()); + logger.LogInformation("{Graph}", resolver.GetDependencyGraphVisualization()); var loadOrder = resolver.GetLoadOrder(); - // logger.LogInformation("Loading {Count} export assemblies in dependency order", loadOrder.Count); + logger.LogInformation("Loading {Count} export assemblies in dependency order", loadOrder.Count); loadOrder.ForEach(exportFile => { @@ -331,7 +327,7 @@ void PopulateSharedManually( string startDirectory ) 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 manually"); PopulateSharedManually(rootDirService.GetPluginsRoot()); @@ -348,39 +344,41 @@ void PopulateSharedManually( string startDirectory ) private void LoadPlugins() { - void LoadPluginsFromFolder( string directory ) + EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), pluginDir => { - var pluginDirs = Directory.GetDirectories(directory); - - foreach (var pluginDir in pluginDirs) + Console.WriteLine(string.Empty); + var relativePath = pluginDir is { } dir ? Path.Join("(swRoot)", Path.GetRelativePath(rootDirService.GetRoot(), dir)) : string.Empty; + logger.LogInformation("Loading plugin: {RelativePath}.dll", Path.Join(relativePath, Path.GetFileName(relativePath))); + try { - var dirName = Path.GetFileName(pluginDir); - if (dirName.StartsWith('[') && dirName.EndsWith(']')) - { - LoadPluginsFromFolder(pluginDir); - continue; - } - - try + var context = LoadPlugin(pluginDir, false); + if (context?.Status == PluginStatus.Loaded) { - var context = LoadPlugin(pluginDir, false); - if (context?.Status == PluginStatus.Loaded) - { - logger.LogInformation("Loaded plugin: {Id}", 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, + relativePath + ); } - catch (Exception e) + } + catch (Exception e) + { + if (!GlobalExceptionHandler.Handle(e)) { - if (!GlobalExceptionHandler.Handle(e)) - { - continue; - } - logger.LogWarning(e, "Failed to load plugin: {Path}", pluginDir); + return; } + logger.LogWarning(e, "Failed to load plugin: {RelativePath}.dll", Path.Join(relativePath, Path.GetFileName(relativePath))); } - } + Console.WriteLine(string.Empty); + }); - LoadPluginsFromFolder(rootDirService.GetPluginsRoot()); RebuildSharedServices(); plugins @@ -389,6 +387,91 @@ void LoadPluginsFromFolder( string directory ) .ForEach(p => p.Plugin!.OnAllPluginsLoaded()); } + 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; + } + + 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)) + { + 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( + entrypointDll, + [typeof(BasePlugin), .. sharedTypes], + config => + { + config.IsUnloadable = config.LoadInMemory = true; + if (currentContext != null) + { + (config.DefaultContext, config.PreferSharedTypes) = (currentContext, true); + } + } + ); + + var pluginType = loader.LoadDefaultAssembly() + .GetTypes() + .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin))); + if (pluginType == null) + { + return FailWithError(context, $"Failed to find plugin type: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + + var metadata = pluginType.GetCustomAttribute(); + if (metadata == null) + { + return FailWithError(context, $"Failed to find plugin metadata: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + + context.Metadata = metadata; + dataDirectoryService.EnsurePluginDataDirectory(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) + { + _ = GlobalExceptionHandler.Handle(e); + + try + { + plugin.Unload(); + loader?.Dispose(); + core?.Dispose(); + } + catch { } + + return FailWithError(context, $"Failed to load plugin: {Path.Combine(dir, Path.GetFileName(dir))}.dll"); + } + } + private void RebuildSharedServices() { interfaceManager.Dispose(); @@ -403,4 +486,31 @@ private void RebuildSharedServices() loadedPlugins.ForEach(p => p.Plugin?.UseSharedInterface(interfaceManager)); loadedPlugins.ForEach(p => p.Plugin?.OnSharedInterfaceInjected(interfaceManager)); } + + private static void EnumeratePluginDirectories( string directory, Action action ) + { + var pluginDirs = Directory.GetDirectories(directory); + + foreach (var pluginDir in pluginDirs) + { + var dirName = Path.GetFileName(pluginDir); + if (dirName.Trim().StartsWith('[') && dirName.EndsWith(']')) + { + EnumeratePluginDirectories(pluginDir, action); + continue; + } + + if (dirName.Trim().Equals("disable", StringComparison.OrdinalIgnoreCase) || dirName.Trim().Equals("_", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (dirName.Trim().Length >= 2 && dirName.StartsWith('_')) + { + continue; + } + + action(pluginDir); + } + } } \ 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 6d8f43e68..eb64b6c8a 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -258,11 +258,11 @@ private void ProfilerCommand( ICommandContext context ) var args = context.Args; if (args.Length == 1) { - var table = new Table().AddColumn("Command").AddColumn("Description"); - table = table.AddRow("enable", "Enable the profiler"); - table = table.AddRow("disable", "Disable the profiler"); - table = table.AddRow("status", "Show the status of the profiler"); - table = table.AddRow("save", "Save the profiler data to a file"); + 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; } @@ -380,19 +380,46 @@ bool ValidatePluginId( string[] args, string command, string usage ) case "load": if (ValidatePluginId(args, "load", "")) { - _ = pluginManager.LoadPluginById(args[2]); + 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", "/")) { - _ = pluginManager.UnloadPluginById(args[2]); + 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", "/")) { - pluginManager.ReloadPluginById(args[2], true); + 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: 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) From 5def8b7a863fbcfc533781e06f550cf7d3052604 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 15:54:27 +0800 Subject: [PATCH 08/12] fix: Auto hot reload logic issues --- .../Modules/Plugins/PluginManager.cs | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 5bbc36252..95d467486 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -7,6 +7,7 @@ using SwiftlyS2.Core.Services; using SwiftlyS2.Shared.Plugins; using SwiftlyS2.Core.Modules.Plugins; +using System.Collections.Concurrent; namespace SwiftlyS2.Core.Plugins; @@ -20,6 +21,7 @@ internal class PluginManager private readonly InterfaceManager interfaceManager; private readonly List sharedTypes; private readonly List plugins; + private readonly ConcurrentDictionary fileLastChange; private readonly FileSystemWatcher? fileWatcher; @@ -38,6 +40,7 @@ DataDirectoryService dataDirectoryService this.interfaceManager = new(); this.sharedTypes = []; this.plugins = []; + this.fileLastChange = new(); this.fileWatcher = new FileSystemWatcher { Path = rootDirService.GetPluginsRoot(), @@ -56,9 +59,26 @@ DataDirectoryService dataDirectoryService } var directoryName = Path.GetFileName(Path.GetDirectoryName(e.FullPath)) ?? string.Empty; - if (string.IsNullOrWhiteSpace(directoryName)) + var fileName = Path.GetFileNameWithoutExtension(e.FullPath); + if (string.IsNullOrWhiteSpace(directoryName) || !fileName.Equals(directoryName)) { - _ = ReloadPluginByDllName(directoryName, true); + return; + } + + var now = DateTime.UtcNow; + if ((now - fileLastChange.GetValueOrDefault(directoryName, DateTime.MinValue)).TotalSeconds > 2) + { + _ = fileLastChange.AddOrUpdate(directoryName, now, ( _, _ ) => now); + 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 ex) @@ -341,17 +361,21 @@ void PopulateSharedManually( string startDirectory ) logger.LogError(ex, "Failed to load exports"); } } - private void LoadPlugins() { EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), pluginDir => { + 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); - var relativePath = pluginDir is { } dir ? Path.Join("(swRoot)", Path.GetRelativePath(rootDirService.GetRoot(), dir)) : string.Empty; - logger.LogInformation("Loading plugin: {RelativePath}.dll", Path.Join(relativePath, Path.GetFileName(relativePath))); + logger.LogInformation("Loading plugin: {Path}", fullDisplayPath); + try { - var context = LoadPlugin(pluginDir, false); + var context = LoadPlugin(pluginDir, true); if (context?.Status == PluginStatus.Loaded) { logger.LogInformation( @@ -364,9 +388,13 @@ private void LoadPlugins() context.Metadata!.Id, context.Metadata!.Version, context.Metadata!.Author, - relativePath + displayPath ); } + else + { + logger.LogWarning("Failed to load plugin: {Path}", fullDisplayPath); + } } catch (Exception e) { @@ -374,8 +402,9 @@ private void LoadPlugins() { return; } - logger.LogWarning(e, "Failed to load plugin: {RelativePath}.dll", Path.Join(relativePath, Path.GetFileName(relativePath))); + logger.LogWarning(e, "Failed to load plugin: {Path}", fullDisplayPath); } + Console.WriteLine(string.Empty); }); From f4b8827313e4ab43ba1e420b4066bbb31425056f Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 15:57:27 +0800 Subject: [PATCH 09/12] refactor: Improve command usage clarity --- managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs index eb64b6c8a..a6d9373cd 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -393,7 +393,7 @@ bool ValidatePluginId( string[] args, string command, string usage ) } break; case "unload": - if (ValidatePluginId(args, "unload", "/")) + if (ValidatePluginId(args, "unload", "")) { Console.WriteLine("\n"); if (pluginManager.UnloadPluginById(args[2], true) || pluginManager.UnloadPluginByDllName(args[2], true)) @@ -408,7 +408,7 @@ bool ValidatePluginId( string[] args, string command, string usage ) } break; case "reload": - if (ValidatePluginId(args, "reload", "/")) + if (ValidatePluginId(args, "reload", "")) { Console.WriteLine("\n"); if (pluginManager.ReloadPluginById(args[2], true) || pluginManager.ReloadPluginByDllName(args[2], true)) From d6160a4e926ca382ec476cd9cc81ac3a2ec18f90 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 21:25:56 +0800 Subject: [PATCH 10/12] fix: Typo --- managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 95d467486..673a917ed 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -186,7 +186,7 @@ public bool UnloadPluginByDllName( string dllName, bool silent = false ) { if (!silent) { - logger.LogWarning("Failed to find plugin by name: {DllName}", dllName); + logger.LogWarning("Failed to find plugin by name: {DllName}", dllName); } return false; } @@ -413,7 +413,7 @@ private void LoadPlugins() plugins .Where(p => p.Status == PluginStatus.Loaded) .ToList() - .ForEach(p => p.Plugin!.OnAllPluginsLoaded()); + .ForEach(p => p.Plugin?.OnAllPluginsLoaded()); } private PluginContext? LoadPlugin( string dir, bool hotReload, bool silent = false ) From dc820682f06214e037f8362faa81aa9130cbf7ef Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 22:49:56 +0800 Subject: [PATCH 11/12] fix: Wait for DLL file lock release before hot reload --- .../Modules/Plugins/PluginManager.cs | 97 +++++++++++++------ 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 673a917ed..8372b6b7e 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -22,6 +22,7 @@ internal class PluginManager private readonly List sharedTypes; private readonly List plugins; private readonly ConcurrentDictionary fileLastChange; + private readonly ConcurrentDictionary fileReloadTokens; private readonly FileSystemWatcher? fileWatcher; @@ -38,9 +39,10 @@ DataDirectoryService dataDirectoryService this.logger = logger; this.interfaceManager = new(); - this.sharedTypes = []; - this.plugins = []; - this.fileLastChange = new(); + this.sharedTypes = new List(); + this.plugins = new List(); + this.fileLastChange = new ConcurrentDictionary(); + this.fileReloadTokens = new ConcurrentDictionary(); this.fileWatcher = new FileSystemWatcher { Path = rootDirService.GetPluginsRoot(), @@ -51,6 +53,31 @@ DataDirectoryService dataDirectoryService }; 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; + } + } + } + try { if (!NativeServerHelpers.UseAutoHotReload() || e.ChangeType != WatcherChangeTypes.Changed) @@ -65,20 +92,40 @@ DataDirectoryService dataDirectoryService return; } - var now = DateTime.UtcNow; - if ((now - fileLastChange.GetValueOrDefault(directoryName, DateTime.MinValue)).TotalSeconds > 2) + if ((DateTime.UtcNow - fileLastChange.GetValueOrDefault(directoryName, DateTime.MinValue)).TotalSeconds > 2) { - _ = fileLastChange.AddOrUpdate(directoryName, now, ( _, _ ) => now); - Console.WriteLine("\n"); - if (ReloadPluginByDllName(directoryName, true)) + _ = fileLastChange.AddOrUpdate(directoryName, DateTime.UtcNow, ( _, _ ) => DateTime.UtcNow); + + if (fileReloadTokens.TryRemove(directoryName, out var oldCts)) { - logger.LogInformation("Reloaded plugin: {Format}", directoryName); + oldCts.Cancel(); + oldCts.Dispose(); } - else + + var cts = new CancellationTokenSource(); + _ = fileReloadTokens.AddOrUpdate(directoryName, cts, ( _, _ ) => cts); + + // Wait for file to be accessible, then reload + _ = Task.Run(async () => { - logger.LogWarning("Failed to reload plugin: {Format}", directoryName); - } - Console.WriteLine("\n"); + 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) @@ -224,32 +271,26 @@ public bool LoadPluginByDllName( string dllName, bool silent = false ) return false; } - var context = plugins + var oldContext = plugins .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded) .FirstOrDefault(p => p.PluginDirectory?.Trim().Equals(pluginDir.Trim()) ?? false); + PluginContext? newContext = null; try { - if (context != null && plugins.Remove(context)) + if (oldContext != null && plugins.Remove(oldContext)) { - var newContext = LoadPlugin(pluginDir, true, silent); + newContext = LoadPlugin(pluginDir, true, silent); if (newContext?.Status == PluginStatus.Loaded) { if (!silent) { - logger.LogInformation("Loaded plugin: {Id}", context.Metadata!.Id); + logger.LogInformation("Loaded plugin: {Id}", newContext.Metadata!.Id); } return true; } - else - { - throw new ArgumentException(string.Empty, string.Empty); - } - } - else - { - throw new ArgumentException(string.Empty, string.Empty); } + throw new ArgumentException(string.Empty, string.Empty); } catch (Exception e) { @@ -259,11 +300,11 @@ public bool LoadPluginByDllName( string dllName, bool silent = false ) } if (!silent) { - logger.LogWarning(e, "Failed to load plugin by name: {Path}", pluginDir); + logger.LogWarning("Failed to load plugin by name: {Path}", pluginDir); } - if (context != null) + if (newContext != null) { - context.Status = PluginStatus.Indeterminate; + newContext.Status = PluginStatus.Indeterminate; } return false; } From 66a9e60b2ddf487b7cd4a24ad33efc5106f747b6 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Tue, 18 Nov 2025 22:53:10 +0800 Subject: [PATCH 12/12] chore: Clean up code --- managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs index 8372b6b7e..da854605b 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.Loader; +using System.Collections.Concurrent; using McMaster.NETCore.Plugins; using Microsoft.Extensions.Logging; using SwiftlyS2.Shared; @@ -7,7 +8,6 @@ using SwiftlyS2.Core.Services; using SwiftlyS2.Shared.Plugins; using SwiftlyS2.Core.Modules.Plugins; -using System.Collections.Concurrent; namespace SwiftlyS2.Core.Plugins; @@ -39,8 +39,8 @@ DataDirectoryService dataDirectoryService this.logger = logger; this.interfaceManager = new(); - this.sharedTypes = new List(); - this.plugins = new List(); + this.sharedTypes = []; + this.plugins = []; this.fileLastChange = new ConcurrentDictionary(); this.fileReloadTokens = new ConcurrentDictionary();