Skip to content

Commit

Permalink
Merge pull request planetarium#3935 from planetarium/exp/sdk/action-l…
Browse files Browse the repository at this point in the history
…oader

Action loader from external assembly
  • Loading branch information
s2quake authored Sep 9, 2024
2 parents cdd701e + 24c08d7 commit acd0bce
Show file tree
Hide file tree
Showing 33 changed files with 493 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.7.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand All @@ -26,6 +27,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\tools\Libplanet.Explorer.Executable\Libplanet.Explorer.Executable.csproj" />
<ProjectReference Include="..\..\..\src\Libplanet.Crypto.Secp256k1\Libplanet.Crypto.Secp256k1.csproj" />
<ProjectReference Include="..\Libplanet.Node.Extensions\Libplanet.Node.Extensions.csproj" />
<ProjectReference Include="..\Libplanet.Node\Libplanet.Node.csproj" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions sdk/node/Libplanet.Node.Executable/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Libplanet.Node.API.Services;
using Libplanet.Node.Extensions;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Serilog;
using Serilog.Events;

var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
Expand Down
26 changes: 26 additions & 0 deletions sdk/node/Libplanet.Node.Executable/appsettings-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,22 @@
}
}
},
"Action": {
"title": "ActionOptions",
"type": "object",
"additionalProperties": false,
"properties": {
"ModulePath": {
"type": "string"
},
"ActionLoaderType": {
"type": "string"
},
"PolicyActionRegistryType": {
"type": "string"
}
}
},
"Genesis": {
"title": "GenesisOptions",
"type": "object",
Expand Down Expand Up @@ -1207,6 +1223,12 @@
"type": "string",
"description": "The endpoint of the node to block sync.",
"pattern": "^$|^(?:[0-9a-fA-F]{130}|[0-9a-fA-F]{66}),(?:(?:[a-zA-Z0-9\\-\\.]+)|(?:\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})),\\d{1,5}$"
},
"TrustedAppProtocolVersionSigners": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down Expand Up @@ -1252,6 +1274,10 @@
"description": "Type 'ExplorerOptions' does not have a description.",
"$ref": "#/definitions/Explorer"
},
"Action": {
"description": "Type 'ActionOptions' does not have a description.",
"$ref": "#/definitions/Action"
},
"Genesis": {
"description": "Options for the genesis block.",
"$ref": "#/definitions/Genesis"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public static ILibplanetNodeBuilder AddLibplanetNode(
.Bind(configuration.GetSection(StoreOptions.Position));
services.AddSingleton<IConfigureOptions<StoreOptions>, StoreOptionsConfigurator>();

services.AddOptions<ActionOptions>()
.Bind(configuration.GetSection(ActionOptions.Position));

services.AddOptions<SwarmOptions>()
.Bind(configuration.GetSection(SwarmOptions.Position));
services.AddSingleton<IConfigureOptions<SwarmOptions>, SwarmOptionsConfigurator>();
Expand All @@ -43,6 +46,8 @@ public static ILibplanetNodeBuilder AddLibplanetNode(
services.AddSingleton<PolicyService>();
services.AddSingleton<StoreService>();
services.AddSingleton(s => (IStoreService)s.GetRequiredService<StoreService>());
services.AddSingleton<ActionService>();
services.AddSingleton(s => (IActionService)s.GetRequiredService<ActionService>());
services.AddSingleton<IBlockChainService, BlockChainService>();
services.AddSingleton<IReadChainService, ReadChainService>();
services.AddSingleton<TransactionService>();
Expand Down
6 changes: 6 additions & 0 deletions sdk/node/Libplanet.Node.Tests/Libplanet.Node.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="Moq.AutoMock" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

<ItemGroup>
Expand All @@ -24,4 +25,9 @@
<ProjectReference Include="..\Libplanet.Node.Extensions\Libplanet.Node.Extensions.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Remove=".\Services\ActionServiceTestSource.cs" />
<EmbeddedResource Include=".\Services\ActionServiceTestSource.cs" />
</ItemGroup>

</Project>
57 changes: 57 additions & 0 deletions sdk/node/Libplanet.Node.Tests/Services/ActionServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Libplanet.Action;
using Libplanet.Action.Loader;
using Libplanet.Node.Options;
using Libplanet.Node.Services;
using Microsoft.Extensions.DependencyInjection;

namespace Libplanet.Node.Tests.Services;

public class ActionServiceTest(TempDirectoryFixture tempDirectoryFixture)
: IClassFixture<TempDirectoryFixture>
{
private readonly TempDirectoryFixture _tempDirectoryFixture = tempDirectoryFixture;

[Fact]
public void Base_Test()
{
var serviceProvider = TestUtility.CreateServiceProvider();
var actionService = serviceProvider.GetRequiredService<IActionService>();

Assert.IsType<AggregateTypedActionLoader>(actionService.ActionLoader);
Assert.IsType<PolicyActionsRegistry>(actionService.PolicyActionsRegistry);
}

[Fact]
public void Base_WithModulePath_Test()
{
var actionLoaderType = "Libplanet.Node.DumbActions.DumbActionLoader";
var policyActionRegistryType = "Libplanet.Node.DumbActions.DumbActionPolicyActionsRegistry";
var codePath = "Libplanet.Node.Tests.Services.ActionServiceTestSource.cs";
var codeStream = typeof(ActionServiceTest).Assembly.GetManifestResourceStream(codePath)
?? throw new FileNotFoundException($"Resource '{codePath}' not found.");
using var reader = new StreamReader(codeStream);
var code = reader.ReadToEnd();
var assemblyName = Path.GetRandomFileName();
var assemblyPath = $"{_tempDirectoryFixture.GetRandomFileName()}.dll";

var settings = new Dictionary<string, string?>
{
[$"{ActionOptions.Position}:{nameof(ActionOptions.ModulePath)}"]
= assemblyPath,
[$"{ActionOptions.Position}:{nameof(ActionOptions.ActionLoaderType)}"]
= actionLoaderType,
[$"{ActionOptions.Position}:{nameof(ActionOptions.PolicyActionRegistryType)}"]
= policyActionRegistryType,
};

RuntimeCompiler.CompileCode(code, assemblyName, assemblyPath);

var serviceProvider = TestUtility.CreateServiceProvider(settings);
var actionService = serviceProvider.GetRequiredService<IActionService>();

Assert.Equal(actionLoaderType, actionService.ActionLoader.GetType().FullName);
Assert.Equal(
expected: policyActionRegistryType,
actual: actionService.PolicyActionsRegistry.GetType().FullName);
}
}
63 changes: 63 additions & 0 deletions sdk/node/Libplanet.Node.Tests/Services/ActionServiceTestSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// This code does not compile because it is used by ActionServiceTest test.
#pragma warning disable MEN008 // A file's name should match or include the name of the main type it contains.
using System.Collections.Immutable;
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Action.Loader;
using Libplanet.Action.State;
using Libplanet.Action.Sys;

namespace Libplanet.Node.DumbActions;

public class DumbAction : IAction
{
public IValue PlainValue => Dictionary.Empty;

public void LoadPlainValue(IValue plainValue)
{
// Do nothing.
}

public IWorld Execute(IActionContext context) =>
context.PreviousState;
}

public sealed class DumbBeginAction : DumbAction
{
}

public sealed class DumbEndAction : DumbAction
{
}

public sealed class DumbBeginTxAction : DumbAction
{
}

public sealed class DumbEndTxAction : DumbAction
{
}

public sealed class DumbActionLoader : IActionLoader
{
public IAction LoadAction(long index, IValue value)
{
if (Registry.IsSystemAction(value))
{
return Registry.Deserialize(value);
}

return new DumbAction();
}
}

public sealed class DumbActionPolicyActionsRegistry : IPolicyActionsRegistry
{
public ImmutableArray<IAction> BeginBlockActions => [new DumbBeginAction()];

public ImmutableArray<IAction> EndBlockActions => [new DumbEndAction()];

public ImmutableArray<IAction> BeginTxActions => [new DumbBeginTxAction()];

public ImmutableArray<IAction> EndTxActions => [new DumbEndTxAction()];
}
52 changes: 52 additions & 0 deletions sdk/node/Libplanet.Node.Tests/Services/RuntimeCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Bencodex.Types;
using Libplanet.Action;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Libplanet.Node.Tests.Services;

internal static class RuntimeCompiler
{
public static void CompileCode(string code, string assemblyName, string assemblyPath)
{
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var references = new MetadataReference[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IAction).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IValue).Assembly.Location),
MetadataReference.CreateFromFile(GetRuntimeLibraryPath("netstandard.dll")),
MetadataReference.CreateFromFile(GetRuntimeLibraryPath("System.Runtime.dll")),
MetadataReference.CreateFromFile(
GetRuntimeLibraryPath("System.Collections.Immutable.dll")),
};

var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var compilation = CSharpCompilation.Create(
assemblyName, [syntaxTree], references, options);

// 어셈블리 스트림 생성
using var fs = new FileStream(assemblyPath, FileMode.Create);
var result = compilation.Emit(fs);

if (!result.Success)
{
var sb = new StringBuilder();
sb.AppendLine("Compilation failed.");
foreach (var diagnostic in result.Diagnostics)
{
sb.AppendLine(diagnostic.ToString());
}

throw new InvalidOperationException(sb.ToString());
}

fs.Close();
}

private static string GetRuntimeLibraryPath(string name)
=> Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), name);
}
37 changes: 37 additions & 0 deletions sdk/node/Libplanet.Node.Tests/TempDirectoryFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Libplanet.Node.Tests;

public sealed class TempDirectoryFixture : IDisposable
{
public TempDirectoryFixture()
{
TempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(TempDirectory);
}

public string TempDirectory { get; }

public string GetRandomFileName()
{
if (!Directory.Exists(TempDirectory))
{
Directory.CreateDirectory(TempDirectory);
}

return Path.Combine(TempDirectory, Path.GetRandomFileName());
}

public void Dispose()
{
if (Directory.Exists(TempDirectory))
{
try
{
Directory.Delete(TempDirectory, true);
}
catch (UnauthorizedAccessException)
{
// ignore
}
}
}
}
31 changes: 31 additions & 0 deletions sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Reflection;
using System.Runtime.Loader;

namespace Libplanet.Node.Actions;

internal sealed class PluginLoadContext(string pluginPath) : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver = new(pluginPath);

protected override Assembly? Load(AssemblyName assemblyName)
{
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath is not null)
{
return LoadFromAssemblyPath(assemblyPath);
}

return null;
}

protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath is not null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}

return IntPtr.Zero;
}
}
Loading

0 comments on commit acd0bce

Please sign in to comment.