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

Fix plugin exception #3426

Merged
merged 20 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
53 changes: 51 additions & 2 deletions src/Neo/Ledger/Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
InvokeCommitting(system, block, snapshot, all_application_executed);
snapshot.Commit();
}
Committed?.Invoke(system, block);
InvokeCommitted(system, block);
system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView);
extensibleWitnessWhiteList = null;
block_cache.Remove(block.PrevHash);
Expand All @@ -480,6 +484,51 @@ private void Persist(Block block)
Debug.Assert(header.Index == block.Index);
}

internal static void InvokeCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList<ApplicationExecuted> applicationExecutedList)
{
InvokeHandlers(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList));
}

internal static void InvokeCommitted(NeoSystem system, Block block)
{
InvokeHandlers(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block));
}

private static void InvokeHandlers(Delegate[] handlers, Action<Delegate> handlerAction)
{
handlers?.ForEach(handler =>
{
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:
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);
}
});
}

/// <summary>
/// Gets a <see cref="Akka.Actor.Props"/> object used for creating the <see cref="Blockchain"/> actor.
/// </summary>
Expand Down
56 changes: 50 additions & 6 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"/>.
Jim8y marked this conversation as resolved.
Show resolved Hide resolved
/// </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,7 +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)
{
return Plugins.Any(plugin => plugin.OnMessage(message));

return Plugins.Any(plugin =>
Jim8y marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
}
);
}
}
}
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))
shargon marked this conversation as resolved.
Show resolved Hide resolved
{
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
shargon marked this conversation as resolved.
Show resolved Hide resolved
{
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better in almost all of them StopNode by default, is the current behaviour

Copy link
Contributor Author

@Jim8y Jim8y Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothoing was changed since the last review but the deledage invoke. Different plugin has different behavior on unhandled exception that are precisely set by superboy. Default behavior means nothing here.

},
"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 @@ -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

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, 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;
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
5 changes: 4 additions & 1 deletion src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
using Neo.IO.Data.LevelDB;
using Neo.Persistence;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace Neo.Plugins.Storage
{
Expand All @@ -27,7 +30,7 @@ public LevelDBStore()

public IStore GetStore(string path)
{
if (Environment.CommandLine.Split(' ').Any(p => p == "/repair" || p == "--repair"))
if (Environment.CommandLine.Split(' ').Any(p => p is "/repair" or "--repair"))
Jim8y marked this conversation as resolved.
Show resolved Hide resolved
DB.Repair(path, Options.Default);
return new Store(path);
}
Expand Down
Loading
Loading