Skip to content

Commit b2f060f

Browse files
Jim8ycschuchardt88shargonNGDAdmin
authored
Plugin unhandled exception (#3349)
* [Neo Core] Part 1. Isolate Plugins Exceptions from the Node. (#3309) * catch plugin exceptions. * add UT test * udpate format * make the test more complete * complete the ut test * format * complete UT tests with NonPlugin case * async invoke * Update src/Neo/Ledger/Blockchain.cs Co-authored-by: Christopher Schuchardt <cschuchardt88@gmail.com> --------- Co-authored-by: Christopher Schuchardt <cschuchardt88@gmail.com> * [Neo Plugin New feature] UnhandledExceptionPolicy on Plugin Unhandled Exception (#3311) * catch plugin exceptions. * add UT test * udpate format * make the test more complete * complete the ut test * format * complete UT tests with NonPlugin case * async invoke * stop plugin on exception * remove watcher from blockchain if uint test is done to avoid cross test data pollution. * add missing file * 3 different policy on handling plugin exception * add missing file * fix null warning * format * Apply suggestions from code review Clean * Update src/Neo/Plugins/PluginSettings.cs Co-authored-by: Shargon <shargon@gmail.com> * Update src/Neo/Plugins/PluginSettings.cs Co-authored-by: Christopher Schuchardt <cschuchardt88@gmail.com> * Update src/Plugins/TokensTracker/TokensTracker.cs Co-authored-by: Christopher Schuchardt <cschuchardt88@gmail.com> * Update src/Plugins/TokensTracker/TokensTracker.json --------- Co-authored-by: Shargon <shargon@gmail.com> Co-authored-by: Christopher Schuchardt <cschuchardt88@gmail.com> * make the exception message clear --------- Co-authored-by: Christopher Schuchardt <cschuchardt88@gmail.com> Co-authored-by: Shargon <shargon@gmail.com> Co-authored-by: NGD Admin <154295625+NGDAdmin@users.noreply.github.com>
1 parent f379dab commit b2f060f

26 files changed

+417
-29
lines changed

src/Neo/Ledger/Blockchain.cs

+64-2
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,22 @@
1212
using Akka.Actor;
1313
using Akka.Configuration;
1414
using Akka.IO;
15+
using Akka.Util.Internal;
1516
using Neo.IO.Actors;
1617
using Neo.Network.P2P;
1718
using Neo.Network.P2P.Payloads;
1819
using Neo.Persistence;
20+
using Neo.Plugins;
1921
using Neo.SmartContract;
2022
using Neo.SmartContract.Native;
2123
using Neo.VM;
2224
using System;
25+
using System.Collections.Concurrent;
2326
using System.Collections.Generic;
2427
using System.Collections.Immutable;
2528
using System.Diagnostics;
2629
using System.Linq;
30+
using System.Threading.Tasks;
2731

2832
namespace Neo.Ledger
2933
{
@@ -468,10 +472,10 @@ private void Persist(Block block)
468472
Context.System.EventStream.Publish(application_executed);
469473
all_application_executed.Add(application_executed);
470474
}
471-
Committing?.Invoke(system, block, snapshot, all_application_executed);
475+
_ = InvokeCommittingAsync(system, block, snapshot, all_application_executed);
472476
snapshot.Commit();
473477
}
474-
Committed?.Invoke(system, block);
478+
_ = InvokeCommittedAsync(system, block);
475479
system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView);
476480
extensibleWitnessWhiteList = null;
477481
block_cache.Remove(block.PrevHash);
@@ -480,6 +484,64 @@ private void Persist(Block block)
480484
Debug.Assert(header.Index == block.Index);
481485
}
482486

487+
internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList<ApplicationExecuted> applicationExecutedList)
488+
{
489+
await InvokeHandlersAsync(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList));
490+
}
491+
492+
internal static async Task InvokeCommittedAsync(NeoSystem system, Block block)
493+
{
494+
await InvokeHandlersAsync(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block));
495+
}
496+
497+
private static async Task InvokeHandlersAsync(Delegate[] handlers, Action<Delegate> handlerAction)
498+
{
499+
if (handlers == null) return;
500+
501+
var exceptions = new ConcurrentBag<Exception>();
502+
var tasks = handlers.Select(handler => Task.Run(() =>
503+
{
504+
try
505+
{
506+
// skip stopped plugin.
507+
if (handler.Target is Plugin { IsStopped: true })
508+
{
509+
return;
510+
}
511+
512+
handlerAction(handler);
513+
}
514+
catch (Exception ex) when (handler.Target is Plugin plugin)
515+
{
516+
switch (plugin.ExceptionPolicy)
517+
{
518+
case UnhandledExceptionPolicy.StopNode:
519+
exceptions.Add(ex);
520+
throw;
521+
case UnhandledExceptionPolicy.StopPlugin:
522+
//Stop plugin on exception
523+
plugin.IsStopped = true;
524+
break;
525+
case UnhandledExceptionPolicy.Ignore:
526+
// Log the exception and continue with the next handler
527+
break;
528+
default:
529+
throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid.");
530+
}
531+
532+
Utility.Log(nameof(plugin), LogLevel.Error, ex);
533+
}
534+
catch (Exception ex)
535+
{
536+
exceptions.Add(ex);
537+
}
538+
})).ToList();
539+
540+
await Task.WhenAll(tasks);
541+
542+
exceptions.ForEach(e => throw e);
543+
}
544+
483545
/// <summary>
484546
/// Gets a <see cref="Akka.Actor.Props"/> object used for creating the <see cref="Blockchain"/> actor.
485547
/// </summary>

src/Neo/Plugins/Plugin.cs

+50-6
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public abstract class Plugin : IDisposable
3333
/// <summary>
3434
/// The directory containing the plugin folders. Files can be contained in any subdirectory.
3535
/// </summary>
36-
public static readonly string PluginsDirectory = Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins");
36+
public static readonly string PluginsDirectory =
37+
Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins");
3738

3839
private static readonly FileSystemWatcher configWatcher;
3940

@@ -67,14 +68,27 @@ public abstract class Plugin : IDisposable
6768
/// </summary>
6869
public virtual Version Version => GetType().Assembly.GetName().Version;
6970

71+
/// <summary>
72+
/// If the plugin should be stopped when an exception is thrown.
73+
/// Default is <see langword="true"/>.
74+
/// </summary>
75+
protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode;
76+
77+
/// <summary>
78+
/// The plugin will be stopped if an exception is thrown.
79+
/// But it also depends on <see cref="UnhandledExceptionPolicy"/>.
80+
/// </summary>
81+
internal bool IsStopped { get; set; }
82+
7083
static Plugin()
7184
{
7285
if (!Directory.Exists(PluginsDirectory)) return;
7386
configWatcher = new FileSystemWatcher(PluginsDirectory)
7487
{
7588
EnableRaisingEvents = true,
7689
IncludeSubdirectories = true,
77-
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size,
90+
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime |
91+
NotifyFilters.LastWrite | NotifyFilters.Size,
7892
};
7993
configWatcher.Changed += ConfigWatcher_Changed;
8094
configWatcher.Created += ConfigWatcher_Changed;
@@ -106,7 +120,8 @@ private static void ConfigWatcher_Changed(object sender, FileSystemEventArgs e)
106120
{
107121
case ".json":
108122
case ".dll":
109-
Utility.Log(nameof(Plugin), LogLevel.Warning, $"File {e.Name} is {e.ChangeType}, please restart node.");
123+
Utility.Log(nameof(Plugin), LogLevel.Warning,
124+
$"File {e.Name} is {e.ChangeType}, please restart node.");
110125
break;
111126
}
112127
}
@@ -119,7 +134,8 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven
119134
AssemblyName an = new(args.Name);
120135

121136
Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ??
122-
AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name);
137+
AppDomain.CurrentDomain.GetAssemblies()
138+
.FirstOrDefault(a => a.GetName().Name == an.Name);
123139
if (assembly != null) return assembly;
124140

125141
string filename = an.Name + ".dll";
@@ -150,7 +166,8 @@ public virtual void Dispose()
150166
/// <returns>The content of the configuration file read.</returns>
151167
protected IConfigurationSection GetConfiguration()
152168
{
153-
return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build().GetSection("PluginConfiguration");
169+
return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build()
170+
.GetSection("PluginConfiguration");
154171
}
155172

156173
private static void LoadPlugin(Assembly assembly)
@@ -187,6 +204,7 @@ internal static void LoadPlugins()
187204
catch { }
188205
}
189206
}
207+
190208
foreach (Assembly assembly in assemblies)
191209
{
192210
LoadPlugin(assembly);
@@ -229,7 +247,33 @@ protected internal virtual void OnSystemLoaded(NeoSystem system)
229247
/// <returns><see langword="true"/> if the <paramref name="message"/> is handled by a plugin; otherwise, <see langword="false"/>.</returns>
230248
public static bool SendMessage(object message)
231249
{
232-
return Plugins.Any(plugin => plugin.OnMessage(message));
250+
251+
return Plugins.Any(plugin =>
252+
{
253+
try
254+
{
255+
return !plugin.IsStopped &&
256+
plugin.OnMessage(message);
257+
}
258+
catch (Exception ex)
259+
{
260+
switch (plugin.ExceptionPolicy)
261+
{
262+
case UnhandledExceptionPolicy.StopNode:
263+
throw;
264+
case UnhandledExceptionPolicy.StopPlugin:
265+
plugin.IsStopped = true;
266+
break;
267+
case UnhandledExceptionPolicy.Ignore:
268+
break;
269+
default:
270+
throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid.");
271+
}
272+
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
273+
return false;
274+
}
275+
}
276+
);
233277
}
234278
}
235279
}

src/Neo/Plugins/PluginSettings.cs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (C) 2015-2024 The Neo Project.
2+
//
3+
// PluginSettings.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
using Microsoft.Extensions.Configuration;
13+
using Org.BouncyCastle.Security;
14+
using System;
15+
16+
namespace Neo.Plugins;
17+
18+
public abstract class PluginSettings(IConfigurationSection section)
19+
{
20+
public UnhandledExceptionPolicy ExceptionPolicy
21+
{
22+
get
23+
{
24+
var policyString = section.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode));
25+
if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy))
26+
{
27+
return policy;
28+
}
29+
30+
throw new InvalidParameterException($"{policyString} is not a valid UnhandledExceptionPolicy");
31+
}
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (C) 2015-2024 The Neo Project.
2+
//
3+
// UnhandledExceptionPolicy.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
namespace Neo.Plugins
13+
{
14+
public enum UnhandledExceptionPolicy
15+
{
16+
Ignore = 0,
17+
StopPlugin = 1,
18+
StopNode = 2,
19+
}
20+
}

src/Plugins/ApplicationLogs/ApplicationLogs.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"Path": "ApplicationLogs_{0}",
44
"Network": 860833102,
55
"MaxStackSize": 65535,
6-
"Debug": false
6+
"Debug": false,
7+
"UnhandledExceptionPolicy": "StopPlugin"
78
},
89
"Dependency": [
910
"RpcServer"

src/Plugins/ApplicationLogs/LogReader.cs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class LogReader : Plugin, ICommittingHandler, ICommittedHandler, ILogHand
3838

3939
public override string Name => "ApplicationLogs";
4040
public override string Description => "Synchronizes smart contract VM executions and notifications (NotifyLog) on blockchain.";
41+
protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy;
4142

4243
#region Ctor
4344

src/Plugins/ApplicationLogs/Settings.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace Neo.Plugins.ApplicationLogs
1515
{
16-
internal class Settings
16+
internal class Settings : PluginSettings
1717
{
1818
public string Path { get; }
1919
public uint Network { get; }
@@ -23,7 +23,7 @@ internal class Settings
2323

2424
public static Settings Default { get; private set; }
2525

26-
private Settings(IConfigurationSection section)
26+
private Settings(IConfigurationSection section) : base(section)
2727
{
2828
Path = section.GetValue("Path", "ApplicationLogs_{0}");
2929
Network = section.GetValue("Network", 5195086u);

src/Plugins/DBFTPlugin/DBFTPlugin.cs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class DBFTPlugin : Plugin, IServiceAddedHandler, IMessageReceivedHandler,
3131

3232
public override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json");
3333

34+
protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy;
35+
3436
public DBFTPlugin()
3537
{
3638
RemoteNode.MessageReceived += ((IMessageReceivedHandler)this).RemoteNode_MessageReceived_Handler;

src/Plugins/DBFTPlugin/DBFTPlugin.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"AutoStart": false,
66
"Network": 860833102,
77
"MaxBlockSize": 2097152,
8-
"MaxBlockSystemFee": 150000000000
8+
"MaxBlockSystemFee": 150000000000,
9+
"UnhandledExceptionPolicy": "StopNode"
910
}
1011
}

src/Plugins/DBFTPlugin/Settings.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace Neo.Plugins.DBFTPlugin
1515
{
16-
public class Settings
16+
public class Settings : PluginSettings
1717
{
1818
public string RecoveryLogs { get; }
1919
public bool IgnoreRecoveryLogs { get; }
@@ -22,7 +22,7 @@ public class Settings
2222
public uint MaxBlockSize { get; }
2323
public long MaxBlockSystemFee { get; }
2424

25-
public Settings(IConfigurationSection section)
25+
public Settings(IConfigurationSection section) : base(section)
2626
{
2727
RecoveryLogs = section.GetValue("RecoveryLogs", "ConsensusState");
2828
IgnoreRecoveryLogs = section.GetValue("IgnoreRecoveryLogs", false);

src/Plugins/OracleService/OracleService.cs

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public class OracleService : Plugin, ICommittingHandler, IServiceAddedHandler, I
6262

6363
public override string Description => "Built-in oracle plugin";
6464

65+
protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy;
66+
6567
public override string ConfigFile => System.IO.Path.Combine(RootPath, "OracleService.json");
6668

6769
public OracleService()

src/Plugins/OracleService/OracleService.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"MaxOracleTimeout": 10000,
77
"AllowPrivateHost": false,
88
"AllowedContentTypes": [ "application/json" ],
9+
"UnhandledExceptionPolicy": "Ignore",
910
"Https": {
1011
"Timeout": 5000
1112
},

src/Plugins/OracleService/Settings.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public NeoFSSettings(IConfigurationSection section)
3737
}
3838
}
3939

40-
class Settings
40+
class Settings : PluginSettings
4141
{
4242
public uint Network { get; }
4343
public Uri[] Nodes { get; }
@@ -51,7 +51,7 @@ class Settings
5151

5252
public static Settings Default { get; private set; }
5353

54-
private Settings(IConfigurationSection section)
54+
private Settings(IConfigurationSection section) : base(section)
5555
{
5656
Network = section.GetValue("Network", 5195086u);
5757
Nodes = section.GetSection("Nodes").GetChildren().Select(p => new Uri(p.Get<string>(), UriKind.Absolute)).ToArray();

src/Plugins/RpcServer/RpcServer.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"PluginConfiguration": {
3+
"UnhandledExceptionPolicy": "Ignore",
34
"Servers": [
45
{
56
"Network": 860833102,

src/Plugins/RpcServer/RpcServerPlugin.cs

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class RpcServerPlugin : Plugin
2424
private static readonly Dictionary<uint, List<object>> handlers = new();
2525

2626
public override string ConfigFile => System.IO.Path.Combine(RootPath, "RpcServer.json");
27+
protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy;
2728

2829
protected override void Configure()
2930
{

0 commit comments

Comments
 (0)