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

Worknet plugins/extensions #7

Merged
merged 11 commits into from
Jul 11, 2023
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": "worknet run",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/worknet/bin/Debug/net6.0/neo-worknet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/worknet",
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": "list storage",
"type": "coreclr",
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ dotnet tool update Neo.WorkNet -g --prerelease
different directories. Full details on installing and updating .NET tools are available in the
[official documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools).

## Installation (Preview)
## Shell

### Installation (Preview)

The Neo Blockchain Toolkit has a public [package feed](https://dev.azure.com/ngdenterprise/Build/_artifacts).
that contains interim builds of Neo Shell and Worknet. You can unreleased preview builds of Neo-Shell by using the
Expand All @@ -93,7 +95,7 @@ Several Neo sample projects like
[NeoContributorToken](https://github.com/ngdenterprise/neo-contrib-token)
use a NuGet.config file.

## Extending NEO Shell
### Extending NEO Shell

NEO Shell allows developers to extend its functionality by adding custom commands. To do this, create a ~/.neo/neosh-extensions.json file that contains a list of commands executable from the shell. NEO Shell communicates with extensions using standard input/output.

Expand Down Expand Up @@ -155,3 +157,15 @@ var script = contractHash.MakeScript("transfer", toHash, idBytes, string.Empty);
var payload = new { Script = Convert.ToBase64String(script), Account = this.Account, Trace = this.Trace, Json = this.Json };
Console.WriteLine(JsonConvert.SerializeObject(payload));
```

## Worknet

### Extending NEO Worknet

Neo Worknet's capabilities can be extended through the use of plugins or modules. The [Neo Modules](https://github.com/neo-project/neo-modules/tree/master) package offers a variety of plugins compatible with both Neo and Neo Worknet. To utilize these plugins, simply copy the DLL files containing the plugins into either the ~/.neo/plugins directory or the /plugins directory located within the worknet executable folder. Upon initiation, Neo Worknet will automatically load and activate these plugins.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also mention the need to override ConfigFile here in the readme?


As an illustrative example, we've included a sample Worknet plugin, WorkNetLogger, in the /workenet-ext directory. This plugin is designed to direct Worknet's logs to a specified file. It operates by reading the designated log file path from a custom config.json file, and then recording the logs into this file.

If your Worknet plugin requires custom configuration, it's essential to ensure that the plugin class overrides the ConfigFile property. This enables the GetConfiguration() method to locate the config.json file. Without the override, the default ConfigFile value will be sourced from the PluginsDirectory property, which is relative to the assembly location.

After building the plugin, copy both the "worknet-ext-filelogger.dll" and config.json files to the ~/.neo/plugins directory. Upon startup, Worknet will automatically identify, load and execute the plugin. If preferred, you may also create a /plugins directory within the same directory as the neo-worknet executable, and relocate the DLL to this /plugins directory.
1 change: 1 addition & 0 deletions dirs.proj
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<ProjectReference Include="src\worknet\neoworknet.csproj" />
<ProjectReference Include="src\shell\neoshell.csproj" />
<ProjectReference Include="src\shell-ext\nft\neonft.csproj" />
<ProjectReference Include="src\worknet-ext\logger\filelogger.csproj" />
</ItemGroup>
</Project>
92 changes: 92 additions & 0 deletions src/worknet-ext/logger/WorkNetLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Microsoft.Extensions.Configuration;
using Neo;
using Neo.Ledger;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.Plugins;
using Neo.SmartContract;
using Neo.SmartContract.Native;

namespace WorkNetExt;

public class WorkNetLogger : Plugin
{
private string _logFile = "./logger.log";

NeoSystem? neoSystem;

public WorkNetLogger()
{
Blockchain.Committing += OnCommitting;
ApplicationEngine.Log += OnAppEngineLog!;
Neo.Utility.Logging += OnNeoUtilityLog;

}

public override void Dispose()
{
Neo.Utility.Logging -= OnNeoUtilityLog;
ApplicationEngine.Log -= OnAppEngineLog!;
Blockchain.Committing -= OnCommitting;
GC.SuppressFinalize(this);
}

// If Worknet plugins require custom configuration. The plugins are required to override the ConfigFile property so GetConfiguration() method can locate the config.json file.
// Without this override, the default ConfigFile value is read from the PluginsDirectory property, which is relative to the assembly location.
// However, WorkNet plugins are loaded from either ~/.neo/plugins folder or /plugins folder in the worknet exe directory.
public override string ConfigFile => System.IO.Path.Combine(AppContext.BaseDirectory, "plugins", "config.json");

protected override void Configure()
{
IConfigurationSection config = GetConfiguration();
_logFile = config.GetValue<string>("LogFile", _logFile);

}
protected override void OnSystemLoaded(NeoSystem system)
{
if (neoSystem is not null) throw new Exception($"{nameof(OnSystemLoaded)} already called");
neoSystem = system;
base.OnSystemLoaded(system);
}

void OnAppEngineLog(object sender, LogEventArgs args)
{
var container = args.ScriptContainer is null
? string.Empty
: $" [{args.ScriptContainer.GetType().Name}]";

WriteToFile($"{GetContractName(args.ScriptHash)} Log: \"{args.Message}\" {container}");
}

void OnNeoUtilityLog(string source, LogLevel level, object message)
{
WriteToFile($"{DateTimeOffset.Now:HH:mm:ss.ff} {source} {level} {message}");
}

void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList<Blockchain.ApplicationExecuted> applicationExecutedList)
{
WriteToFile($"Blockchain Committing: {block.Hash} {block.Index} {block.Timestamp} {block.Transactions.Length} txs");
}

protected string GetContractName(UInt160 scriptHash)
{
if (neoSystem is not null)
{
var contract = NativeContract.ContractManagement.GetContract(neoSystem.StoreView, scriptHash);
if (contract is not null)
{
return contract.Manifest.Name;
}
}

return scriptHash.ToString();
}

private void WriteToFile(string logMessage)
{
using(StreamWriter writer = new StreamWriter(_logFile, true))
{
writer.WriteLine(logMessage);
}
}
}
6 changes: 6 additions & 0 deletions src/worknet-ext/logger/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"PluginConfiguration": {
"LogFile": "./samplePlugLogger.log"

}
}
34 changes: 34 additions & 0 deletions src/worknet-ext/logger/filelogger.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>worknet-ext-filelogger</AssemblyName>
<OutputType>library</OutputType>
<PackageId>WorkNetExt.FileLogger</PackageId>
<PackAsTool>true</PackAsTool>
<RootNamespace>WorkNetExt</RootNamespace>
<PackageIcon />
</PropertyGroup>

<ItemGroup>
<None Include="../../neo-logo-72.png" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>

<Choose>
<When Condition=" '$(BlockchainToolkitLibraryVersion)' == 'local'">
<ItemGroup>
<ProjectReference Include="$(BlockchainToolkitLibraryLocalPath)\src\bctklib\bctklib.csproj" />
<ProjectReference Include="$(NeoMonorepoPath)\modules\src\DBFTPlugin\DBFTPlugin.csproj" />
<ProjectReference Include="$(NeoMonorepoPath)\modules\src\RpcServer\RpcServer.csproj" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<PackageReference Include="Neo.BlockchainToolkit.Library"
Version="$(BlockchainToolkitLibraryVersion)" />
<PackageReference Include="Neo.Consensus.DBFT" Version="$(NeoVersion)" />
<PackageReference Include="Neo.Plugins.RpcServer" Version="$(NeoVersion)" />
</ItemGroup>
</Otherwise>
</Choose>

</Project>
46 changes: 46 additions & 0 deletions src/worknet/Node/PluginHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;
using Neo.Plugins;

static class PluginHandler
{
public static void LoadPlugins(string directory, TextWriter? writer = null)
{
if (!Directory.Exists(directory)) return;
List<Assembly> assemblies = new();

foreach (var filename in Directory.EnumerateFiles(directory, "*.dll", SearchOption.TopDirectoryOnly))
{
try
{
assemblies.Add(Assembly.Load(File.ReadAllBytes(filename)));
writer?.WriteLine($"Loaded plugin: {filename}");
}
catch { }
}

foreach (Assembly assembly in assemblies)
{
LoadPlugin(assembly, writer);
}
}

public static void LoadPlugin(Assembly assembly, TextWriter? writer = null)
{
foreach (Type type in assembly.ExportedTypes)
{
if (!type.IsSubclassOf(typeof(Plugin))) continue;
if (type.IsAbstract) continue;

ConstructorInfo? constructor = type.GetConstructor(Type.EmptyTypes);
try
{
constructor?.Invoke(null);
}
catch (Exception ex)
{
writer?.WriteLine($"Failed to load plugin: {type.FullName}");
writer?.WriteLine(ex.Message);
}
}
}
}
3 changes: 2 additions & 1 deletion src/worknet/Node/WorkNetNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ public async Task RunAsync(uint secondsPerBlock, IConsole console, CancellationT
using var dbftPlugin = new Neo.Consensus.DBFTPlugin(GetConsensusSettings(chain));
using var rpcServerPlugin = new WorknetRpcServerPlugin(GetRpcServerSettings(chain), persistencePlugin, chain.Uri);
using var neoSystem = new Neo.NeoSystem(protocolSettings, storeProvider.Name);

PluginHandler.LoadPlugins(Path.Combine(AppContext.BaseDirectory, "plugins"), console.Out);
PluginHandler.LoadPlugins(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".neo", "plugins"), console.Out);
neoSystem.StartNode(new Neo.Network.P2P.ChannelsConfig
{
Tcp = new IPEndPoint(IPAddress.Loopback, chain.ConsensusNode.TcpPort),
Expand Down