diff --git a/src/Neo/Ledger/Blockchain.cs b/src/Neo/Ledger/Blockchain.cs index ca27bb2124..e9e3cb0020 100644 --- a/src/Neo/Ledger/Blockchain.cs +++ b/src/Neo/Ledger/Blockchain.cs @@ -12,18 +12,22 @@ using Akka.Actor; using Akka.Configuration; using Akka.IO; +using Akka.Util.Internal; using Neo.IO.Actors; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.Persistence; +using Neo.Plugins; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; namespace Neo.Ledger { @@ -468,10 +472,10 @@ private void Persist(Block block) Context.System.EventStream.Publish(application_executed); all_application_executed.Add(application_executed); } - Committing?.Invoke(system, block, snapshot, all_application_executed); + _ = InvokeCommittingAsync(system, block, snapshot, all_application_executed); snapshot.Commit(); } - Committed?.Invoke(system, block); + _ = InvokeCommittedAsync(system, block); system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView); extensibleWitnessWhiteList = null; block_cache.Remove(block.PrevHash); @@ -480,6 +484,64 @@ private void Persist(Block block) Debug.Assert(header.Index == block.Index); } + internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + await InvokeHandlersAsync(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); + } + + internal static async Task InvokeCommittedAsync(NeoSystem system, Block block) + { + await InvokeHandlersAsync(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); + } + + private static async Task InvokeHandlersAsync(Delegate[] handlers, Action handlerAction) + { + if (handlers == null) return; + + var exceptions = new ConcurrentBag(); + var tasks = handlers.Select(handler => Task.Run(() => + { + try + { + // skip stopped plugin. + if (handler.Target is Plugin { IsStopped: true }) + { + return; + } + + handlerAction(handler); + } + catch (Exception ex) when (handler.Target is Plugin plugin) + { + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + exceptions.Add(ex); + throw; + case UnhandledExceptionPolicy.StopPlugin: + //Stop plugin on exception + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + // Log the exception and continue with the next handler + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + + Utility.Log(nameof(plugin), LogLevel.Error, ex); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + })).ToList(); + + await Task.WhenAll(tasks); + + exceptions.ForEach(e => throw e); + } + /// /// Gets a object used for creating the actor. /// diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs index 248301af56..9feee25d57 100644 --- a/src/Neo/Plugins/Plugin.cs +++ b/src/Neo/Plugins/Plugin.cs @@ -33,7 +33,8 @@ public abstract class Plugin : IDisposable /// /// The directory containing the plugin folders. Files can be contained in any subdirectory. /// - public static readonly string PluginsDirectory = Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins"); + public static readonly string PluginsDirectory = + Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins"); private static readonly FileSystemWatcher configWatcher; @@ -67,6 +68,18 @@ public abstract class Plugin : IDisposable /// public virtual Version Version => GetType().Assembly.GetName().Version; + /// + /// If the plugin should be stopped when an exception is thrown. + /// Default is . + /// + protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode; + + /// + /// The plugin will be stopped if an exception is thrown. + /// But it also depends on . + /// + internal bool IsStopped { get; set; } + static Plugin() { if (!Directory.Exists(PluginsDirectory)) return; @@ -74,7 +87,8 @@ static Plugin() { EnableRaisingEvents = true, IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size, + NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | + NotifyFilters.LastWrite | NotifyFilters.Size, }; configWatcher.Changed += ConfigWatcher_Changed; configWatcher.Created += ConfigWatcher_Changed; @@ -106,7 +120,8 @@ private static void ConfigWatcher_Changed(object sender, FileSystemEventArgs e) { case ".json": case ".dll": - Utility.Log(nameof(Plugin), LogLevel.Warning, $"File {e.Name} is {e.ChangeType}, please restart node."); + Utility.Log(nameof(Plugin), LogLevel.Warning, + $"File {e.Name} is {e.ChangeType}, please restart node."); break; } } @@ -119,7 +134,8 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven AssemblyName an = new(args.Name); Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ?? - AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name); + AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == an.Name); if (assembly != null) return assembly; string filename = an.Name + ".dll"; @@ -150,7 +166,8 @@ public virtual void Dispose() /// The content of the configuration file read. protected IConfigurationSection GetConfiguration() { - return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build().GetSection("PluginConfiguration"); + return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build() + .GetSection("PluginConfiguration"); } private static void LoadPlugin(Assembly assembly) @@ -187,6 +204,7 @@ internal static void LoadPlugins() catch { } } } + foreach (Assembly assembly in assemblies) { LoadPlugin(assembly); @@ -229,7 +247,33 @@ protected internal virtual void OnSystemLoaded(NeoSystem system) /// if the is handled by a plugin; otherwise, . public static bool SendMessage(object message) { - return Plugins.Any(plugin => plugin.OnMessage(message)); + + return Plugins.Any(plugin => + { + try + { + return !plugin.IsStopped && + plugin.OnMessage(message); + } + catch (Exception ex) + { + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + throw; + case UnhandledExceptionPolicy.StopPlugin: + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + Utility.Log(nameof(Plugin), LogLevel.Error, ex); + return false; + } + } + ); } } } diff --git a/src/Neo/Plugins/PluginSettings.cs b/src/Neo/Plugins/PluginSettings.cs new file mode 100644 index 0000000000..af33e44eea --- /dev/null +++ b/src/Neo/Plugins/PluginSettings.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PluginSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Extensions.Configuration; +using Org.BouncyCastle.Security; +using System; + +namespace Neo.Plugins; + +public abstract class PluginSettings(IConfigurationSection section) +{ + public UnhandledExceptionPolicy ExceptionPolicy + { + get + { + var policyString = section.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); + if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + { + return policy; + } + + throw new InvalidParameterException($"{policyString} is not a valid UnhandledExceptionPolicy"); + } + } +} diff --git a/src/Neo/Plugins/UnhandledExceptionPolicy.cs b/src/Neo/Plugins/UnhandledExceptionPolicy.cs new file mode 100644 index 0000000000..035e173aa3 --- /dev/null +++ b/src/Neo/Plugins/UnhandledExceptionPolicy.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UnhandledExceptionPolicy.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Plugins +{ + public enum UnhandledExceptionPolicy + { + Ignore = 0, + StopPlugin = 1, + StopNode = 2, + } +} diff --git a/src/Plugins/ApplicationLogs/ApplicationLogs.json b/src/Plugins/ApplicationLogs/ApplicationLogs.json index af601bc81e..2664665dd2 100644 --- a/src/Plugins/ApplicationLogs/ApplicationLogs.json +++ b/src/Plugins/ApplicationLogs/ApplicationLogs.json @@ -3,7 +3,8 @@ "Path": "ApplicationLogs_{0}", "Network": 860833102, "MaxStackSize": 65535, - "Debug": false + "Debug": false, + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/src/Plugins/ApplicationLogs/LogReader.cs b/src/Plugins/ApplicationLogs/LogReader.cs index e0e886d93b..b3f767fecb 100644 --- a/src/Plugins/ApplicationLogs/LogReader.cs +++ b/src/Plugins/ApplicationLogs/LogReader.cs @@ -38,6 +38,7 @@ public class LogReader : Plugin, ICommittingHandler, ICommittedHandler, ILogHand public override string Name => "ApplicationLogs"; public override string Description => "Synchronizes smart contract VM executions and notifications (NotifyLog) on blockchain."; + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; #region Ctor diff --git a/src/Plugins/ApplicationLogs/Settings.cs b/src/Plugins/ApplicationLogs/Settings.cs index 8f2a0da1e1..6a5f238272 100644 --- a/src/Plugins/ApplicationLogs/Settings.cs +++ b/src/Plugins/ApplicationLogs/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.ApplicationLogs { - internal class Settings + internal class Settings : PluginSettings { public string Path { get; } public uint Network { get; } @@ -23,7 +23,7 @@ internal class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Path = section.GetValue("Path", "ApplicationLogs_{0}"); Network = section.GetValue("Network", 5195086u); diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.cs b/src/Plugins/DBFTPlugin/DBFTPlugin.cs index f09be9d291..65fc5011dc 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.cs +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.cs @@ -31,6 +31,8 @@ public class DBFTPlugin : Plugin, IServiceAddedHandler, IMessageReceivedHandler, public override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy; + public DBFTPlugin() { RemoteNode.MessageReceived += ((IMessageReceivedHandler)this).RemoteNode_MessageReceived_Handler; diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.json b/src/Plugins/DBFTPlugin/DBFTPlugin.json index 2e2b710ba3..705b2b77cb 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.json +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.json @@ -5,6 +5,7 @@ "AutoStart": false, "Network": 860833102, "MaxBlockSize": 2097152, - "MaxBlockSystemFee": 150000000000 + "MaxBlockSystemFee": 150000000000, + "UnhandledExceptionPolicy": "StopNode" } } diff --git a/src/Plugins/DBFTPlugin/Settings.cs b/src/Plugins/DBFTPlugin/Settings.cs index 28ad21f37a..1f37feaf16 100644 --- a/src/Plugins/DBFTPlugin/Settings.cs +++ b/src/Plugins/DBFTPlugin/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.DBFTPlugin { - public class Settings + public class Settings : PluginSettings { public string RecoveryLogs { get; } public bool IgnoreRecoveryLogs { get; } @@ -22,7 +22,7 @@ public class Settings public uint MaxBlockSize { get; } public long MaxBlockSystemFee { get; } - public Settings(IConfigurationSection section) + public Settings(IConfigurationSection section) : base(section) { RecoveryLogs = section.GetValue("RecoveryLogs", "ConsensusState"); IgnoreRecoveryLogs = section.GetValue("IgnoreRecoveryLogs", false); diff --git a/src/Plugins/OracleService/OracleService.cs b/src/Plugins/OracleService/OracleService.cs index d7a54c5c9d..bb5da83654 100644 --- a/src/Plugins/OracleService/OracleService.cs +++ b/src/Plugins/OracleService/OracleService.cs @@ -62,6 +62,8 @@ public class OracleService : Plugin, ICommittingHandler, IServiceAddedHandler, I public override string Description => "Built-in oracle plugin"; + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; + public override string ConfigFile => System.IO.Path.Combine(RootPath, "OracleService.json"); public OracleService() diff --git a/src/Plugins/OracleService/OracleService.json b/src/Plugins/OracleService/OracleService.json index 1ab0d93399..49bf1153b3 100644 --- a/src/Plugins/OracleService/OracleService.json +++ b/src/Plugins/OracleService/OracleService.json @@ -6,6 +6,7 @@ "MaxOracleTimeout": 10000, "AllowPrivateHost": false, "AllowedContentTypes": [ "application/json" ], + "UnhandledExceptionPolicy": "Ignore", "Https": { "Timeout": 5000 }, diff --git a/src/Plugins/OracleService/Settings.cs b/src/Plugins/OracleService/Settings.cs index 952ea0c27b..db93c1c400 100644 --- a/src/Plugins/OracleService/Settings.cs +++ b/src/Plugins/OracleService/Settings.cs @@ -37,7 +37,7 @@ public NeoFSSettings(IConfigurationSection section) } } - class Settings + class Settings : PluginSettings { public uint Network { get; } public Uri[] Nodes { get; } @@ -51,7 +51,7 @@ class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Network = section.GetValue("Network", 5195086u); Nodes = section.GetSection("Nodes").GetChildren().Select(p => new Uri(p.Get(), UriKind.Absolute)).ToArray(); diff --git a/src/Plugins/RpcServer/RpcServer.json b/src/Plugins/RpcServer/RpcServer.json index 8f6905dead..dc9c25b8da 100644 --- a/src/Plugins/RpcServer/RpcServer.json +++ b/src/Plugins/RpcServer/RpcServer.json @@ -1,5 +1,6 @@ { "PluginConfiguration": { + "UnhandledExceptionPolicy": "Ignore", "Servers": [ { "Network": 860833102, diff --git a/src/Plugins/RpcServer/RpcServerPlugin.cs b/src/Plugins/RpcServer/RpcServerPlugin.cs index c22462d139..03416c1be5 100644 --- a/src/Plugins/RpcServer/RpcServerPlugin.cs +++ b/src/Plugins/RpcServer/RpcServerPlugin.cs @@ -24,6 +24,7 @@ public class RpcServerPlugin : Plugin private static readonly Dictionary> handlers = new(); public override string ConfigFile => System.IO.Path.Combine(RootPath, "RpcServer.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy; protected override void Configure() { diff --git a/src/Plugins/RpcServer/Settings.cs b/src/Plugins/RpcServer/Settings.cs index ad624d9082..2cf7b72fb8 100644 --- a/src/Plugins/RpcServer/Settings.cs +++ b/src/Plugins/RpcServer/Settings.cs @@ -18,11 +18,11 @@ namespace Neo.Plugins.RpcServer { - class Settings + class Settings : PluginSettings { public IReadOnlyList Servers { get; init; } - public Settings(IConfigurationSection section) + public Settings(IConfigurationSection section) : base(section) { Servers = section.GetSection(nameof(Servers)).GetChildren().Select(p => RpcServerSettings.Load(p)).ToArray(); } diff --git a/src/Plugins/StateService/Settings.cs b/src/Plugins/StateService/Settings.cs index 8557866bc1..a425b57d7e 100644 --- a/src/Plugins/StateService/Settings.cs +++ b/src/Plugins/StateService/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.StateService { - internal class Settings + internal class Settings : PluginSettings { public string Path { get; } public bool FullState { get; } @@ -23,7 +23,7 @@ internal class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Path = section.GetValue("Path", "Data_MPT_{0}"); FullState = section.GetValue("FullState", false); diff --git a/src/Plugins/StateService/StatePlugin.cs b/src/Plugins/StateService/StatePlugin.cs index 5f6e1ef1d8..03dcc55aac 100644 --- a/src/Plugins/StateService/StatePlugin.cs +++ b/src/Plugins/StateService/StatePlugin.cs @@ -41,6 +41,8 @@ public class StatePlugin : Plugin, ICommittingHandler, ICommittedHandler, IWalle public override string Description => "Enables MPT for the node"; public override string ConfigFile => System.IO.Path.Combine(RootPath, "StateService.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; + internal IActorRef Store; internal IActorRef Verifier; diff --git a/src/Plugins/StateService/StateService.json b/src/Plugins/StateService/StateService.json index 265436fc30..cadd2da5fd 100644 --- a/src/Plugins/StateService/StateService.json +++ b/src/Plugins/StateService/StateService.json @@ -4,7 +4,8 @@ "FullState": false, "Network": 860833102, "AutoVerify": false, - "MaxFindResultItems": 100 + "MaxFindResultItems": 100, + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/src/Plugins/StorageDumper/Settings.cs b/src/Plugins/StorageDumper/Settings.cs index c2761ce6b9..e645cd7074 100644 --- a/src/Plugins/StorageDumper/Settings.cs +++ b/src/Plugins/StorageDumper/Settings.cs @@ -14,7 +14,7 @@ namespace Neo.Plugins.StorageDumper { - internal class Settings + internal class Settings : PluginSettings { /// /// Amount of storages states (heights) to be dump in a given json file @@ -32,7 +32,7 @@ internal class Settings public static Settings? Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { // Geting settings for storage changes state dumper BlockCacheSize = section.GetValue("BlockCacheSize", 1000u); diff --git a/src/Plugins/StorageDumper/StorageDumper.cs b/src/Plugins/StorageDumper/StorageDumper.cs index c47df9ac1d..6f5498b3a2 100644 --- a/src/Plugins/StorageDumper/StorageDumper.cs +++ b/src/Plugins/StorageDumper/StorageDumper.cs @@ -30,7 +30,7 @@ public class StorageDumper : Plugin, ICommittingHandler, ICommittedHandler /// private JObject? _currentBlock; private string? _lastCreateDirectory; - + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default?.ExceptionPolicy ?? UnhandledExceptionPolicy.Ignore; public override string Description => "Exports Neo-CLI status data"; diff --git a/src/Plugins/StorageDumper/StorageDumper.json b/src/Plugins/StorageDumper/StorageDumper.json index b327c37e0c..0c314cf262 100644 --- a/src/Plugins/StorageDumper/StorageDumper.json +++ b/src/Plugins/StorageDumper/StorageDumper.json @@ -3,6 +3,7 @@ "BlockCacheSize": 1000, "HeightToBegin": 0, "StoragePerFolder": 100000, - "Exclude": [ -4 ] + "Exclude": [ -4 ], + "UnhandledExceptionPolicy": "Ignore" } } diff --git a/src/Plugins/TokensTracker/TokensTracker.cs b/src/Plugins/TokensTracker/TokensTracker.cs index e5ffdce3f4..a7ab075245 100644 --- a/src/Plugins/TokensTracker/TokensTracker.cs +++ b/src/Plugins/TokensTracker/TokensTracker.cs @@ -16,6 +16,7 @@ using Neo.Persistence; using Neo.Plugins.RpcServer; using Neo.Plugins.Trackers; +using System; using System.Collections.Generic; using System.Linq; using static System.IO.Path; @@ -30,8 +31,10 @@ public class TokensTracker : Plugin, ICommittingHandler, ICommittedHandler private uint _network; private string[] _enabledTrackers; private IStore _db; + private UnhandledExceptionPolicy _exceptionPolicy; private NeoSystem neoSystem; private readonly List trackers = new(); + protected override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; public override string Description => "Enquiries balances and transaction history of accounts through RPC"; @@ -57,6 +60,11 @@ protected override void Configure() _maxResults = config.GetValue("MaxResults", 1000u); _network = config.GetValue("Network", 860833102u); _enabledTrackers = config.GetSection("EnabledTrackers").GetChildren().Select(p => p.Value).ToArray(); + var policyString = config.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); + if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + { + _exceptionPolicy = policy; + } } protected override void OnSystemLoaded(NeoSystem system) diff --git a/src/Plugins/TokensTracker/TokensTracker.json b/src/Plugins/TokensTracker/TokensTracker.json index ca63183b68..dbdbecfd40 100644 --- a/src/Plugins/TokensTracker/TokensTracker.json +++ b/src/Plugins/TokensTracker/TokensTracker.json @@ -4,7 +4,8 @@ "TrackHistory": true, "MaxResults": 1000, "Network": 860833102, - "EnabledTrackers": [ "NEP-11", "NEP-17" ] + "EnabledTrackers": [ "NEP-11", "NEP-17" ], + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/tests/Neo.UnitTests/Plugins/TestPlugin.cs b/tests/Neo.UnitTests/Plugins/TestPlugin.cs index dde500b927..5e51220c7f 100644 --- a/tests/Neo.UnitTests/Plugins/TestPlugin.cs +++ b/tests/Neo.UnitTests/Plugins/TestPlugin.cs @@ -10,15 +10,60 @@ // modifications are permitted. using Microsoft.Extensions.Configuration; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; using Neo.Plugins; +using System; +using System.Collections.Generic; namespace Neo.UnitTests.Plugins { - public class TestPlugin : Plugin + + internal class TestPluginSettings(IConfigurationSection section) : PluginSettings(section) + { + public static TestPluginSettings Default { get; private set; } + public static void Load(IConfigurationSection section) + { + Default = new TestPluginSettings(section); + } + } + internal class TestNonPlugin + { + public TestNonPlugin() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException("Test exception from OnCommitting"); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException("Test exception from OnCommitted"); + } + } + + + internal class TestPlugin : Plugin { - public TestPlugin() : base() { } + private readonly UnhandledExceptionPolicy _exceptionPolicy; + protected internal override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; - protected override void Configure() { } + public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) : base() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + _exceptionPolicy = exceptionPolicy; + } + + protected override void Configure() + { + TestPluginSettings.Load(GetConfiguration()); + } public void LogMessage(string message) { @@ -36,5 +81,15 @@ public IConfigurationSection TestGetConfiguration() } protected override bool OnMessage(object message) => true; + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException(); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException(); + } } } diff --git a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs index e5e8cb6e49..c48d32563f 100644 --- a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs +++ b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs @@ -11,8 +11,11 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Ledger; using Neo.Plugins; using System; +using System.Reflection; +using System.Threading.Tasks; namespace Neo.UnitTests.Plugins { @@ -21,6 +24,50 @@ public class UT_Plugin { private static readonly object locker = new(); + [TestInitialize] + public void TestInitialize() + { + ClearEventHandlers(); + } + + [TestCleanup] + public void TestCleanup() + { + ClearEventHandlers(); + } + + private static void ClearEventHandlers() + { + ClearEventHandler("Committing"); + ClearEventHandler("Committed"); + } + + private static void ClearEventHandler(string eventName) + { + var eventInfo = typeof(Blockchain).GetEvent(eventName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (eventInfo == null) + { + return; + } + + var fields = typeof(Blockchain).GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); + foreach (var field in fields) + { + if (field.FieldType == typeof(MulticastDelegate) || field.FieldType.BaseType == typeof(MulticastDelegate)) + { + var eventDelegate = (MulticastDelegate)field.GetValue(null); + if (eventDelegate != null && field.Name.Contains(eventName)) + { + foreach (var handler in eventDelegate.GetInvocationList()) + { + eventInfo.RemoveEventHandler(null, handler); + } + break; + } + } + } + } + [TestMethod] public void TestGetConfigFile() { @@ -63,5 +110,109 @@ public void TestGetConfiguration() var pp = new TestPlugin(); pp.TestGetConfiguration().Key.Should().Be("PluginConfiguration"); } + + [TestMethod] + public async Task TestOnException() + { + _ = new TestPlugin(); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + // Register TestNonPlugin that throws exceptions + _ = new TestNonPlugin(); + + // Ensure exception is thrown + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + }); + + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittedAsync(null, null); + }); + } + + [TestMethod] + public async Task TestOnPluginStopped() + { + var pp = new TestPlugin(); + Assert.AreEqual(false, pp.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(true, pp.IsStopped); + } + + [TestMethod] + public async Task TestOnPluginStopOnException() + { + // pp will stop on exception. + var pp = new TestPlugin(); + Assert.AreEqual(false, pp.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(true, pp.IsStopped); + + // pp2 will not stop on exception. + var pp2 = new TestPlugin(UnhandledExceptionPolicy.Ignore); + Assert.AreEqual(false, pp2.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(false, pp2.IsStopped); + } + + [TestMethod] + public async Task TestOnNodeStopOnPluginException() + { + // node will stop on pp exception. + var pp = new TestPlugin(UnhandledExceptionPolicy.StopNode); + Assert.AreEqual(false, pp.IsStopped); + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + }); + + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittedAsync(null, null); + }); + + Assert.AreEqual(false, pp.IsStopped); + } } }