Skip to content

Commit 1488958

Browse files
Jimmycschuchardt88shargonNGDAdmin
authored
Fix plugin exception (#3426)
* Revert "Revert "Plugin unhandled exception (#3349)" (#3366)" This reverts commit f307a31. * ensure leveldb is not used in multithread env * Revert "Revert "Plugin unhandled exception (#3349)" (#3366)" This reverts commit f307a31. * remove async. * Update src/Neo/Plugins/UnhandledExceptionPolicy.cs Co-authored-by: Christopher Schuchardt <[email protected]> * not use linq * Update src/Neo/Plugins/UnhandledExceptionPolicy.cs Co-authored-by: Christopher Schuchardt <[email protected]> * use ignore case * Update src/Plugins/TokensTracker/TokensTracker.cs * Update src/Neo/Plugins/Plugin.cs Co-authored-by: Shargon <[email protected]> --------- Co-authored-by: Christopher Schuchardt <[email protected]> Co-authored-by: Shargon <[email protected]> Co-authored-by: NGD Admin <[email protected]>
1 parent 1dcec2e commit 1488958

26 files changed

+421
-31
lines changed

src/Neo/Ledger/Blockchain.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Neo.Network.P2P;
1717
using Neo.Network.P2P.Payloads;
1818
using Neo.Persistence;
19+
using Neo.Plugins;
1920
using Neo.SmartContract;
2021
using Neo.SmartContract.Native;
2122
using Neo.VM;
@@ -24,6 +25,7 @@
2425
using System.Collections.Immutable;
2526
using System.Diagnostics;
2627
using System.Linq;
28+
using System.Runtime.CompilerServices;
2729

2830
namespace Neo.Ledger
2931
{
@@ -468,10 +470,10 @@ private void Persist(Block block)
468470
Context.System.EventStream.Publish(application_executed);
469471
all_application_executed.Add(application_executed);
470472
}
471-
Committing?.Invoke(system, block, snapshot, all_application_executed);
473+
InvokeCommitting(system, block, snapshot, all_application_executed);
472474
snapshot.Commit();
473475
}
474-
Committed?.Invoke(system, block);
476+
InvokeCommitted(system, block);
475477
system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView);
476478
extensibleWitnessWhiteList = null;
477479
block_cache.Remove(block.PrevHash);
@@ -480,6 +482,56 @@ private void Persist(Block block)
480482
Debug.Assert(header.Index == block.Index);
481483
}
482484

485+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
486+
internal static void InvokeCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList<ApplicationExecuted> applicationExecutedList)
487+
{
488+
InvokeHandlers(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList));
489+
}
490+
491+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
492+
internal static void InvokeCommitted(NeoSystem system, Block block)
493+
{
494+
InvokeHandlers(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block));
495+
}
496+
497+
private static void InvokeHandlers(Delegate[] handlers, Action<Delegate> handlerAction)
498+
{
499+
if (handlers == null) return;
500+
501+
foreach (var handler in handlers)
502+
{
503+
try
504+
{
505+
// skip stopped plugin.
506+
if (handler.Target is Plugin { IsStopped: true })
507+
{
508+
continue;
509+
}
510+
511+
handlerAction(handler);
512+
}
513+
catch (Exception ex) when (handler.Target is Plugin plugin)
514+
{
515+
Utility.Log(nameof(plugin), LogLevel.Error, ex);
516+
switch (plugin.ExceptionPolicy)
517+
{
518+
case UnhandledExceptionPolicy.StopNode:
519+
throw;
520+
case UnhandledExceptionPolicy.StopPlugin:
521+
//Stop plugin on exception
522+
plugin.IsStopped = true;
523+
break;
524+
case UnhandledExceptionPolicy.Ignore:
525+
// Log the exception and continue with the next handler
526+
break;
527+
default:
528+
throw new InvalidCastException(
529+
$"The exception policy {plugin.ExceptionPolicy} is not valid.");
530+
}
531+
}
532+
}
533+
}
534+
483535
/// <summary>
484536
/// Gets a <see cref="Akka.Actor.Props"/> object used for creating the <see cref="Blockchain"/> actor.
485537
/// </summary>

src/Neo/Plugins/Plugin.cs

Lines changed: 63 additions & 6 deletions
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 StopNode.
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,46 @@ 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+
foreach (var plugin in Plugins)
251+
{
252+
if (plugin.IsStopped)
253+
{
254+
continue;
255+
}
256+
257+
bool result;
258+
try
259+
{
260+
result = plugin.OnMessage(message);
261+
}
262+
catch (Exception ex)
263+
{
264+
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
265+
266+
switch (plugin.ExceptionPolicy)
267+
{
268+
case UnhandledExceptionPolicy.StopNode:
269+
throw;
270+
case UnhandledExceptionPolicy.StopPlugin:
271+
plugin.IsStopped = true;
272+
break;
273+
case UnhandledExceptionPolicy.Ignore:
274+
break;
275+
default:
276+
throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid.");
277+
}
278+
279+
continue; // Skip to the next plugin if an exception is handled
280+
}
281+
282+
if (result)
283+
{
284+
return true;
285+
}
286+
}
287+
288+
return false;
233289
}
290+
234291
}
235292
}

src/Neo/Plugins/PluginSettings.cs

Lines changed: 33 additions & 0 deletions
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, true, out UnhandledExceptionPolicy policy))
26+
{
27+
return policy;
28+
}
29+
30+
throw new InvalidParameterException($"{policyString} is not a valid UnhandledExceptionPolicy");
31+
}
32+
}
33+
}
Lines changed: 20 additions & 0 deletions
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 : byte
15+
{
16+
Ignore = 0,
17+
StopPlugin = 1,
18+
StopNode = 2,
19+
}
20+
}

src/Plugins/ApplicationLogs/ApplicationLogs.json

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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);

0 commit comments

Comments
 (0)