Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Neo Plugin New feature] UnhandledExceptionPolicy on Plugin Unhandled Exception #3311

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/Neo/Ledger/Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,33 @@ private static async Task InvokeHandlersAsync(Delegate[] handlers, Action<Delega
{
try
{
// skip stopped plugin.
if (handler.Target is Plugin { IsStopped: true })
{
return;
}

handlerAction(handler);
}
catch (Exception ex) when (handler.Target is Plugin)
catch (Exception ex) when (handler.Target is Plugin plugin)
{
// Log the exception and continue with the next handler
// Isolate the plugin exception
Utility.Log(nameof(handler.Target), LogLevel.Error, ex);
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 ArgumentOutOfRangeException();
}

Utility.Log(nameof(plugin), LogLevel.Error, ex);
}
catch (Exception ex)
{
Expand All @@ -522,7 +542,6 @@ private static async Task InvokeHandlersAsync(Delegate[] handlers, Action<Delega
exceptions.ForEach(e => throw e);
}


/// <summary>
/// Gets a <see cref="Akka.Actor.Props"/> object used for creating the <see cref="Blockchain"/> actor.
/// </summary>
Expand Down
64 changes: 50 additions & 14 deletions src/Neo/Plugins/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public abstract class Plugin : IDisposable
/// <summary>
/// The directory containing the plugin folders. Files can be contained in any subdirectory.
/// </summary>
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;

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

/// <summary>
/// If the plugin should be stopped when an exception is thrown.
/// Default is <see langword="true"/>.
/// </summary>
protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode;

/// <summary>
/// The plugin will be stopped if an exception is thrown.
/// But it also depends on <see cref="UnhandledExceptionPolicy"/>.
/// </summary>
internal bool IsStopped { get; set; }

static Plugin()
{
if (!Directory.Exists(PluginsDirectory)) return;
configWatcher = new FileSystemWatcher(PluginsDirectory)
{
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;
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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";
Expand Down Expand Up @@ -150,7 +166,8 @@ public virtual void Dispose()
/// <returns>The content of the configuration file read.</returns>
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)
Expand Down Expand Up @@ -187,6 +204,7 @@ internal static void LoadPlugins()
catch { }
}
}

foreach (Assembly assembly in assemblies)
{
LoadPlugin(assembly);
Expand Down Expand Up @@ -229,15 +247,33 @@ protected internal virtual void OnSystemLoaded(NeoSystem system)
/// <returns><see langword="true"/> if the <paramref name="message"/> is handled by a plugin; otherwise, <see langword="false"/>.</returns>
public static bool SendMessage(object message)
{
try
{
return Plugins.Any(plugin => plugin.OnMessage(message));
}
catch (Exception ex)
{
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
return false;
}

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 ArgumentOutOfRangeException();
}
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
return false;
}
}
);
}
}
}
33 changes: 33 additions & 0 deletions src/Neo/Plugins/PluginSettings.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
20 changes: 20 additions & 0 deletions src/Neo/Plugins/UnhandledExceptionPolicy.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
3 changes: 2 additions & 1 deletion src/Plugins/ApplicationLogs/ApplicationLogs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"Path": "ApplicationLogs_{0}",
"Network": 860833102,
"MaxStackSize": 65535,
"Debug": false
"Debug": false,
"UnhandledExceptionPolicy": "StopPlugin"
},
"Dependency": [
"RpcServer"
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/ApplicationLogs/LogReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class LogReader : Plugin

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

Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/ApplicationLogs/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Neo.Plugins.ApplicationLogs
{
internal class Settings
internal class Settings : PluginSettings
{
public string Path { get; }
public uint Network { get; }
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Plugins/DBFTPlugin/DBFTPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class DBFTPlugin : Plugin

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

protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy;

public DBFTPlugin()
{
RemoteNode.MessageReceived += RemoteNode_MessageReceived;
Expand Down
3 changes: 2 additions & 1 deletion src/Plugins/DBFTPlugin/DBFTPlugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"AutoStart": false,
"Network": 860833102,
"MaxBlockSize": 2097152,
"MaxBlockSystemFee": 150000000000
"MaxBlockSystemFee": 150000000000,
"UnhandledExceptionPolicy": "StopNode"
}
}
4 changes: 2 additions & 2 deletions src/Plugins/DBFTPlugin/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Neo.Plugins.DBFTPlugin
{
public class Settings
public class Settings : PluginSettings
{
public string RecoveryLogs { get; }
public bool IgnoreRecoveryLogs { get; }
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Plugins/OracleService/OracleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public class OracleService : Plugin

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()
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/OracleService/OracleService.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"MaxOracleTimeout": 10000,
"AllowPrivateHost": false,
"AllowedContentTypes": [ "application/json" ],
"UnhandledExceptionPolicy": "Ignore",
"Https": {
"Timeout": 5000
},
Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/OracleService/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public NeoFSSettings(IConfigurationSection section)
}
}

class Settings
class Settings : PluginSettings
{
public uint Network { get; }
public Uri[] Nodes { get; }
Expand All @@ -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<string>(), UriKind.Absolute)).ToArray();
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/RpcServer/RpcServer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"PluginConfiguration": {
"UnhandledExceptionPolicy": "Ignore",
"Servers": [
{
"Network": 860833102,
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/RpcServer/RpcServerPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class RpcServerPlugin : Plugin
private static readonly Dictionary<uint, List<object>> handlers = new();

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

protected override void Configure()
{
Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/RpcServer/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@

namespace Neo.Plugins.RpcServer
{
class Settings
class Settings : PluginSettings
{
public IReadOnlyList<RpcServerSettings> 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();
}
Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/StateService/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Neo.Plugins.StateService
{
internal class Settings
internal class Settings : PluginSettings
{
public string Path { get; }
public bool FullState { get; }
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Plugins/StateService/StatePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class StatePlugin : Plugin
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;

Expand Down
3 changes: 2 additions & 1 deletion src/Plugins/StateService/StateService.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"FullState": false,
"Network": 860833102,
"AutoVerify": false,
"MaxFindResultItems": 100
"MaxFindResultItems": 100,
"UnhandledExceptionPolicy": "StopPlugin"
},
"Dependency": [
"RpcServer"
Expand Down
Loading