diff --git a/SLCommandScript.FileScriptsLoader.UnitTests/FileScriptsLoaderTests.cs b/SLCommandScript.FileScriptsLoader.UnitTests/FileScriptsLoaderTests.cs index 96fa137..c12fa59 100644 --- a/SLCommandScript.FileScriptsLoader.UnitTests/FileScriptsLoaderTests.cs +++ b/SLCommandScript.FileScriptsLoader.UnitTests/FileScriptsLoaderTests.cs @@ -4,6 +4,7 @@ using SLCommandScript.FileScriptsLoader.Helpers; using SLCommandScript.FileScriptsLoader.Commands; using FluentAssertions; +using SLCommandScript.Core; using SLCommandScript.Core.Permissions; using Moq; using PluginAPI.Enums; @@ -63,6 +64,44 @@ public void InitScriptsLoader_ShouldNotInitialize_WhenProvidedPluginHandlerIsNul FileScriptCommandBase.ConcurrentExecutionsLimit.Should().Be(0); } + [TestCase(-9)] + [TestCase(9)] + [TestCase(1)] + public void InitScriptsLoader_ShouldNotInitialize_WhenAnInstanceIsAlreadyInitialized(int execsLimit) + { + // Arrange + var config = new ScriptsLoaderConfig() + { + CustomPermissionsResolver = null, + ScriptExecutionsLimit = execsLimit, + AllowedScriptCommandTypes = 0, + EnableScriptEventHandlers = false + }; + + HelpersProvider.FileSystemHelper = null; + HelpersProvider.FileSystemWatcherHelperFactory = null; + FileScriptCommandBase.PermissionsResolver = null; + FileScriptCommandBase.ConcurrentExecutionsLimit = 0; + using var loader = new FileScriptsLoader(); + var plugin = new TestPlugin(); + loader.InitScriptsLoader(plugin, new(_testDirectory, plugin, plugin.GetType(), _emptyTypesArray), config); + var fileSystemHelper = HelpersProvider.FileSystemHelper; + var fileSystemWatcherFactory = HelpersProvider.FileSystemWatcherHelperFactory; + var permissionsResolver = FileScriptCommandBase.PermissionsResolver; + var executionsLimit = FileScriptCommandBase.ConcurrentExecutionsLimit; + var secondLoader = new FileScriptsLoader(); + config.ScriptExecutionsLimit *= 2; + + // Act + secondLoader.InitScriptsLoader(plugin, new(_testDirectory, plugin, plugin.GetType(), _emptyTypesArray), config); + + // Assert + HelpersProvider.FileSystemHelper.Should().Be(fileSystemHelper); + HelpersProvider.FileSystemWatcherHelperFactory.Should().Be(fileSystemWatcherFactory); + FileScriptCommandBase.PermissionsResolver.Should().Be(permissionsResolver); + FileScriptCommandBase.ConcurrentExecutionsLimit.Should().Be(executionsLimit); + } + [TestCase(3)] [TestCase(7)] [TestCase(0)] @@ -73,7 +112,7 @@ public void InitScriptsLoader_ShouldInitialize_WhenNoDirectoriesAreEnabled(int e HelpersProvider.FileSystemWatcherHelperFactory = null; FileScriptCommandBase.PermissionsResolver = null; FileScriptCommandBase.ConcurrentExecutionsLimit = 0; - var loader = new FileScriptsLoader(); + using var loader = new FileScriptsLoader(); var plugin = new TestPlugin(); // Act @@ -103,7 +142,7 @@ public void InitScriptsLoader_ShouldLoadCustomPermissionsLoader(int execsLimit) HelpersProvider.FileSystemWatcherHelperFactory = null; FileScriptCommandBase.PermissionsResolver = null; FileScriptCommandBase.ConcurrentExecutionsLimit = 0; - var loader = new FileScriptsLoader(); + using var loader = new FileScriptsLoader(); var plugin = new TestPlugin(); // Act @@ -133,7 +172,7 @@ public void InitScriptsLoader_ShouldInitialize_WhenEventsAreEnabled() HelpersProvider.FileSystemWatcherHelperFactory = _testWatcherFactory; FileScriptCommandBase.PermissionsResolver = null; FileScriptCommandBase.ConcurrentExecutionsLimit = 0; - var loader = new FileScriptsLoader(); + using var loader = new FileScriptsLoader(); var plugin = new TestPlugin(); // Act @@ -168,7 +207,7 @@ public void InitScriptsLoader_ShouldInitialize_WhenCommandsAreEnabled(CommandTyp HelpersProvider.FileSystemWatcherHelperFactory = _testWatcherFactory; FileScriptCommandBase.PermissionsResolver = null; FileScriptCommandBase.ConcurrentExecutionsLimit = 0; - var loader = new FileScriptsLoader(); + using var loader = new FileScriptsLoader(); var plugin = new TestPlugin(); // Act @@ -201,7 +240,7 @@ public void InitScriptsLoader_ShouldInitialize_WhenLoaderConfigIsNull() HelpersProvider.FileSystemWatcherHelperFactory = _testWatcherFactory; FileScriptCommandBase.PermissionsResolver = null; FileScriptCommandBase.ConcurrentExecutionsLimit = 0; - var loader = new FileScriptsLoader(); + using var loader = new FileScriptsLoader(); var plugin = new TestPlugin(); // Act @@ -242,6 +281,31 @@ public void Dispose_ShouldProperlyCleanupResources() fileSystemMock.VerifyAll(); fileSystemMock.VerifyNoOtherCalls(); } + + [Test] + public void Dispose_ShouldNotCleanupHelpers_WhenAnInstanceIsStillActive() + { + // Arrange + var plugin = new TestPlugin(); + var fileSystemMock = new Mock(MockBehavior.Strict); + fileSystemMock.Setup(x => x.DirectoryExists(It.IsAny())).Returns(true); + HelpersProvider.FileSystemHelper = fileSystemMock.Object; + HelpersProvider.FileSystemWatcherHelperFactory = _testWatcherFactory; + using var loader = new FileScriptsLoader(); + loader.InitScriptsLoader(plugin, new(_testDirectory, plugin, plugin.GetType(), _emptyTypesArray), null); + var secondLoader = new FileScriptsLoader(); + + // Act + secondLoader.Dispose(); + + // Assert + FileScriptCommandBase.ConcurrentExecutionsLimit.Should().Be(10); + FileScriptCommandBase.PermissionsResolver.Should().NotBeNull(); + HelpersProvider.FileSystemHelper.Should().NotBeNull(); + HelpersProvider.FileSystemWatcherHelperFactory.Should().NotBeNull(); + fileSystemMock.VerifyAll(); + fileSystemMock.VerifyNoOtherCalls(); + } #endregion } diff --git a/SLCommandScript.FileScriptsLoader.UnitTests/Loader/CommandsDirectoryTests.cs b/SLCommandScript.FileScriptsLoader.UnitTests/Loader/CommandsDirectoryTests.cs index 78b7bd1..afd6bf7 100644 --- a/SLCommandScript.FileScriptsLoader.UnitTests/Loader/CommandsDirectoryTests.cs +++ b/SLCommandScript.FileScriptsLoader.UnitTests/Loader/CommandsDirectoryTests.cs @@ -53,6 +53,7 @@ private static Mock MakeWatcherMock() watcherMock.SetupAdd(x => x.Changed += It.IsAny()); watcherMock.SetupAdd(x => x.Deleted += It.IsAny()); watcherMock.SetupAdd(x => x.Renamed += It.IsAny()); + watcherMock.SetupAdd(x => x.Error += It.IsAny()); return watcherMock; } diff --git a/SLCommandScript.FileScriptsLoader.UnitTests/Loader/EventsDirectoryTests.cs b/SLCommandScript.FileScriptsLoader.UnitTests/Loader/EventsDirectoryTests.cs index 059e74c..b05dc7b 100644 --- a/SLCommandScript.FileScriptsLoader.UnitTests/Loader/EventsDirectoryTests.cs +++ b/SLCommandScript.FileScriptsLoader.UnitTests/Loader/EventsDirectoryTests.cs @@ -39,6 +39,7 @@ private static Mock MakeWatcherMock() watcherMock.SetupAdd(x => x.Created += It.IsAny()); watcherMock.SetupAdd(x => x.Deleted += It.IsAny()); watcherMock.SetupAdd(x => x.Renamed += It.IsAny()); + watcherMock.SetupAdd(x => x.Error += It.IsAny()); return watcherMock; } diff --git a/SLCommandScript.FileScriptsLoader/FileScriptsLoader.cs b/SLCommandScript.FileScriptsLoader/FileScriptsLoader.cs index 0369934..38c39b8 100644 --- a/SLCommandScript.FileScriptsLoader/FileScriptsLoader.cs +++ b/SLCommandScript.FileScriptsLoader/FileScriptsLoader.cs @@ -4,11 +4,11 @@ using System.Collections.Generic; using SLCommandScript.FileScriptsLoader.Loader; using SLCommandScript.Core; -using SLCommandScript.Core.Permissions; -using SLCommandScript.Core.Reflection; using SLCommandScript.FileScriptsLoader.Commands; using PluginAPI.Enums; using System; +using SLCommandScript.Core.Permissions; +using SLCommandScript.Core.Reflection; namespace SLCommandScript.FileScriptsLoader; @@ -22,6 +22,11 @@ public class FileScriptsLoader : IScriptsLoader /// private const string LoaderPrefix = "FileScriptsLoader: "; + /// + /// Contains a reference to initialized instance. + /// + private static FileScriptsLoader _instance = null; + /// /// Prints a message to server log. /// @@ -73,30 +78,16 @@ public void InitScriptsLoader(object plugin, PluginHandler handler, ScriptsLoade return; } - loaderConfig ??= new(); - IPermissionsResolver permissionsResolver; - - if (string.IsNullOrWhiteSpace(loaderConfig.CustomPermissionsResolver)) + if (_instance is not null) { - PrintLog("Using default permissions resolver."); - permissionsResolver = new VanillaPermissionsResolver(); - } - else - { - permissionsResolver = CustomTypesUtils.MakeCustomTypeInstance(loaderConfig.CustomPermissionsResolver, out var message); - - if (permissionsResolver is null) - { - PrintError(message); - permissionsResolver = new VanillaPermissionsResolver(); - } - else - { - PrintLog("Custom permissions resolver loaded successfully."); - } + PrintError("Only one instance of FileScriptsLoader can be initialized."); + return; } - FileScriptCommandBase.PermissionsResolver = permissionsResolver; + _instance = this; + PrintLog("Initializing scripts loader..."); + loaderConfig ??= new(); + FileScriptCommandBase.PermissionsResolver = LoadPermissionsResolver(loaderConfig.CustomPermissionsResolver); FileScriptCommandBase.ConcurrentExecutionsLimit = loaderConfig.ScriptExecutionsLimit; HelpersProvider.FileSystemHelper ??= new FileSystemHelper(); HelpersProvider.FileSystemWatcherHelperFactory ??= CreateWatcher; @@ -104,6 +95,7 @@ public void InitScriptsLoader(object plugin, PluginHandler handler, ScriptsLoade LoadDirectory(null, $"{handler.PluginDirectoryPath}/scripts/ra/", loaderConfig.AllowedScriptCommandTypes & CommandType.RemoteAdmin); LoadDirectory(null, $"{handler.PluginDirectoryPath}/scripts/server/", loaderConfig.AllowedScriptCommandTypes & CommandType.Console); LoadDirectory(null, $"{handler.PluginDirectoryPath}/scripts/client/", loaderConfig.AllowedScriptCommandTypes & CommandType.GameConsole); + PrintLog("Scripts loader is initialized."); } /// @@ -125,6 +117,8 @@ public void Dispose() /// protected void PerformCleanup() { + PrintLog("Disabling scripts loader..."); + foreach (var dir in _registeredDirectories) { dir.Dispose(); @@ -133,10 +127,46 @@ protected void PerformCleanup() _registeredDirectories.Clear(); _eventsDirectory?.Dispose(); _eventsDirectory = null; + + if (!ReferenceEquals(_instance, this)) + { + PrintLog("Scripts loader is disabled but static helpers are still active."); + return; + } + FileScriptCommandBase.ConcurrentExecutionsLimit = 0; FileScriptCommandBase.PermissionsResolver = null; HelpersProvider.FileSystemHelper = null; HelpersProvider.FileSystemWatcherHelperFactory = null; + _instance = null; + PrintLog("Scripts loader is disabled."); + } + + /// + /// Loads permissions resolver instance to use. + /// + /// Custom type of resolver to use. + /// Loaded permissions resolver instance. + private IPermissionsResolver LoadPermissionsResolver(string resolverToUse) + { + if (string.IsNullOrWhiteSpace(resolverToUse)) + { + PrintLog("Using default permissions resolver."); + return new VanillaPermissionsResolver(); + } + + var permissionsResolver = CustomTypesUtils.MakeCustomTypeInstance(resolverToUse, out var message); + + if (permissionsResolver is null) + { + PrintError(message); + return new VanillaPermissionsResolver(); + } + else + { + PrintLog("Custom permissions resolver loaded successfully."); + return permissionsResolver; + } } /// @@ -159,10 +189,12 @@ private void LoadDirectory(object plugin, string directory, CommandType handlerT if (plugin is null) { + PrintLog($"Initializing commands directory for {handlerType}..."); _registeredDirectories.Add(new CommandsDirectory(HelpersProvider.FileSystemWatcherHelperFactory(directory, null, true), handlerType)); } else { + PrintLog("Initializing events directory..."); _eventsDirectory = new EventsDirectory(plugin, HelpersProvider.FileSystemWatcherHelperFactory(directory, EventsDirectory.ScriptFilesFilter, false)); } } diff --git a/SLCommandScript.FileScriptsLoader/Helpers/FileSystemWatcherHelper.cs b/SLCommandScript.FileScriptsLoader/Helpers/FileSystemWatcherHelper.cs index 0605369..480626d 100644 --- a/SLCommandScript.FileScriptsLoader/Helpers/FileSystemWatcherHelper.cs +++ b/SLCommandScript.FileScriptsLoader/Helpers/FileSystemWatcherHelper.cs @@ -1,8 +1,6 @@ using System; using System.IO; -using PluginAPI.Core; - namespace SLCommandScript.FileScriptsLoader.Helpers; /// @@ -34,6 +32,11 @@ public interface IFileSystemWatcherHelper : IDisposable /// Event invoked on file deletion. /// event FileSystemEventHandler Deleted; + + /// + /// Event invoked on error. + /// + event ErrorEventHandler Error; } /// @@ -71,6 +74,11 @@ public class FileSystemWatcherHelper : IFileSystemWatcherHelper /// public event FileSystemEventHandler Deleted { add => _watcher.Deleted += value; remove => _watcher.Deleted -= value; } + /// + /// Event invoked on error. + /// + public event ErrorEventHandler Error { add => _watcher.Error += value; remove => _watcher.Error -= value; } + /// /// Initializes new file system watcher. /// @@ -86,11 +94,6 @@ public FileSystemWatcherHelper(string path, string filter, bool includeSubdirect IncludeSubdirectories = includeSubdirectories, EnableRaisingEvents = true }; - - _watcher.Created += (obj, args) => Log.Debug($"Created: {args.Name}", "FilesWatcher: "); - _watcher.Changed += (obj, args) => Log.Debug($"Changed: {args.Name}", "FilesWatcher: "); - _watcher.Renamed += (obj, args) => Log.Debug($"Renamed: {args.OldName} -> {args.Name}", "FilesWatcher: "); - _watcher.Deleted += (obj, args) => Log.Debug($"Deleted: {args.Name}", "FilesWatcher: "); } /// diff --git a/SLCommandScript.FileScriptsLoader/Loader/CommandsDirectory.cs b/SLCommandScript.FileScriptsLoader/Loader/CommandsDirectory.cs index ae00028..a82bc94 100644 --- a/SLCommandScript.FileScriptsLoader/Loader/CommandsDirectory.cs +++ b/SLCommandScript.FileScriptsLoader/Loader/CommandsDirectory.cs @@ -84,6 +84,7 @@ public CommandsDirectory(IFileSystemWatcherHelper watcher, CommandType handlerTy Watcher.Changed += (obj, args) => RefreshDescription(args.FullPath); Watcher.Deleted += (obj, args) => UnregisterFile(args.FullPath); Watcher.Renamed += (obj, args) => RefreshFile(args.OldFullPath, args.FullPath); + Watcher.Error += (obj, args) => FileScriptsLoader.PrintError($"A {HandlerType} commands watcher error has occured: {args.GetException().Message}"); } /// @@ -230,6 +231,13 @@ private void RefreshFile(string oldPath, string newPath) RegisterFile(newPath); } + /// + /// Checks if command parent directory exists and can be accessed. + /// + /// Directory to check. + /// if parent exists, otherwise or if parent is root directory. + private bool? CheckParent(string dir) => dir.Length > 0 ? (Directories.ContainsKey(dir) ? true : null) : false; + /// /// Registers a script command. /// @@ -239,37 +247,48 @@ private void RefreshFile(string oldPath, string newPath) private bool RegisterCommand(string path, ICommand cmd) { var dir = ProcessDirectoryPath(HelpersProvider.FileSystemHelper.GetDirectory(path)); - var hasParent = Directories.ContainsKey(dir); - var registered = hasParent ? (CommandsUtils.RegisterCommand(Directories[dir], cmd) == true ? HandlerType : null) : CommandsUtils.RegisterCommand(HandlerType, cmd); + var hasParent = CheckParent(dir); + var displayName = dir.Length > 0 ? $"{dir}/{cmd.Command}" : cmd.Command; + + var registered = hasParent switch + { + true => CommandsUtils.RegisterCommand(Directories[dir], cmd) == true ? HandlerType : null, + false => CommandsUtils.RegisterCommand(HandlerType, cmd), + _ => null + }; if (registered != HandlerType) { - FileScriptsLoader.PrintError($"Could not register command '{cmd.Command}' for {HandlerType}."); + FileScriptsLoader.PrintError($"Could not register command '{displayName}' for {HandlerType}."); return false; } - if (!hasParent) + if (hasParent == false) { Commands[cmd.Command] = cmd; } - FileScriptsLoader.PrintLog($"Registered command '{cmd.Command}' for {HandlerType}."); + FileScriptsLoader.PrintLog($"Registered command '{displayName}' for {HandlerType}."); return true; } /// - /// Updates script command description info. + /// Attempts to retrieve a registered command. /// - /// Script description file to update. - /// if updated without issues, otherwise. - private bool UpdateScriptDescription(string path) + /// Whether or not the command has valid parent. + /// Parent directory of the command. + /// Name of the command to find. + /// Found command or if nothing was found. + private ICommand GetCommand(bool? hasParent, string dir, string name) { - var dir = ProcessDirectoryPath(HelpersProvider.FileSystemHelper.GetDirectory(path)); - var hasParent = Directories.ContainsKey(dir); - var name = HelpersProvider.FileSystemHelper.GetFileNameWithoutExtension(path); + if (hasParent == null) + { + return null; + } + ICommand foundCommand; - if (hasParent) + if (hasParent.Value) { Directories[dir].TryGetCommand(name, out foundCommand); } @@ -278,11 +297,25 @@ private bool UpdateScriptDescription(string path) Commands.TryGetValue(name, out foundCommand); } - var cmd = foundCommand as FileScriptCommand; + return foundCommand; + } + + /// + /// Updates script command description info. + /// + /// Script description file to update. + /// if updated without issues, otherwise. + private bool UpdateScriptDescription(string path) + { + var dir = ProcessDirectoryPath(HelpersProvider.FileSystemHelper.GetDirectory(path)); + var hasParent = CheckParent(dir); + var name = HelpersProvider.FileSystemHelper.GetFileNameWithoutExtension(path); + var displayName = dir.Length > 0 ? $"{dir}/{name}" : name; + var cmd = GetCommand(hasParent, dir, name) as FileScriptCommand; if (cmd is null) { - FileScriptsLoader.PrintError($"Could not update description for command '{name}' in {HandlerType}."); + FileScriptsLoader.PrintError($"Could not update description for command '{displayName}' in {HandlerType}."); return false; } @@ -294,12 +327,12 @@ private bool UpdateScriptDescription(string path) } catch (Exception ex) { - FileScriptsLoader.PrintError($"An error has occured during '{cmd.Command}' in {HandlerType} description deserialization: {ex.Message}"); + FileScriptsLoader.PrintError($"An error has occured during '{displayName}' in {HandlerType} description deserialization: {ex.Message}"); return false; } UpdateCommandDesc(cmd, desc); - FileScriptsLoader.PrintLog($"Description update for '{cmd.Command}' command in {HandlerType} finished successfully."); + FileScriptsLoader.PrintLog($"Description update for '{displayName}' command in {HandlerType} finished successfully."); return true; } @@ -311,33 +344,30 @@ private bool UpdateScriptDescription(string path) private ICommand UnregisterCommand(string path) { var dir = ProcessDirectoryPath(HelpersProvider.FileSystemHelper.GetDirectory(path)); - var hasParent = Directories.ContainsKey(dir); + var hasParent = CheckParent(dir); var name = HelpersProvider.FileSystemHelper.GetFileNameWithoutExtension(path); - ICommand cmd; + var displayName = dir.Length > 0 ? $"{dir}/{name}" : name; + var cmd = GetCommand(hasParent, dir, name); - if (hasParent) + var removed = hasParent switch { - Directories[dir].TryGetCommand(name, out cmd); - } - else - { - Commands.TryGetValue(name, out cmd); - } - - var removed = hasParent ? (CommandsUtils.UnregisterCommand(Directories[dir], cmd) == true ? HandlerType : null) : CommandsUtils.UnregisterCommand(HandlerType, cmd); + true => CommandsUtils.UnregisterCommand(Directories[dir], cmd) == true ? HandlerType : null, + false => CommandsUtils.UnregisterCommand(HandlerType, cmd), + _ => null + }; if (removed != HandlerType) { - FileScriptsLoader.PrintError($"Could not unregister command '{cmd?.Command}' from {HandlerType}."); + FileScriptsLoader.PrintError($"Could not unregister command '{displayName}' from {HandlerType}."); return null; } - if (!hasParent) + if (hasParent == false) { Commands.Remove(cmd.Command); } - FileScriptsLoader.PrintLog($"Unregistered command '{cmd.Command}' from {HandlerType}."); + FileScriptsLoader.PrintLog($"Unregistered command '{displayName}' from {HandlerType}."); return cmd; } } diff --git a/SLCommandScript.FileScriptsLoader/Loader/EventsDirectory.cs b/SLCommandScript.FileScriptsLoader/Loader/EventsDirectory.cs index 68a9da2..ef4e4ab 100644 --- a/SLCommandScript.FileScriptsLoader/Loader/EventsDirectory.cs +++ b/SLCommandScript.FileScriptsLoader/Loader/EventsDirectory.cs @@ -62,6 +62,7 @@ public EventsDirectory(object plugin, IFileSystemWatcherHelper watcher) Watcher.Created += (obj, args) => RegisterEvent(args.FullPath); Watcher.Deleted += (obj, args) => UnregisterEvent(args.FullPath); Watcher.Renamed += (obj, args) => RefreshEvent(args.OldFullPath, args.FullPath); + Watcher.Error += (obj, args) => FileScriptsLoader.PrintError($"An events watcher error has occured: {args.GetException().Message}"); if (PluginObject is null) {