diff --git a/.editorconfig b/.editorconfig index 3beec024..7b3f1984 100644 --- a/.editorconfig +++ b/.editorconfig @@ -83,6 +83,13 @@ dotnet_diagnostic.MEN007.severity = none # MEN016: Avoid top-level statements dotnet_diagnostic.MEN016.severity = none +[**/obj/**/*.cs] +# SA1200: Using directives should be placed correctly +dotnet_diagnostic.SA1200.severity = none + +# CS8981: The type name only contains lower-cased ascii characters +dotnet_diagnostic.CS8981.severity = none + [*.csproj] quote_type = double diff --git a/src/client/LibplanetConsole.Client.Executable/Application.cs b/src/client/LibplanetConsole.Client.Executable/Application.cs index 22a34c3b..7c22a225 100644 --- a/src/client/LibplanetConsole.Client.Executable/Application.cs +++ b/src/client/LibplanetConsole.Client.Executable/Application.cs @@ -29,7 +29,10 @@ public Application(ApplicationOptions options, object[] instances) options.ListenLocalhost(port, o => o.Protocols = HttpProtocols.Http2); }); - services.AddLogging(options.LogPath, "client.log", _filters); + if (options.LogPath != string.Empty) + { + services.AddLogging(options.LogPath, "client.log", _filters); + } services.AddSingleton(); services.AddSingleton(); diff --git a/src/client/LibplanetConsole.Client.Executable/ApplicationSettings.cs b/src/client/LibplanetConsole.Client.Executable/ApplicationSettings.cs index 0fc6be43..aff7d370 100644 --- a/src/client/LibplanetConsole.Client.Executable/ApplicationSettings.cs +++ b/src/client/LibplanetConsole.Client.Executable/ApplicationSettings.cs @@ -36,7 +36,7 @@ internal sealed record class ApplicationSettings [CommandProperty] [CommandSummary("Indicates the file path to save logs.")] - [Path(Type = PathType.File, AllowEmpty = true)] + [Path(Type = PathType.Directory, AllowEmpty = true)] [DefaultValue("")] public string LogPath { get; set; } = string.Empty; diff --git a/src/client/LibplanetConsole.Client.Executable/EntryCommands/InitializeCommand.cs b/src/client/LibplanetConsole.Client.Executable/EntryCommands/InitializeCommand.cs index 541ae4d8..c34affca 100644 --- a/src/client/LibplanetConsole.Client.Executable/EntryCommands/InitializeCommand.cs +++ b/src/client/LibplanetConsole.Client.Executable/EntryCommands/InitializeCommand.cs @@ -33,9 +33,8 @@ public InitializeCommand() public string EndPoint { get; set; } = string.Empty; [CommandProperty] - [CommandSummary("The file path to store the application logs." + - "If omitted, the 'app.log' file is used.")] - [Path(Type = PathType.File, ExistsType = PathExistsType.NotExistOrEmpty, AllowEmpty = true)] + [CommandSummary("Indicates the file path to save logs.")] + [Path(Type = PathType.Directory, AllowEmpty = true)] public string LogPath { get; set; } = string.Empty; [CommandPropertySwitch("quiet", 'q')] diff --git a/src/client/LibplanetConsole.Client.Executable/SystemTerminal.cs b/src/client/LibplanetConsole.Client.Executable/SystemTerminal.cs index 190e7bbb..4ce6c41e 100644 --- a/src/client/LibplanetConsole.Client.Executable/SystemTerminal.cs +++ b/src/client/LibplanetConsole.Client.Executable/SystemTerminal.cs @@ -1,22 +1,67 @@ +using System.Text; using JSSoft.Commands.Extensions; using JSSoft.Terminals; +using LibplanetConsole.Blockchain; +using LibplanetConsole.Common.Extensions; namespace LibplanetConsole.Client.Executable; internal sealed class SystemTerminal : SystemTerminalBase { + private const string PromptText = "libplanet-client $ "; + private readonly SynchronizationContext _synchronizationContext; private readonly CommandContext _commandContext; + private readonly IBlockChain _blockChain; + private BlockInfo _tip; public SystemTerminal( - IHostApplicationLifetime applicationLifetime, CommandContext commandContext) + IHostApplicationLifetime applicationLifetime, + CommandContext commandContext, + IBlockChain blockChain, + SynchronizationContext synchronizationContext) { + _synchronizationContext = synchronizationContext; _commandContext = commandContext; _commandContext.Owner = applicationLifetime; - Prompt = "libplanet-client $ "; + _blockChain = blockChain; + _blockChain.BlockAppended += BlockChain_BlockAppended; + _blockChain.Started += BlockChain_Started; + _blockChain.Stopped += BlockChain_Stopped; + UpdatePrompt(_blockChain.Tip); applicationLifetime.ApplicationStopping.Register(() => Prompt = "\u001b0"); } - protected override string FormatPrompt(string prompt) => prompt; + protected override void OnDispose() + { + _blockChain.Started -= BlockChain_Started; + _blockChain.Stopped -= BlockChain_Stopped; + _blockChain.BlockAppended -= BlockChain_BlockAppended; + base.OnDispose(); + } + + protected override string FormatPrompt(string prompt) + { + var tip = _tip; + if (_tip.Height == -1) + { + return prompt; + } + else + { + var tsb = new TerminalStringBuilder(); + tsb.AppendEnd(); + tsb.Append($"#{tip.Height} "); + tsb.Foreground = TerminalColorType.BrightGreen; + tsb.Append($"{tip.Hash.ToShortString()} "); + tsb.ResetOptions(); + tsb.Append($"by "); + tsb.Foreground = TerminalColorType.BrightGreen; + tsb.Append($"{tip.Miner.ToShortString()}"); + tsb.ResetOptions(); + tsb.AppendEnd(); + return $"[{tsb}] {PromptText}"; + } + } protected override string[] GetCompletion(string[] items, string find) => _commandContext.GetCompletion(items, find); @@ -29,4 +74,32 @@ protected override void OnInitialize(TextWriter @out, TextWriter error) _commandContext.Out = @out; _commandContext.Error = error; } + + private void BlockChain_BlockAppended(object? sender, BlockEventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(e.BlockInfo), null); + + private void BlockChain_Started(object? sender, EventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(_blockChain.Tip), null); + + private void BlockChain_Stopped(object? sender, EventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(_blockChain.Tip), null); + + private void UpdatePrompt(BlockInfo tip) + { + if (tip.Height == -1) + { + Prompt = PromptText; + } + else + { + var sb = new StringBuilder(); + sb.Append($"#{tip.Height} "); + sb.Append($"{tip.Hash.ToShortString()} "); + sb.Append($"by "); + sb.Append($"{tip.Miner.ToShortString()}"); + Prompt = $"[{sb}] {PromptText}"; + } + + _tip = tip; + } } diff --git a/src/client/LibplanetConsole.Client.Executable/Tracers/BlockChainEventTracer.cs b/src/client/LibplanetConsole.Client.Executable/Tracers/BlockChainEventTracer.cs index 377a9885..4bf1ef64 100644 --- a/src/client/LibplanetConsole.Client.Executable/Tracers/BlockChainEventTracer.cs +++ b/src/client/LibplanetConsole.Client.Executable/Tracers/BlockChainEventTracer.cs @@ -33,9 +33,10 @@ private void Client_BlockAppended(object? sender, BlockEventArgs e) var blockInfo = e.BlockInfo; var hash = blockInfo.Hash; var miner = blockInfo.Miner; - var message = $"Block #{blockInfo.Height} '{hash.ToShortString()}' " + - $"Appended by '{miner.ToShortString()}'"; - Console.Out.WriteColoredLine(message, TerminalColorType.BrightGreen); - _logger.LogInformation(message); + _logger.LogInformation( + "Block #{TipHeight} '{TipHash}' Appended by '{TipMiner}'", + blockInfo.Height, + hash.ToShortString(), + miner.ToShortString()); } } diff --git a/src/client/LibplanetConsole.Client/Client.BlockChain.cs b/src/client/LibplanetConsole.Client/Client.BlockChain.cs index a3740e12..d13404d4 100644 --- a/src/client/LibplanetConsole.Client/Client.BlockChain.cs +++ b/src/client/LibplanetConsole.Client/Client.BlockChain.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using Grpc.Core; +using LibplanetConsole.Blockchain; using LibplanetConsole.Blockchain.Grpc; using LibplanetConsole.Node; @@ -9,6 +10,10 @@ internal sealed partial class Client : IBlockChain { private static readonly Codec _codec = new(); + public event EventHandler? BlockAppended; + + public BlockInfo Tip => Info.Tip; + public async Task SendTransactionAsync( IAction[] actions, CancellationToken cancellationToken) { diff --git a/src/client/LibplanetConsole.Client/Client.cs b/src/client/LibplanetConsole.Client/Client.cs index e1fe082f..e55bf3c6 100644 --- a/src/client/LibplanetConsole.Client/Client.cs +++ b/src/client/LibplanetConsole.Client/Client.cs @@ -1,3 +1,4 @@ +using Grpc.Core; using Grpc.Net.Client; using LibplanetConsole.Blockchain; using LibplanetConsole.Blockchain.Grpc; @@ -31,8 +32,6 @@ public Client(ILogger logger, ApplicationOptions options) _logger.LogDebug("Client is created: {Address}", Address); } - public event EventHandler? BlockAppended; - public event EventHandler? Started; public event EventHandler? Stopped; @@ -85,11 +84,11 @@ public async Task StartAsync(CancellationToken cancellationToken) var blockChainService = new BlockChainService(channel); nodeService.Started += (sender, e) => InvokeNodeStartedEvent(e); nodeService.Stopped += (sender, e) => InvokeNodeStoppedEvent(); - blockChainService.BlockAppended += (sender, e) => InvokeBlockAppendedEvent(e); + blockChainService.BlockAppended += BlockChainService_BlockAppended; try { - await nodeService.StartAsync(cancellationToken); - await blockChainService.StartAsync(cancellationToken); + await nodeService.InitializeAsync(cancellationToken); + await blockChainService.InitializeAsync(cancellationToken); } catch { @@ -102,7 +101,11 @@ public async Task StartAsync(CancellationToken cancellationToken) _channel = channel; _nodeService = nodeService; _blockChainService = blockChainService; - _info = _info with { NodeAddress = NodeInfo.Address }; + _info = _info with + { + NodeAddress = NodeInfo.Address, + Tip = nodeService.Info.Tip, + }; IsRunning = true; _logger.LogDebug( "Client is started: {Address} -> {NodeAddress}", Address, NodeInfo.Address); @@ -124,13 +127,13 @@ public async Task StopAsync(CancellationToken cancellationToken) if (_nodeService is not null) { - await _nodeService.StopAsync(cancellationToken); + await _nodeService.ReleaseAsync(cancellationToken); _nodeService = null; } if (_blockChainService is not null) { - await _blockChainService.StopAsync(cancellationToken); + await _blockChainService.ReleaseAsync(cancellationToken); _blockChainService = null; } @@ -140,7 +143,7 @@ public async Task StopAsync(CancellationToken cancellationToken) _blockChainService = null; _nodeService = null; IsRunning = false; - _info = _info with { NodeAddress = default }; + _info = ClientInfo.Empty; _logger.LogDebug("Client is stopped: {Address}", Address); Stopped?.Invoke(this, EventArgs.Empty); } @@ -186,4 +189,13 @@ private void NodeService_Disconnected(object? sender, EventArgs e) Stopped?.Invoke(this, EventArgs.Empty); } } + + private void BlockChainService_BlockAppended(object? sender, BlockEventArgs e) + { + _info = _info with + { + Tip = e.BlockInfo, + }; + BlockAppended?.Invoke(this, e); + } } diff --git a/src/client/LibplanetConsole.Client/Commands/TxCommand.cs b/src/client/LibplanetConsole.Client/Commands/TxCommand.cs index 841a2800..6f8f34fd 100644 --- a/src/client/LibplanetConsole.Client/Commands/TxCommand.cs +++ b/src/client/LibplanetConsole.Client/Commands/TxCommand.cs @@ -1,4 +1,5 @@ using JSSoft.Commands; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common.Actions; using LibplanetConsole.Common.Extensions; diff --git a/src/client/LibplanetConsole.Client/Grpc/BlockChainGrpcServiceV1.cs b/src/client/LibplanetConsole.Client/Grpc/BlockChainGrpcServiceV1.cs index dcd345ff..faf417c7 100644 --- a/src/client/LibplanetConsole.Client/Grpc/BlockChainGrpcServiceV1.cs +++ b/src/client/LibplanetConsole.Client/Grpc/BlockChainGrpcServiceV1.cs @@ -33,7 +33,7 @@ public async override Task GetNextNonce( public override async Task GetTipHash( GetTipHashRequest request, ServerCallContext context) { - var blockHash = await blockChain.GetTipHashAsync(context.CancellationToken); + var blockHash = await Task.FromResult(blockChain.Tip.Hash); return new GetTipHashResponse { BlockHash = blockHash.ToString() }; } diff --git a/src/client/LibplanetConsole.Client/IClient.cs b/src/client/LibplanetConsole.Client/IClient.cs index 0436d3b9..478e740a 100644 --- a/src/client/LibplanetConsole.Client/IClient.cs +++ b/src/client/LibplanetConsole.Client/IClient.cs @@ -24,6 +24,4 @@ public interface IClient : IVerifier Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); - - Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); } diff --git a/src/client/LibplanetConsole.Client/ServiceCollectionExtensions.cs b/src/client/LibplanetConsole.Client/ServiceCollectionExtensions.cs index fe2f04f4..043edd80 100644 --- a/src/client/LibplanetConsole.Client/ServiceCollectionExtensions.cs +++ b/src/client/LibplanetConsole.Client/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using JSSoft.Commands; +using LibplanetConsole.Blockchain; using LibplanetConsole.Client.Commands; using LibplanetConsole.Common; using Microsoft.Extensions.DependencyInjection; diff --git a/src/console/LibplanetConsole.Console.Evidence/Commands/EvidenceCommand.cs b/src/console/LibplanetConsole.Console.Evidence/Commands/EvidenceCommand.cs index cde30c51..c8bc52ed 100644 --- a/src/console/LibplanetConsole.Console.Evidence/Commands/EvidenceCommand.cs +++ b/src/console/LibplanetConsole.Console.Evidence/Commands/EvidenceCommand.cs @@ -15,7 +15,7 @@ public async Task NewAsync( string nodeAddress = "", CancellationToken cancellationToken = default) { var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); - var evidence = node.GetRequiredService(); + var evidence = node.GetRequiredKeyedService(INode.Key); var evidenceInfo = await evidence.AddEvidenceAsync(cancellationToken); await Out.WriteLineAsJsonAsync(evidenceInfo); } @@ -25,7 +25,7 @@ public async Task RaiseAsync( CancellationToken cancellationToken = default) { var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); - var evidence = node.GetRequiredService(); + var evidence = node.GetRequiredKeyedService(INode.Key); await evidence.ViolateAsync(cancellationToken); } @@ -33,7 +33,7 @@ public async Task RaiseAsync( public async Task ListAsync(long height = -1, CancellationToken cancellationToken = default) { var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); - var evidence = node.GetRequiredService(); + var evidence = node.GetRequiredKeyedService(INode.Key); var evidenceInfos = await evidence.GetEvidenceAsync(height, cancellationToken); await Out.WriteLineAsJsonAsync(evidenceInfos); } @@ -44,7 +44,7 @@ public async Task UnjailAsync( CancellationToken cancellationToken = default) { var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); - var evidence = node.GetService(); + var evidence = node.GetRequiredKeyedService(INode.Key); await evidence.UnjailAsync(cancellationToken); } #endif // LIBPLANET_DPOS diff --git a/src/console/LibplanetConsole.Console.Evidence/Evidence.cs b/src/console/LibplanetConsole.Console.Evidence/Evidence.cs index 7c861e7d..66c548a7 100644 --- a/src/console/LibplanetConsole.Console.Evidence/Evidence.cs +++ b/src/console/LibplanetConsole.Console.Evidence/Evidence.cs @@ -1,37 +1,68 @@ +using Grpc.Core; +using Grpc.Net.Client; using LibplanetConsole.Evidence; +using LibplanetConsole.Evidence.Grpc; +using Microsoft.Extensions.DependencyInjection; namespace LibplanetConsole.Console.Evidence; -internal sealed class Evidence(INode node) - : INodeContent, IEvidence - // , INodeContentService +internal sealed class Evidence([FromKeyedServices(INode.Key)] INode node) + : NodeContentBase("evidence"), IEvidence { - // private readonly RemoteService _evidenceService = new(); + private GrpcChannel? _channel; + private EvidenceGrpcService.EvidenceGrpcServiceClient? _client; - INode INodeContent.Node => node; + public async Task AddEvidenceAsync(CancellationToken cancellationToken) + { + if (_client is null) + { + throw new InvalidOperationException("The channel is not initialized."); + } - string INodeContent.Name => "evidence"; + var request = new AddEvidenceRequest(); + var callOptions = new CallOptions(cancellationToken: cancellationToken); + var response = await _client.AddEvidenceAsync(request, callOptions); + return response.EvidenceInformation; + } - // IRemoteService INodeContentService.RemoteService => _evidenceService; + public async Task GetEvidenceAsync( + long height, CancellationToken cancellationToken) + { + if (_client is null) + { + throw new InvalidOperationException("The channel is not initialized."); + } - // private IEvidenceService Service => _evidenceService.Service; + var request = new GetEvidenceRequest { Height = height }; + var callOptions = new CallOptions(cancellationToken: cancellationToken); + var response = await _client.GetEvidenceAsync(request, callOptions); + return [.. response.EvidenceInformations.Select(item => (EvidenceInfo)item)]; + } - public Task AddEvidenceAsync(CancellationToken cancellationToken) + public async Task ViolateAsync(CancellationToken cancellationToken) { - // return Service.AddEvidenceAsync(cancellationToken); - throw new NotImplementedException(); + if (_client is null) + { + throw new InvalidOperationException("The channel is not initialized."); + } + + var request = new ViolateRequest(); + var callOptions = new CallOptions(cancellationToken: cancellationToken); + await _client.ViolateAsync(request, callOptions); } - public Task GetEvidenceAsync(long height, CancellationToken cancellationToken) + protected override async Task OnStartAsync(CancellationToken cancellationToken) { - // return Service.GetEvidenceAsync(height, cancellationToken); - throw new NotImplementedException(); + _channel = EvidenceChannel.CreateChannel(node.EndPoint); + _client = new EvidenceGrpcService.EvidenceGrpcServiceClient(_channel); + await Task.CompletedTask; } - public Task ViolateAsync(CancellationToken cancellationToken) + protected override async Task OnStopAsync(CancellationToken cancellationToken) { - // return Service.ViolateAsync(cancellationToken); - throw new NotImplementedException(); + _channel?.Dispose(); + _channel = null; + await Task.CompletedTask; } #if LIBPLANET_DPOS diff --git a/src/console/LibplanetConsole.Console.Evidence/Grpc/EvidenceChannel.cs b/src/console/LibplanetConsole.Console.Evidence/Grpc/EvidenceChannel.cs new file mode 100644 index 00000000..3ccfdf7c --- /dev/null +++ b/src/console/LibplanetConsole.Console.Evidence/Grpc/EvidenceChannel.cs @@ -0,0 +1,24 @@ +using Grpc.Core; +using Grpc.Net.Client; +using Grpc.Net.Client.Configuration; +using LibplanetConsole.Common; + +namespace LibplanetConsole.Evidence.Grpc; + +internal static class EvidenceChannel +{ + private static readonly GrpcChannelOptions _channelOptions = new() + { + ThrowOperationCanceledOnCancellation = true, + MaxRetryAttempts = 10, + ServiceConfig = new() + { + }, + }; + + public static GrpcChannel CreateChannel(EndPoint endPoint) + { + var address = $"http://{EndPointUtility.ToString(endPoint)}"; + return GrpcChannel.ForAddress(address, _channelOptions); + } +} diff --git a/src/console/LibplanetConsole.Console.Evidence/ServiceCollectionExtensions.cs b/src/console/LibplanetConsole.Console.Evidence/ServiceCollectionExtensions.cs index 24886676..365377d7 100644 --- a/src/console/LibplanetConsole.Console.Evidence/ServiceCollectionExtensions.cs +++ b/src/console/LibplanetConsole.Console.Evidence/ServiceCollectionExtensions.cs @@ -8,8 +8,11 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddEvidence(this IServiceCollection @this) { - @this.AddScoped() - .AddScoped(s => s.GetRequiredService()); + @this.AddKeyedScoped(INode.Key) + .AddKeyedScoped( + INode.Key, (s, k) => s.GetRequiredKeyedService(k)) + .AddKeyedScoped( + INode.Key, (s, k) => s.GetRequiredKeyedService(k)); @this.AddSingleton(); diff --git a/src/console/LibplanetConsole.Console.Executable/Application.cs b/src/console/LibplanetConsole.Console.Executable/Application.cs index 8c2baac5..ed5270c0 100644 --- a/src/console/LibplanetConsole.Console.Executable/Application.cs +++ b/src/console/LibplanetConsole.Console.Executable/Application.cs @@ -34,7 +34,11 @@ public Application(ApplicationOptions options, object[] instances) options.ListenLocalhost(port, o => o.Protocols = HttpProtocols.Http2); }); - services.AddLogging(options.LogPath, "console.log", _filters); + if (options.LogPath != string.Empty) + { + services.AddLogging(options.LogPath, "console.log", _filters); + } + services.AddSingleton(); services.AddSingleton(); @@ -43,8 +47,8 @@ public Application(ApplicationOptions options, object[] instances) services.AddSingleton() .AddSingleton(s => s.GetRequiredService()); - services.AddEvidence(); services.AddConsole(options); + services.AddEvidence(); services.AddGrpc(); services.AddGrpcReflection(); diff --git a/src/console/LibplanetConsole.Console.Executable/SystemTerminal.cs b/src/console/LibplanetConsole.Console.Executable/SystemTerminal.cs index 98f2e363..66a38cad 100644 --- a/src/console/LibplanetConsole.Console.Executable/SystemTerminal.cs +++ b/src/console/LibplanetConsole.Console.Executable/SystemTerminal.cs @@ -1,22 +1,67 @@ +using System.Text; using JSSoft.Commands.Extensions; using JSSoft.Terminals; +using LibplanetConsole.Blockchain; +using LibplanetConsole.Common.Extensions; namespace LibplanetConsole.Console.Executable; internal sealed class SystemTerminal : SystemTerminalBase { + private const string PromptText = "libplanet-console $ "; + private readonly SynchronizationContext _synchronizationContext; private readonly CommandContext _commandContext; + private readonly IBlockChain _blockChain; + private BlockInfo _tip; public SystemTerminal( - IHostApplicationLifetime applicationLifetime, CommandContext commandContext) + IHostApplicationLifetime applicationLifetime, + CommandContext commandContext, + IBlockChain blockChain, + SynchronizationContext synchronizationContext) { + _synchronizationContext = synchronizationContext; _commandContext = commandContext; _commandContext.Owner = applicationLifetime; - Prompt = "libplanet-console $ "; + _blockChain = blockChain; + _blockChain.BlockAppended += BlockChain_BlockAppended; + _blockChain.Started += BlockChain_Started; + _blockChain.Stopped += BlockChain_Stopped; + UpdatePrompt(_blockChain.Tip); applicationLifetime.ApplicationStopping.Register(() => Prompt = "\u001b0"); } - protected override string FormatPrompt(string prompt) => prompt; + protected override void OnDispose() + { + _blockChain.Started -= BlockChain_Started; + _blockChain.Stopped -= BlockChain_Stopped; + _blockChain.BlockAppended -= BlockChain_BlockAppended; + base.OnDispose(); + } + + protected override string FormatPrompt(string prompt) + { + var tip = _tip; + if (_tip.Height == -1) + { + return prompt; + } + else + { + var tsb = new TerminalStringBuilder(); + tsb.AppendEnd(); + tsb.Append($"#{tip.Height} "); + tsb.Foreground = TerminalColorType.BrightGreen; + tsb.Append($"{tip.Hash.ToShortString()} "); + tsb.ResetOptions(); + tsb.Append($"by "); + tsb.Foreground = TerminalColorType.BrightGreen; + tsb.Append($"{tip.Miner.ToShortString()}"); + tsb.ResetOptions(); + tsb.AppendEnd(); + return $"[{tsb}] {PromptText}"; + } + } protected override string[] GetCompletion(string[] items, string find) => _commandContext.GetCompletion(items, find); @@ -29,4 +74,32 @@ protected override void OnInitialize(TextWriter @out, TextWriter error) _commandContext.Out = @out; _commandContext.Error = error; } + + private void BlockChain_BlockAppended(object? sender, BlockEventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(e.BlockInfo), null); + + private void BlockChain_Started(object? sender, EventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(_blockChain.Tip), null); + + private void BlockChain_Stopped(object? sender, EventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(_blockChain.Tip), null); + + private void UpdatePrompt(BlockInfo tip) + { + if (tip.Height == -1) + { + Prompt = PromptText; + } + else + { + var sb = new StringBuilder(); + sb.Append($"#{tip.Height} "); + sb.Append($"{tip.Hash.ToShortString()} "); + sb.Append($"by "); + sb.Append($"{tip.Miner.ToShortString()}"); + Prompt = $"[{sb}] {PromptText}"; + } + + _tip = tip; + } } diff --git a/src/console/LibplanetConsole.Console.Executable/Tracers/NodeCollectionEventTracer.cs b/src/console/LibplanetConsole.Console.Executable/Tracers/NodeCollectionEventTracer.cs index b1cfd005..f3390aa9 100644 --- a/src/console/LibplanetConsole.Console.Executable/Tracers/NodeCollectionEventTracer.cs +++ b/src/console/LibplanetConsole.Console.Executable/Tracers/NodeCollectionEventTracer.cs @@ -8,11 +8,14 @@ namespace LibplanetConsole.Console.Executable.Tracers; internal sealed class NodeCollectionEventTracer : IHostedService, IDisposable { private readonly INodeCollection _nodes; + private readonly ILogger _logger; private INode? _current; - public NodeCollectionEventTracer(INodeCollection nodes) + public NodeCollectionEventTracer( + INodeCollection nodes, ILogger logger) { _nodes = nodes; + _logger = logger; UpdateCurrent(_nodes.Current); foreach (var node in _nodes) { @@ -42,14 +45,14 @@ void IDisposable.Dispose() private void UpdateCurrent(INode? node) { - if (_current?.GetService(typeof(IBlockChain)) is IBlockChain blockChain1) + if (_current?.GetKeyedService(INode.Key) is IBlockChain blockChain1) { blockChain1.BlockAppended -= BlockChain_BlockAppended; } _current = node; - if (_current?.GetService(typeof(IBlockChain)) is IBlockChain blockChain2) + if (_current?.GetKeyedService(INode.Key) is IBlockChain blockChain2) { blockChain2.BlockAppended += BlockChain_BlockAppended; } @@ -105,9 +108,11 @@ private void BlockChain_BlockAppended(object? sender, BlockEventArgs e) var blockInfo = e.BlockInfo; var hash = blockInfo.Hash; var miner = blockInfo.Miner; - var message = $"Block #{blockInfo.Height} '{hash.ToShortString()}' " + - $"Appended by '{miner.ToShortString()}'"; - System.Console.Out.WriteColoredLine(message, TerminalColorType.BrightBlue); + _logger.LogInformation( + "Block #{TipHeight} '{TipHash}' Appended by '{TipMiner}'", + blockInfo.Height, + hash.ToShortString(), + miner.ToShortString()); } private void Node_Attached(object? sender, EventArgs e) diff --git a/src/console/LibplanetConsole.Console/BlockChain.cs b/src/console/LibplanetConsole.Console/BlockChain.cs new file mode 100644 index 00000000..8d95042a --- /dev/null +++ b/src/console/LibplanetConsole.Console/BlockChain.cs @@ -0,0 +1,183 @@ +using System.Diagnostics; +using System.Security.Cryptography; +using LibplanetConsole.Blockchain; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace LibplanetConsole.Console; + +internal sealed class BlockChain : IBlockChain, IDisposable +{ + private readonly INodeCollection _nodes; + private readonly ILogger _logger; + private IBlockChain? _blockChain; + private bool _isDisposed; + + public BlockChain(NodeCollection nodes, ILogger logger) + { + _nodes = nodes; + _logger = logger; + UpdateCurrent(_nodes.Current); + _nodes.CurrentChanged += Nodes_CurrentChanged; + } + + public event EventHandler? BlockAppended; + + public event EventHandler? Started; + + public event EventHandler? Stopped; + + public BlockInfo Tip { get; private set; } = BlockInfo.Empty; + + public bool IsRunning { get; private set; } + + void IDisposable.Dispose() + { + if (_isDisposed is false) + { + _nodes.CurrentChanged -= Nodes_CurrentChanged; + UpdateCurrent(null); + + _isDisposed = true; + } + } + + Task IBlockChain.SendTransactionAsync( + IAction[] actions, CancellationToken cancellationToken) + { + if (IsRunning is false || _blockChain is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _blockChain.SendTransactionAsync(actions, cancellationToken); + } + + Task IBlockChain.GetNextNonceAsync( + Address address, CancellationToken cancellationToken) + { + if (IsRunning is false || _blockChain is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _blockChain.GetNextNonceAsync(address, cancellationToken); + } + + Task IBlockChain.GetStateAsync( + BlockHash? blockHash, + Address accountAddress, + Address address, + CancellationToken cancellationToken) + { + if (IsRunning is false || _blockChain is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _blockChain.GetStateAsync(blockHash, accountAddress, address, cancellationToken); + } + + Task IBlockChain.GetStateByStateRootHashAsync( + HashDigest stateRootHash, + Address accountAddress, + Address address, + CancellationToken cancellationToken) + { + if (IsRunning is false || _blockChain is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _blockChain.GetStateByStateRootHashAsync( + stateRootHash, accountAddress, address, cancellationToken); + } + + Task IBlockChain.GetBlockHashAsync( + long height, CancellationToken cancellationToken) + { + if (IsRunning is false || _blockChain is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _blockChain.GetBlockHashAsync(height, cancellationToken); + } + + Task IBlockChain.GetActionAsync( + TxId txId, int actionIndex, CancellationToken cancellationToken) + { + if (IsRunning is false || _blockChain is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _blockChain.GetActionAsync(txId, actionIndex, cancellationToken); + } + + private void UpdateCurrent(INode? node) + { + if (_blockChain is not null) + { + _blockChain.Started -= BlockChain_Started; + _blockChain.Stopped -= BlockChain_Stopped; + _blockChain.BlockAppended -= BlockChain_BlockAppended; + if (_blockChain.IsRunning is false) + { + Tip = BlockInfo.Empty; + IsRunning = false; + _logger.LogDebug("BlockChain is stopped."); + Stopped?.Invoke(this, EventArgs.Empty); + } + } + + _blockChain = node?.GetKeyedService(INode.Key); + + if (_blockChain is not null) + { + if (_blockChain.IsRunning is true) + { + Tip = _blockChain.Tip; + IsRunning = true; + _logger.LogDebug("BlockChain is started."); + Started?.Invoke(this, EventArgs.Empty); + } + + _blockChain.Started += BlockChain_Started; + _blockChain.Stopped += BlockChain_Stopped; + _blockChain.BlockAppended += BlockChain_BlockAppended; + } + } + + private void Nodes_CurrentChanged(object? sender, EventArgs e) + => UpdateCurrent(_nodes.Current); + + private void BlockChain_BlockAppended(object? sender, BlockEventArgs e) + { + Tip = e.BlockInfo; + BlockAppended?.Invoke(sender, e); + } + + private void BlockChain_Started(object? sender, EventArgs e) + { + if (sender is IBlockChain blockChain && blockChain == _blockChain) + { + Tip = _blockChain.Tip; + IsRunning = true; + _logger.LogDebug("BlockChain is started."); + Started?.Invoke(this, EventArgs.Empty); + } + else + { + throw new UnreachableException("The sender is not an instance of IBlockChain."); + } + } + + private void BlockChain_Stopped(object? sender, EventArgs e) + { + Tip = BlockInfo.Empty; + IsRunning = false; + _logger.LogDebug("BlockChain is stopped."); + Stopped?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/console/LibplanetConsole.Console/Client.BlockChain.cs b/src/console/LibplanetConsole.Console/Client.BlockChain.cs index 99c50032..e2583363 100644 --- a/src/console/LibplanetConsole.Console/Client.BlockChain.cs +++ b/src/console/LibplanetConsole.Console/Client.BlockChain.cs @@ -11,6 +11,8 @@ internal sealed partial class Client public event EventHandler? BlockAppended; + public BlockInfo Tip => Info.Tip; + public async Task GetNextNonceAsync(Address address, CancellationToken cancellationToken) { if (_blockChainService is null) diff --git a/src/console/LibplanetConsole.Console/Client.cs b/src/console/LibplanetConsole.Console/Client.cs index 3ce7533b..45ccd699 100644 --- a/src/console/LibplanetConsole.Console/Client.cs +++ b/src/console/LibplanetConsole.Console/Client.cs @@ -25,6 +25,7 @@ internal sealed partial class Client : IClient, IBlockChain private bool _isDisposed; private ClientProcess? _process; private Task _processTask = Task.CompletedTask; + private IClientContent[]? _contents; public Client(IServiceProvider serviceProvider, ClientOptions clientOptions) { @@ -58,6 +59,12 @@ public Client(IServiceProvider serviceProvider, ClientOptions clientOptions) public ClientInfo Info => _clientInfo; + public IClientContent[] Contents + { + get => _contents ?? throw new InvalidOperationException("Contents is not initialized."); + set => _contents = value; + } + public object? GetService(Type serviceType) => _serviceProvider.GetService(serviceType); public override string ToString() => $"{Address.ToShortString()}: {EndPoint}"; @@ -97,7 +104,7 @@ public async Task AttachAsync(CancellationToken cancellationToken) try { await clientService.StartAsync(cancellationToken); - await blockChainService.StartAsync(cancellationToken); + await blockChainService.InitializeAsync(cancellationToken); } catch { @@ -172,6 +179,8 @@ public async Task StartAsync(INode node, CancellationToken cancellationToken) _clientInfo = response.ClientInfo; IsRunning = true; _logger.LogDebug("Client is started: {Address}", Address); + await Task.WhenAll(Contents.Select(item => item.StartAsync(cancellationToken))); + _logger.LogDebug("Client Contents are started: {Address}", Address); Started?.Invoke(this, EventArgs.Empty); } @@ -188,6 +197,9 @@ public async Task StopAsync(CancellationToken cancellationToken) throw new InvalidOperationException("Client is not attached."); } + await Task.WhenAll(Contents.Select(item => item.StopAsync(cancellationToken))); + _logger.LogDebug("Client Contents are stopped: {Address}", Address); + var request = new StopRequest(); var callOptions = new CallOptions(cancellationToken: cancellationToken); await _clientService.StopAsync(request, callOptions); @@ -276,7 +288,7 @@ private void ClientService_Stopped(object? sender, EventArgs e) private void BlockChainService_BlockAppended(object? sender, BlockEventArgs e) { - _clientInfo = _clientInfo with { TipHash = e.BlockInfo.Hash }; + _clientInfo = _clientInfo with { Tip = e.BlockInfo }; BlockAppended?.Invoke(this, e); } diff --git a/src/console/LibplanetConsole.Console/ClientContentBase.cs b/src/console/LibplanetConsole.Console/ClientContentBase.cs index 07bdd39d..3e8f7425 100644 --- a/src/console/LibplanetConsole.Console/ClientContentBase.cs +++ b/src/console/LibplanetConsole.Console/ClientContentBase.cs @@ -1,63 +1,38 @@ namespace LibplanetConsole.Console; -public abstract class ClientContentBase : IClientContent, IDisposable +public abstract class ClientContentBase(string name) : IClientContent, IDisposable { - private readonly string _name; - private bool _isDisposed; - - protected ClientContentBase(IClient client, string name) - { - _name = name; - Client = client; - Client.Attached += Client_Attached; - Client.Detached += Client_Detached; - Client.Started += Client_Started; - Client.Stopped += Client_Stopped; - } - - protected ClientContentBase(IClient client) - : this(client, string.Empty) - { - } - - public IClient Client { get; } + private readonly string _name = name; + private bool disposedValue; public string Name => _name != string.Empty ? _name : GetType().Name; void IDisposable.Dispose() { - if (_isDisposed is false) - { - Client.Attached -= Client_Attached; - Client.Detached -= Client_Detached; - Client.Started -= Client_Started; - Client.Stopped -= Client_Stopped; - _isDisposed = true; - GC.SuppressFinalize(this); - } + OnDispose(disposing: true); + GC.SuppressFinalize(this); } - protected virtual void OnClientAttached() - { - } + Task IClientContent.StartAsync(CancellationToken cancellationToken) + => OnStartAsync(cancellationToken); - protected virtual void OnClientDetached() - { - } - - protected virtual void OnClientStarted() - { - } + Task IClientContent.StopAsync(CancellationToken cancellationToken) + => OnStopAsync(cancellationToken); - protected virtual void OnClientStopped() - { - } + protected abstract Task OnStartAsync(CancellationToken cancellationToken); - private void Client_Attached(object? sender, EventArgs e) => OnClientAttached(); + protected abstract Task OnStopAsync(CancellationToken cancellationToken); - private void Client_Detached(object? sender, EventArgs e) => OnClientDetached(); - - private void Client_Started(object? sender, EventArgs e) => OnClientStarted(); + protected virtual void OnDispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // do nothing + } - private void Client_Stopped(object? sender, EventArgs e) => OnClientStopped(); + disposedValue = true; + } + } } diff --git a/src/console/LibplanetConsole.Console/ClientFactory.cs b/src/console/LibplanetConsole.Console/ClientFactory.cs index 79fe6c7e..df93c438 100644 --- a/src/console/LibplanetConsole.Console/ClientFactory.cs +++ b/src/console/LibplanetConsole.Console/ClientFactory.cs @@ -9,7 +9,7 @@ internal static class ClientFactory private static readonly ConcurrentDictionary _valueByKey = []; private static readonly ConcurrentDictionary _scopeByClient = []; - public static Client Create(IServiceProvider serviceProvider) + public static Client Create(IServiceProvider serviceProvider, object? key) { if (_valueByKey.Remove(serviceProvider, out var descriptor) is true) { @@ -42,8 +42,10 @@ public static Client CreateNew(IServiceProvider serviceProvider, ClientOptions c }, (k, v) => v); - var client = serviceScope.ServiceProvider.GetRequiredService(); - serviceScope.ServiceProvider.GetServices(); + var scopedServiceProvider = serviceScope.ServiceProvider; + var key = IClient.Key; + var client = scopedServiceProvider.GetRequiredKeyedService(key); + client.Contents = [.. scopedServiceProvider.GetKeyedServices(key)]; return client; } diff --git a/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs b/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs index 19685fba..e57f3796 100644 --- a/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs +++ b/src/console/LibplanetConsole.Console/Commands/ClientCommand.cs @@ -1,5 +1,6 @@ using JSSoft.Commands; using JSSoft.Terminals; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common; using LibplanetConsole.Common.Actions; using LibplanetConsole.Common.Extensions; diff --git a/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs b/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs index e676883d..c7b3ee1a 100644 --- a/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs +++ b/src/console/LibplanetConsole.Console/Commands/NodeCommand.cs @@ -1,5 +1,6 @@ using JSSoft.Commands; using JSSoft.Terminals; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common; using LibplanetConsole.Common.Actions; using LibplanetConsole.Common.Extensions; diff --git a/src/console/LibplanetConsole.Console/Commands/TxCommand.cs b/src/console/LibplanetConsole.Console/Commands/TxCommand.cs index 320fa5c5..1f3d7a3e 100644 --- a/src/console/LibplanetConsole.Console/Commands/TxCommand.cs +++ b/src/console/LibplanetConsole.Console/Commands/TxCommand.cs @@ -1,4 +1,5 @@ using JSSoft.Commands; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common.Actions; using LibplanetConsole.Common.Extensions; using Microsoft.Extensions.DependencyInjection; diff --git a/src/console/LibplanetConsole.Console/IBlockChain.cs b/src/console/LibplanetConsole.Console/IBlockChain.cs deleted file mode 100644 index f6b6fe69..00000000 --- a/src/console/LibplanetConsole.Console/IBlockChain.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Security.Cryptography; -using LibplanetConsole.Blockchain; - -namespace LibplanetConsole.Console; - -public interface IBlockChain -{ - event EventHandler? BlockAppended; - - Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); - - Task GetNextNonceAsync(Address address, CancellationToken cancellationToken); - - Task GetTipHashAsync(CancellationToken cancellationToken); - - Task GetStateAsync( - BlockHash? blockHash, - Address accountAddress, - Address address, - CancellationToken cancellationToken); - - Task GetStateByStateRootHashAsync( - HashDigest stateRootHash, - Address accountAddress, - Address address, - CancellationToken cancellationToken); - - Task GetBlockHashAsync(long height, CancellationToken cancellationToken); - - Task GetActionAsync(TxId txId, int actionIndex, CancellationToken cancellationToken) - where T : IAction; -} diff --git a/src/console/LibplanetConsole.Console/IClient.cs b/src/console/LibplanetConsole.Console/IClient.cs index 0267522b..6d97ed36 100644 --- a/src/console/LibplanetConsole.Console/IClient.cs +++ b/src/console/LibplanetConsole.Console/IClient.cs @@ -5,6 +5,8 @@ namespace LibplanetConsole.Console; public interface IClient : IAddressable, IAsyncDisposable, IServiceProvider, ISigner { + const string Key = nameof(IClient); + event EventHandler? Attached; event EventHandler? Detached; diff --git a/src/console/LibplanetConsole.Console/IClientContent.cs b/src/console/LibplanetConsole.Console/IClientContent.cs index 6d42b653..e8aae15a 100644 --- a/src/console/LibplanetConsole.Console/IClientContent.cs +++ b/src/console/LibplanetConsole.Console/IClientContent.cs @@ -2,7 +2,9 @@ namespace LibplanetConsole.Console; public interface IClientContent { - IClient Client { get; } - string Name { get; } + + Task StartAsync(CancellationToken cancellationToken); + + Task StopAsync(CancellationToken cancellationToken); } diff --git a/src/console/LibplanetConsole.Console/INode.cs b/src/console/LibplanetConsole.Console/INode.cs index f99a95f8..9b84556b 100644 --- a/src/console/LibplanetConsole.Console/INode.cs +++ b/src/console/LibplanetConsole.Console/INode.cs @@ -1,10 +1,13 @@ using LibplanetConsole.Common; using LibplanetConsole.Node; +using Microsoft.Extensions.DependencyInjection; namespace LibplanetConsole.Console; -public interface INode : IAddressable, IAsyncDisposable, IServiceProvider, ISigner +public interface INode : IAddressable, IAsyncDisposable, IKeyedServiceProvider, ISigner { + const string Key = nameof(INode); + event EventHandler? Attached; event EventHandler? Detached; diff --git a/src/console/LibplanetConsole.Console/INodeContent.cs b/src/console/LibplanetConsole.Console/INodeContent.cs index 9550519e..96576087 100644 --- a/src/console/LibplanetConsole.Console/INodeContent.cs +++ b/src/console/LibplanetConsole.Console/INodeContent.cs @@ -2,7 +2,9 @@ namespace LibplanetConsole.Console; public interface INodeContent { - INode Node { get; } - string Name { get; } + + Task StartAsync(CancellationToken cancellationToken); + + Task StopAsync(CancellationToken cancellationToken); } diff --git a/src/console/LibplanetConsole.Console/Node.BlockChain.cs b/src/console/LibplanetConsole.Console/Node.BlockChain.cs index 91240665..74371991 100644 --- a/src/console/LibplanetConsole.Console/Node.BlockChain.cs +++ b/src/console/LibplanetConsole.Console/Node.BlockChain.cs @@ -11,6 +11,8 @@ internal sealed partial class Node public event EventHandler? BlockAppended; + public BlockInfo Tip => Info.Tip; + public async Task GetNextNonceAsync(Address address, CancellationToken cancellationToken) { if (_blockChainService is null) diff --git a/src/console/LibplanetConsole.Console/Node.cs b/src/console/LibplanetConsole.Console/Node.cs index cc2dd787..282d908d 100644 --- a/src/console/LibplanetConsole.Console/Node.cs +++ b/src/console/LibplanetConsole.Console/Node.cs @@ -29,6 +29,7 @@ internal sealed partial class Node : INode, IBlockChain private bool _isDisposed; private NodeProcess? _process; private Task _processTask = Task.CompletedTask; + private INodeContent[]? _contents; public Node(IServiceProvider serviceProvider, NodeOptions nodeOptions) { @@ -68,8 +69,34 @@ public EndPoint ConsensusEndPoint public NodeInfo Info => _nodeInfo; + public INodeContent[] Contents + { + get => _contents ?? throw new InvalidOperationException("Contents is not initialized."); + set => _contents = value; + } + public object? GetService(Type serviceType) => _serviceProvider.GetService(serviceType); + public object? GetKeyedService(Type serviceType, object? serviceKey) + { + if (_serviceProvider is IKeyedServiceProvider serviceProvider) + { + return serviceProvider.GetKeyedService(serviceType, serviceKey); + } + + throw new InvalidOperationException("Service provider does not support keyed service."); + } + + public object GetRequiredKeyedService(Type serviceType, object? serviceKey) + { + if (_serviceProvider is IKeyedServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredKeyedService(serviceType, serviceKey); + } + + throw new InvalidOperationException("Service provider does not support keyed service."); + } + public override string ToString() => $"{Address.ToShortString()}: {EndPoint}"; public byte[] Sign(object obj) => _nodeOptions.PrivateKey.Sign(obj); @@ -93,6 +120,7 @@ public async Task GetInfoAsync(CancellationToken cancellationToken) public async Task AttachAsync(CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_isDisposed, this); + if (_channel is not null) { throw new InvalidOperationException("Node is already attached."); @@ -106,8 +134,8 @@ public async Task AttachAsync(CancellationToken cancellationToken) blockChainService.BlockAppended += BlockChainService_BlockAppended; try { - await nodeService.StartAsync(cancellationToken); - await blockChainService.StartAsync(cancellationToken); + await nodeService.InitializeAsync(cancellationToken); + await blockChainService.InitializeAsync(cancellationToken); } catch { @@ -185,6 +213,8 @@ public async Task StartAsync(CancellationToken cancellationToken) _consensusEndPoint = EndPointUtility.Parse(_nodeInfo.ConsensusEndPoint); IsRunning = true; _logger.LogDebug("Node is started: {Address}", Address); + await Task.WhenAll(Contents.Select(item => item.StartAsync(cancellationToken))); + _logger.LogDebug("Node Contents are started: {Address}", Address); Started?.Invoke(this, EventArgs.Empty); } @@ -201,6 +231,9 @@ public async Task StopAsync(CancellationToken cancellationToken) throw new InvalidOperationException("Node is not attached."); } + await Task.WhenAll(Contents.Select(item => item.StopAsync(cancellationToken))); + _logger.LogDebug("Node Contents are stopped: {Address}", Address); + var request = new StopRequest(); var callOptions = new CallOptions(cancellationToken: cancellationToken); await _nodeService.StopAsync(request, callOptions); @@ -286,7 +319,7 @@ private void NodeService_Stopped(object? sender, EventArgs e) private void BlockChainService_BlockAppended(object? sender, BlockEventArgs e) { - _nodeInfo = _nodeInfo with { TipHash = e.BlockInfo.Hash }; + _nodeInfo = _nodeInfo with { Tip = e.BlockInfo }; BlockAppended?.Invoke(this, e); } diff --git a/src/console/LibplanetConsole.Console/NodeContentBase.cs b/src/console/LibplanetConsole.Console/NodeContentBase.cs index 07b02c75..fddd4bd0 100644 --- a/src/console/LibplanetConsole.Console/NodeContentBase.cs +++ b/src/console/LibplanetConsole.Console/NodeContentBase.cs @@ -1,63 +1,38 @@ namespace LibplanetConsole.Console; -public abstract class NodeContentBase : INodeContent, IDisposable +public abstract class NodeContentBase(string name) : INodeContent, IDisposable { - private readonly string _name; - private bool _isDisposed; - - protected NodeContentBase(INode node, string name) - { - _name = name; - Node = node; - Node.Attached += Node_Attached; - Node.Detached += Node_Detached; - Node.Started += Node_Started; - Node.Stopped += Node_Stopped; - } - - protected NodeContentBase(INode node) - : this(node, string.Empty) - { - } - - public INode Node { get; } + private readonly string _name = name; + private bool disposedValue; public string Name => _name != string.Empty ? _name : GetType().Name; void IDisposable.Dispose() { - if (_isDisposed is false) - { - Node.Attached -= Node_Attached; - Node.Detached -= Node_Detached; - Node.Started -= Node_Started; - Node.Stopped -= Node_Stopped; - _isDisposed = true; - GC.SuppressFinalize(this); - } + OnDispose(disposing: true); + GC.SuppressFinalize(this); } - protected virtual void OnNodeAttached() - { - } + Task INodeContent.StartAsync(CancellationToken cancellationToken) + => OnStartAsync(cancellationToken); - protected virtual void OnNodeDetached() - { - } - - protected virtual void OnNodeStarted() - { - } + Task INodeContent.StopAsync(CancellationToken cancellationToken) + => OnStopAsync(cancellationToken); - protected virtual void OnNodeStopped() - { - } + protected abstract Task OnStartAsync(CancellationToken cancellationToken); - private void Node_Attached(object? sender, EventArgs e) => OnNodeAttached(); + protected abstract Task OnStopAsync(CancellationToken cancellationToken); - private void Node_Detached(object? sender, EventArgs e) => OnNodeDetached(); - - private void Node_Started(object? sender, EventArgs e) => OnNodeStarted(); + protected virtual void OnDispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // do nothing + } - private void Node_Stopped(object? sender, EventArgs e) => OnNodeStopped(); + disposedValue = true; + } + } } diff --git a/src/console/LibplanetConsole.Console/NodeFactory.cs b/src/console/LibplanetConsole.Console/NodeFactory.cs index 8fbe251e..f4202331 100644 --- a/src/console/LibplanetConsole.Console/NodeFactory.cs +++ b/src/console/LibplanetConsole.Console/NodeFactory.cs @@ -9,7 +9,7 @@ internal static class NodeFactory private static readonly ConcurrentDictionary _valueByKey = []; private static readonly ConcurrentDictionary _scopeByNode = []; - public static Node Create(IServiceProvider serviceProvider) + public static Node Create(IServiceProvider serviceProvider, object? key) { if (_valueByKey.Remove(serviceProvider, out var descriptor) is true) { @@ -42,8 +42,10 @@ public static Node CreateNew(IServiceProvider serviceProvider, NodeOptions nodeO }, (k, v) => v); - var node = serviceScope.ServiceProvider.GetRequiredService(); - serviceScope.ServiceProvider.GetServices(); + var scopedServiceProvider = serviceScope.ServiceProvider; + var key = INode.Key; + var node = scopedServiceProvider.GetRequiredKeyedService(key); + node.Contents = [.. scopedServiceProvider.GetKeyedServices(key)]; return node; } diff --git a/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs b/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs index b3466410..ce9c6fc0 100644 --- a/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs +++ b/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using JSSoft.Commands; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common; using LibplanetConsole.Console.Commands; using LibplanetConsole.Seed; @@ -8,35 +9,46 @@ namespace LibplanetConsole.Console; public static class ServiceCollectionExtensions { - public static IServiceCollection AddConsole( - this IServiceCollection @this, ApplicationOptions options) - { - @this.AddSingleton(options); - @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()); - @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()); - @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()); - @this.AddHostedService(); + public static IServiceCollection AddConsole( + this IServiceCollection @this, ApplicationOptions options) + { + var synchronizationContext = SynchronizationContext.Current ?? new(); + SynchronizationContext.SetSynchronizationContext(synchronizationContext); + @this.AddSingleton(synchronizationContext); + @this.AddSingleton(options); + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()); + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()); + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()); + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()); - @this.AddScoped(NodeFactory.Create) - .AddScoped(s => s.GetRequiredService()) - .AddScoped(s => s.GetRequiredService()); - @this.AddScoped(ClientFactory.Create) - .AddScoped(s => s.GetRequiredService()); + @this.AddHostedService(); - @this.AddSingleton(); - @this.AddSingleton(); + @this.AddKeyedScoped(INode.Key, NodeFactory.Create) + .AddKeyedScoped( + INode.Key, (s, k) => s.GetRequiredKeyedService(k)) + .AddKeyedScoped( + INode.Key, (s, k) => s.GetRequiredKeyedService(k)); + @this.AddKeyedScoped(IClient.Key, ClientFactory.Create) + .AddKeyedScoped( + IClient.Key, (s, k) => s.GetRequiredKeyedService(k)) + .AddKeyedScoped( + IClient.Key, (s, k) => s.GetRequiredKeyedService(k)); - @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()); - @this.AddSingleton(); - @this.AddSingleton(); - @this.AddSingleton(); - @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()); - @this.AddSingleton(); - return @this; - } + @this.AddSingleton(); + @this.AddSingleton(); + + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()); + @this.AddSingleton(); + @this.AddSingleton(); + @this.AddSingleton(); + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()); + @this.AddSingleton(); + return @this; + } } diff --git a/src/node/LibplanetConsole.Node.Evidence/Services/EvidenceServiceGrpcV1.cs b/src/node/LibplanetConsole.Node.Evidence/Grpc/EvidenceServiceGrpcV1.cs similarity index 86% rename from src/node/LibplanetConsole.Node.Evidence/Services/EvidenceServiceGrpcV1.cs rename to src/node/LibplanetConsole.Node.Evidence/Grpc/EvidenceServiceGrpcV1.cs index b6c2df2c..9c11f28a 100644 --- a/src/node/LibplanetConsole.Node.Evidence/Services/EvidenceServiceGrpcV1.cs +++ b/src/node/LibplanetConsole.Node.Evidence/Grpc/EvidenceServiceGrpcV1.cs @@ -1,7 +1,7 @@ using Grpc.Core; using LibplanetConsole.Evidence.Grpc; -namespace LibplanetConsole.Node.Evidence.Services; +namespace LibplanetConsole.Node.Evidence.Grpc; internal sealed class EvidenceServiceGrpcV1(Evidence evidence) : EvidenceGrpcService.EvidenceGrpcServiceBase @@ -12,7 +12,7 @@ public override async Task AddEvidence( var evidenceInfo = await evidence.AddEvidenceAsync(context.CancellationToken); return new AddEvidenceResponse { - EvidenceInfo = evidenceInfo, + EvidenceInformation = evidenceInfo, }; } @@ -24,7 +24,7 @@ public override async Task GetEvidence( var response = new GetEvidenceResponse(); for (var i = 0; i < evidenceInfos.Length; i++) { - response.EvidenceInfos.Add(evidenceInfos[i]); + response.EvidenceInformations.Add(evidenceInfos[i]); } return response; diff --git a/src/node/LibplanetConsole.Node.Evidence/NodeEndpointRouteBuilderExtensions.cs b/src/node/LibplanetConsole.Node.Evidence/NodeEndpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..1380097e --- /dev/null +++ b/src/node/LibplanetConsole.Node.Evidence/NodeEndpointRouteBuilderExtensions.cs @@ -0,0 +1,15 @@ +using LibplanetConsole.Node.Evidence.Grpc; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace LibplanetConsole.Node.Evidence; + +public static class NodeEndpointRouteBuilderExtensions +{ + public static IEndpointRouteBuilder UseEvidence(this IEndpointRouteBuilder @this) + { + @this.MapGrpcService(); + + return @this; + } +} diff --git a/src/node/LibplanetConsole.Node.Evidence/ServiceCollectionExtensions.cs b/src/node/LibplanetConsole.Node.Evidence/ServiceCollectionExtensions.cs index 8eb9bf5e..dc13fb40 100644 --- a/src/node/LibplanetConsole.Node.Evidence/ServiceCollectionExtensions.cs +++ b/src/node/LibplanetConsole.Node.Evidence/ServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ public static IServiceCollection AddEvidence(this IServiceCollection @this) { @this.AddSingleton() .AddSingleton(s => s.GetRequiredService()); - // @this.AddSingleton(); @this.AddSingleton(); return @this; } diff --git a/src/node/LibplanetConsole.Node.Executable/Application.cs b/src/node/LibplanetConsole.Node.Executable/Application.cs index d2dfc96a..bdb10619 100644 --- a/src/node/LibplanetConsole.Node.Executable/Application.cs +++ b/src/node/LibplanetConsole.Node.Executable/Application.cs @@ -1,6 +1,7 @@ using JSSoft.Commands; using LibplanetConsole.Common; using LibplanetConsole.Logging; +using LibplanetConsole.Node.Evidence; using LibplanetConsole.Node.Executable.Commands; using LibplanetConsole.Node.Executable.Tracers; using LibplanetConsole.Node.Explorer; @@ -34,7 +35,10 @@ public Application(ApplicationOptions options, object[] instances) options.ListenLocalhost(port, o => o.Protocols = HttpProtocols.Http2); }); - services.AddLogging(options.LogPath, "node.log", _filters); + if (options.LogPath != string.Empty) + { + services.AddLogging(options.LogPath, "node.log", _filters); + } services.AddSingleton(); services.AddSingleton(); @@ -46,6 +50,7 @@ public Application(ApplicationOptions options, object[] instances) services.AddNode(options); services.AddExplorer(_builder.Configuration); + services.AddEvidence(); services.AddGrpc(); services.AddGrpcReflection(); @@ -61,6 +66,7 @@ public async Task RunAsync(CancellationToken cancellationToken) app.UseNode(); app.UseExplorer(); + app.UseEvidence(); app.MapGet("/", () => "Libplanet-Node"); app.UseAuthentication(); app.UseAuthorization(); diff --git a/src/node/LibplanetConsole.Node.Executable/EntryCommands/InitializeCommand.cs b/src/node/LibplanetConsole.Node.Executable/EntryCommands/InitializeCommand.cs index 11590177..f1c1df52 100644 --- a/src/node/LibplanetConsole.Node.Executable/EntryCommands/InitializeCommand.cs +++ b/src/node/LibplanetConsole.Node.Executable/EntryCommands/InitializeCommand.cs @@ -41,8 +41,7 @@ public InitializeCommand() public string StorePath { get; set; } = string.Empty; [CommandProperty] - [CommandSummary("The file path to store the application logs." + - "If omitted, the 'app.log' file is used.")] + [CommandSummary("Indicates the file path to save logs.")] [Path(Type = PathType.Directory, AllowEmpty = true)] public string LogPath { get; set; } = string.Empty; diff --git a/src/node/LibplanetConsole.Node.Executable/SystemTerminal.cs b/src/node/LibplanetConsole.Node.Executable/SystemTerminal.cs index de00a093..ea3f469f 100644 --- a/src/node/LibplanetConsole.Node.Executable/SystemTerminal.cs +++ b/src/node/LibplanetConsole.Node.Executable/SystemTerminal.cs @@ -1,22 +1,67 @@ +using System.Text; using JSSoft.Commands.Extensions; using JSSoft.Terminals; +using LibplanetConsole.Blockchain; +using LibplanetConsole.Common.Extensions; namespace LibplanetConsole.Node.Executable; internal sealed class SystemTerminal : SystemTerminalBase { + private const string PromptText = "libplanet-node $ "; + private readonly SynchronizationContext _synchronizationContext; private readonly CommandContext _commandContext; + private readonly IBlockChain _blockChain; + private BlockInfo _tip; public SystemTerminal( - IHostApplicationLifetime applicationLifetime, CommandContext commandContext) + IHostApplicationLifetime applicationLifetime, + CommandContext commandContext, + IBlockChain blockChain, + SynchronizationContext synchronizationContext) { + _synchronizationContext = synchronizationContext; _commandContext = commandContext; _commandContext.Owner = applicationLifetime; - Prompt = "libplanet-node $ "; + _blockChain = blockChain; + _blockChain.BlockAppended += BlockChain_BlockAppended; + _blockChain.Started += BlockChain_Started; + _blockChain.Stopped += BlockChain_Stopped; + UpdatePrompt(_blockChain.Tip); applicationLifetime.ApplicationStopping.Register(() => Prompt = "\u001b0"); } - protected override string FormatPrompt(string prompt) => prompt; + protected override void OnDispose() + { + _blockChain.Started -= BlockChain_Started; + _blockChain.Stopped -= BlockChain_Stopped; + _blockChain.BlockAppended -= BlockChain_BlockAppended; + base.OnDispose(); + } + + protected override string FormatPrompt(string prompt) + { + var tip = _tip; + if (_tip.Height == -1) + { + return prompt; + } + else + { + var tsb = new TerminalStringBuilder(); + tsb.AppendEnd(); + tsb.Append($"#{tip.Height} "); + tsb.Foreground = TerminalColorType.BrightGreen; + tsb.Append($"{tip.Hash.ToShortString()} "); + tsb.ResetOptions(); + tsb.Append($"by "); + tsb.Foreground = TerminalColorType.BrightGreen; + tsb.Append($"{tip.Miner.ToShortString()}"); + tsb.ResetOptions(); + tsb.AppendEnd(); + return $"[{tsb}] {PromptText}"; + } + } protected override string[] GetCompletion(string[] items, string find) => _commandContext.GetCompletion(items, find); @@ -29,4 +74,32 @@ protected override void OnInitialize(TextWriter @out, TextWriter error) _commandContext.Out = @out; _commandContext.Error = error; } + + private void BlockChain_BlockAppended(object? sender, BlockEventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(e.BlockInfo), null); + + private void BlockChain_Started(object? sender, EventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(_blockChain.Tip), null); + + private void BlockChain_Stopped(object? sender, EventArgs e) + => _synchronizationContext.Post(_ => UpdatePrompt(_blockChain.Tip), null); + + private void UpdatePrompt(BlockInfo tip) + { + if (tip.Height == -1) + { + Prompt = PromptText; + } + else + { + var sb = new StringBuilder(); + sb.Append($"#{tip.Height} "); + sb.Append($"{tip.Hash.ToShortString()} "); + sb.Append($"by "); + sb.Append($"{tip.Miner.ToShortString()}"); + Prompt = $"[{sb}] {PromptText}"; + } + + _tip = tip; + } } diff --git a/src/node/LibplanetConsole.Node.Executable/Tracers/BlockChainEventTracer.cs b/src/node/LibplanetConsole.Node.Executable/Tracers/BlockChainEventTracer.cs index a07e24ea..3d0c23d1 100644 --- a/src/node/LibplanetConsole.Node.Executable/Tracers/BlockChainEventTracer.cs +++ b/src/node/LibplanetConsole.Node.Executable/Tracers/BlockChainEventTracer.cs @@ -33,9 +33,10 @@ private void Node_BlockAppended(object? sender, BlockEventArgs e) var blockInfo = e.BlockInfo; var hash = blockInfo.Hash; var miner = blockInfo.Miner; - var message = $"Block #{blockInfo.Height} '{hash.ToShortString()}' " + - $"Appended by '{miner.ToShortString()}'"; - Console.Out.WriteColoredLine(message, TerminalColorType.BrightGreen); - _logger.LogInformation(message); + _logger.LogInformation( + "Block #{TipHeight} '{TipHash}' Appended by '{TipMiner}'", + blockInfo.Height, + hash.ToShortString(), + miner.ToShortString()); } } diff --git a/src/node/LibplanetConsole.Node/Commands/TxCommand.cs b/src/node/LibplanetConsole.Node/Commands/TxCommand.cs index 40bceff1..fe01c09d 100644 --- a/src/node/LibplanetConsole.Node/Commands/TxCommand.cs +++ b/src/node/LibplanetConsole.Node/Commands/TxCommand.cs @@ -1,4 +1,5 @@ using JSSoft.Commands; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common.Actions; using LibplanetConsole.Common.Extensions; diff --git a/src/node/LibplanetConsole.Node/Grpc/BlockChainGrpcServiceV1.cs b/src/node/LibplanetConsole.Node/Grpc/BlockChainGrpcServiceV1.cs index 7a5ec726..bafb213f 100644 --- a/src/node/LibplanetConsole.Node/Grpc/BlockChainGrpcServiceV1.cs +++ b/src/node/LibplanetConsole.Node/Grpc/BlockChainGrpcServiceV1.cs @@ -48,7 +48,7 @@ public async override Task GetNextNonce( public override async Task GetTipHash( GetTipHashRequest request, ServerCallContext context) { - var blockHash = await _blockChain.GetTipHashAsync(context.CancellationToken); + var blockHash = await Task.FromResult(_blockChain.Tip.Hash); return new GetTipHashResponse { BlockHash = blockHash.ToString() }; } diff --git a/src/node/LibplanetConsole.Node/Grpc/NodeGrpcServiceV1.cs b/src/node/LibplanetConsole.Node/Grpc/NodeGrpcServiceV1.cs index 59172fe0..331fa3e9 100644 --- a/src/node/LibplanetConsole.Node/Grpc/NodeGrpcServiceV1.cs +++ b/src/node/LibplanetConsole.Node/Grpc/NodeGrpcServiceV1.cs @@ -6,11 +6,12 @@ namespace LibplanetConsole.Node.Grpc; -internal sealed class NodeGrpcServiceV1 : NodeGrpcService.NodeGrpcServiceBase +internal sealed class NodeGrpcServiceV1 : NodeGrpcService.NodeGrpcServiceBase, IDisposable { private readonly IHostApplicationLifetime _applicationLifetime; private readonly Node _node; private readonly ILogger _logger; + private bool _isDisposed; public NodeGrpcServiceV1( IHostApplicationLifetime applicationLifetime, @@ -75,4 +76,13 @@ public override async Task GetStoppedStream( handler => _node.Stopped -= handler); await streamer.RunAsync(_applicationLifetime, context.CancellationToken); } + + public void Dispose() + { + if (_isDisposed is false) + { + _logger.LogDebug("{GrpcServiceType} is disposed.", nameof(NodeGrpcServiceV1)); + _isDisposed = true; + } + } } diff --git a/src/node/LibplanetConsole.Node/IBlockChain.cs b/src/node/LibplanetConsole.Node/IBlockChain.cs deleted file mode 100644 index e410ad4d..00000000 --- a/src/node/LibplanetConsole.Node/IBlockChain.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Security.Cryptography; -using LibplanetConsole.Blockchain; - -namespace LibplanetConsole.Node; - -public interface IBlockChain -{ - event EventHandler? BlockAppended; - - Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); - - Task GetNextNonceAsync(Address address, CancellationToken cancellationToken); - - Task GetTipHashAsync(CancellationToken cancellationToken); - - Task GetStateAsync( - BlockHash? blockHash, - Address accountAddress, - Address address, - CancellationToken cancellationToken); - - Task GetStateByStateRootHashAsync( - HashDigest stateRootHash, - Address accountAddress, - Address address, - CancellationToken cancellationToken); - - Task GetBlockHashAsync(long height, CancellationToken cancellationToken); - - Task GetActionAsync(TxId txId, int actionIndex, CancellationToken cancellationToken) - where T : IAction; -} diff --git a/src/node/LibplanetConsole.Node/LibplanetConsole.Node.csproj b/src/node/LibplanetConsole.Node/LibplanetConsole.Node.csproj index 2f999b5f..60cdec0a 100644 --- a/src/node/LibplanetConsole.Node/LibplanetConsole.Node.csproj +++ b/src/node/LibplanetConsole.Node/LibplanetConsole.Node.csproj @@ -22,11 +22,11 @@ - - - - - + + + + + diff --git a/src/node/LibplanetConsole.Node/Node.BlockChain.cs b/src/node/LibplanetConsole.Node/Node.BlockChain.cs index 890e0c13..45519bdd 100644 --- a/src/node/LibplanetConsole.Node/Node.BlockChain.cs +++ b/src/node/LibplanetConsole.Node/Node.BlockChain.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using System.Text; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common; using LibplanetConsole.Common.Exceptions; using Microsoft.Extensions.Logging; @@ -10,6 +11,10 @@ internal sealed partial class Node : IBlockChain { private static readonly Codec _codec = new(); + public event EventHandler? BlockAppended; + + public BlockInfo Tip => Info.Tip; + public async Task SendTransactionAsync( IAction[] actions, CancellationToken cancellationToken) { diff --git a/src/node/LibplanetConsole.Node/Node.cs b/src/node/LibplanetConsole.Node/Node.cs index 8b954d7a..f49cbbee 100644 --- a/src/node/LibplanetConsole.Node/Node.cs +++ b/src/node/LibplanetConsole.Node/Node.cs @@ -54,8 +54,6 @@ public Node(IServiceProvider serviceProvider, ApplicationOptions options) _logger.LogDebug("Node is created: {Address}", Address); } - public event EventHandler? BlockAppended; - public event EventHandler? Started; public event EventHandler? Stopped; @@ -280,10 +278,8 @@ void Action(object? state) } } - var blockChain = _swarm!.BlockChain; - var blockInfo = new BlockInfo(blockChain, blockChain.Tip); UpdateNodeInfo(); - BlockAppended?.Invoke(this, new(blockInfo)); + BlockAppended?.Invoke(this, new(Info.Tip)); } } @@ -341,7 +337,7 @@ private void UpdateNodeInfo() SwarmEndPoint = EndPointUtility.ToString(SwarmEndPoint), ConsensusEndPoint = EndPointUtility.ToString(ConsensusEndPoint), GenesisHash = BlockChain.Genesis.Hash, - TipHash = BlockChain.Tip.Hash, + Tip = new BlockInfo(BlockChain.Tip), IsRunning = IsRunning, }; } diff --git a/src/node/LibplanetConsole.Node/ServiceCollectionExtensions.cs b/src/node/LibplanetConsole.Node/ServiceCollectionExtensions.cs index bfc65575..cc2d16e2 100644 --- a/src/node/LibplanetConsole.Node/ServiceCollectionExtensions.cs +++ b/src/node/LibplanetConsole.Node/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using JSSoft.Commands; +using LibplanetConsole.Blockchain; using LibplanetConsole.Common; using LibplanetConsole.Node.Commands; using LibplanetConsole.Seed; diff --git a/src/shared/LibplanetConsole.Blockchain/BlockInfo.Node.cs b/src/shared/LibplanetConsole.Blockchain/BlockInfo.Node.cs index 3da91018..50c60fbb 100644 --- a/src/shared/LibplanetConsole.Blockchain/BlockInfo.Node.cs +++ b/src/shared/LibplanetConsole.Blockchain/BlockInfo.Node.cs @@ -8,7 +8,7 @@ namespace LibplanetConsole.Blockchain; public readonly partial record struct BlockInfo { - public BlockInfo(BlockChain blockChain, Block block) + public BlockInfo(Block block) { Height = block.Index; Hash = block.Hash; diff --git a/src/shared/LibplanetConsole.Blockchain/BlockInfo.cs b/src/shared/LibplanetConsole.Blockchain/BlockInfo.cs index 52af8475..1292cb07 100644 --- a/src/shared/LibplanetConsole.Blockchain/BlockInfo.cs +++ b/src/shared/LibplanetConsole.Blockchain/BlockInfo.cs @@ -14,6 +14,11 @@ public BlockInfo() public Address Miner { get; init; } + public static BlockInfo Empty { get; } = new BlockInfo + { + Height = -1, + }; + public static implicit operator BlockInfo(BlockInformation blockInfo) { return new BlockInfo diff --git a/src/shared/LibplanetConsole.Blockchain/Grpc/BlockChainService.cs b/src/shared/LibplanetConsole.Blockchain/Grpc/BlockChainService.cs index 46124843..84f40696 100644 --- a/src/shared/LibplanetConsole.Blockchain/Grpc/BlockChainService.cs +++ b/src/shared/LibplanetConsole.Blockchain/Grpc/BlockChainService.cs @@ -25,7 +25,7 @@ public void Dispose() } } - public async Task StartAsync(CancellationToken cancellationToken) + public async Task InitializeAsync(CancellationToken cancellationToken) { if (_blockAppendedReceiver is not null) { @@ -38,7 +38,7 @@ public async Task StartAsync(CancellationToken cancellationToken) await _blockAppendedReceiver.StartAsync(cancellationToken); } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task ReleaseAsync(CancellationToken cancellationToken) { if (_blockAppendedReceiver is null) { diff --git a/src/client/LibplanetConsole.Client/IBlockChain.cs b/src/shared/LibplanetConsole.Blockchain/IBlockChain.cs similarity index 84% rename from src/client/LibplanetConsole.Client/IBlockChain.cs rename to src/shared/LibplanetConsole.Blockchain/IBlockChain.cs index b23a282e..71e6c652 100644 --- a/src/client/LibplanetConsole.Client/IBlockChain.cs +++ b/src/shared/LibplanetConsole.Blockchain/IBlockChain.cs @@ -1,18 +1,23 @@ using System.Security.Cryptography; -using LibplanetConsole.Blockchain; -namespace LibplanetConsole.Client; +namespace LibplanetConsole.Blockchain; public interface IBlockChain { event EventHandler? BlockAppended; + event EventHandler? Started; + + event EventHandler? Stopped; + + bool IsRunning { get; } + + BlockInfo Tip { get; } + Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); Task GetNextNonceAsync(Address address, CancellationToken cancellationToken); - Task GetTipHashAsync(CancellationToken cancellationToken); - Task GetStateAsync( BlockHash? blockHash, Address accountAddress, diff --git a/src/shared/LibplanetConsole.Client/ClientInfo.cs b/src/shared/LibplanetConsole.Client/ClientInfo.cs index 9a3a8ca3..3ecac980 100644 --- a/src/shared/LibplanetConsole.Client/ClientInfo.cs +++ b/src/shared/LibplanetConsole.Client/ClientInfo.cs @@ -1,3 +1,4 @@ +using LibplanetConsole.Blockchain; using LibplanetConsole.Client.Grpc; namespace LibplanetConsole.Client; @@ -10,12 +11,13 @@ public readonly record struct ClientInfo public BlockHash GenesisHash { get; init; } - public BlockHash TipHash { get; init; } + public BlockInfo Tip { get; init; } public bool IsRunning { get; init; } public static ClientInfo Empty { get; } = new ClientInfo { + Tip = BlockInfo.Empty, }; public static implicit operator ClientInfo(ClientInformation clientInfo) @@ -25,7 +27,12 @@ public static implicit operator ClientInfo(ClientInformation clientInfo) Address = new Address(clientInfo.Address), NodeAddress = new Address(clientInfo.NodeAddress), GenesisHash = BlockHash.FromString(clientInfo.GenesisHash), - TipHash = BlockHash.FromString(clientInfo.TipHash), + Tip = new BlockInfo + { + Hash = BlockHash.FromString(clientInfo.TipHash), + Height = clientInfo.TipHeight, + Miner = new Address(clientInfo.TipMiner), + }, IsRunning = clientInfo.IsRunning, }; } @@ -37,7 +44,9 @@ public static implicit operator ClientInformation(ClientInfo clientInfo) Address = clientInfo.Address.ToHex(), NodeAddress = clientInfo.NodeAddress.ToHex(), GenesisHash = clientInfo.GenesisHash.ToString(), - TipHash = clientInfo.TipHash.ToString(), + TipHash = clientInfo.Tip.Hash.ToString(), + TipHeight = clientInfo.Tip.Height, + TipMiner = clientInfo.Tip.Miner.ToHex(), IsRunning = clientInfo.IsRunning, }; } diff --git a/src/shared/LibplanetConsole.Client/Protos/ClientGrpcService.proto b/src/shared/LibplanetConsole.Client/Protos/ClientGrpcService.proto index 2adadc1b..c0e6378b 100644 --- a/src/shared/LibplanetConsole.Client/Protos/ClientGrpcService.proto +++ b/src/shared/LibplanetConsole.Client/Protos/ClientGrpcService.proto @@ -19,7 +19,9 @@ message ClientInformation { string node_address = 2; string genesis_hash = 3; string tip_hash = 4; - bool is_running = 5; + int64 tip_height = 5; + string tip_miner = 6; + bool is_running = 7; } message PingRequest { diff --git a/src/shared/LibplanetConsole.Evidence/EvidenceInfo.cs b/src/shared/LibplanetConsole.Evidence/EvidenceInfo.cs index fbbfbf06..b815ffc8 100644 --- a/src/shared/LibplanetConsole.Evidence/EvidenceInfo.cs +++ b/src/shared/LibplanetConsole.Evidence/EvidenceInfo.cs @@ -1,5 +1,5 @@ using Libplanet.Types.Evidence; -using GrpcEvidenceInfo = LibplanetConsole.Evidence.Grpc.EvidenceInfo; +using LibplanetConsole.Evidence.Grpc; namespace LibplanetConsole.Evidence; @@ -27,7 +27,7 @@ public static explicit operator EvidenceInfo(EvidenceBase evidence) }; } - public static implicit operator EvidenceInfo(GrpcEvidenceInfo evidenceInfo) + public static implicit operator EvidenceInfo(EvidenceInformation evidenceInfo) { return new EvidenceInfo { @@ -39,9 +39,9 @@ public static implicit operator EvidenceInfo(GrpcEvidenceInfo evidenceInfo) }; } - public static implicit operator GrpcEvidenceInfo(EvidenceInfo evidenceInfo) + public static implicit operator EvidenceInformation(EvidenceInfo evidenceInfo) { - return new GrpcEvidenceInfo + return new EvidenceInformation { Type = evidenceInfo.Type, Id = evidenceInfo.Id, diff --git a/src/shared/LibplanetConsole.Evidence/Protos/EvidenceGrpcService.proto b/src/shared/LibplanetConsole.Evidence/Protos/EvidenceGrpcService.proto index bd62d5b1..411fe014 100644 --- a/src/shared/LibplanetConsole.Evidence/Protos/EvidenceGrpcService.proto +++ b/src/shared/LibplanetConsole.Evidence/Protos/EvidenceGrpcService.proto @@ -12,7 +12,7 @@ service EvidenceGrpcService { rpc Violate(ViolateRequest) returns (ViolateResponse); } -message EvidenceInfo { +message EvidenceInformation { string type = 1; string id = 2; string targetAddress = 3; @@ -27,7 +27,7 @@ message AddEvidenceRequest { } message AddEvidenceResponse { - EvidenceInfo evidenceInfo = 1; + EvidenceInformation EvidenceInformation = 1; } message GetEvidenceRequest { @@ -35,7 +35,7 @@ message GetEvidenceRequest { } message GetEvidenceResponse { - repeated EvidenceInfo evidenceInfos = 1; + repeated EvidenceInformation EvidenceInformations = 1; } message ViolateRequest { diff --git a/src/shared/LibplanetConsole.Grpc/ConnectionMonitor.cs b/src/shared/LibplanetConsole.Grpc/ConnectionMonitor.cs index f45c90a5..3b028eb8 100644 --- a/src/shared/LibplanetConsole.Grpc/ConnectionMonitor.cs +++ b/src/shared/LibplanetConsole.Grpc/ConnectionMonitor.cs @@ -24,7 +24,7 @@ protected override async Task OnRunAsync(CancellationToken cancellationToken) { await action(client, cancellationToken); } - catch (RpcException e) + catch (RpcException) { Disconnected?.Invoke(this, EventArgs.Empty); break; diff --git a/src/shared/LibplanetConsole.Grpc/EventStreamer.cs b/src/shared/LibplanetConsole.Grpc/EventStreamer.cs index 81ba1926..e49124e4 100644 --- a/src/shared/LibplanetConsole.Grpc/EventStreamer.cs +++ b/src/shared/LibplanetConsole.Grpc/EventStreamer.cs @@ -11,10 +11,10 @@ internal sealed class EventStreamer( { protected async override Task OnRun(CancellationToken cancellationToken) { - void Handler(object? s, TEventArgs args) + async void Handler(object? s, TEventArgs args) { var value = selector(args); - WriteValue(value); + await WriteValueAsync(value); } attach(Handler); @@ -45,10 +45,10 @@ public EventStreamer( protected async override Task OnRun(CancellationToken cancellationToken) { - void Handler(object? s, EventArgs args) + async void Handler(object? s, EventArgs args) { var value = selector(); - WriteValue(value); + await WriteValueAsync(value); } attach(Handler); diff --git a/src/shared/LibplanetConsole.Grpc/Streamer.cs b/src/shared/LibplanetConsole.Grpc/Streamer.cs index 319f7bd8..1329485f 100644 --- a/src/shared/LibplanetConsole.Grpc/Streamer.cs +++ b/src/shared/LibplanetConsole.Grpc/Streamer.cs @@ -45,7 +45,7 @@ protected virtual async Task OnRun(CancellationToken cancellationToken) } } - protected async void WriteValue(T value) + protected async Task WriteValueAsync(T value) { if (_cancellationTokenSource is null) { diff --git a/src/shared/LibplanetConsole.Node/Grpc/NodeService.cs b/src/shared/LibplanetConsole.Node/Grpc/NodeService.cs index 858c00b5..649a0954 100644 --- a/src/shared/LibplanetConsole.Node/Grpc/NodeService.cs +++ b/src/shared/LibplanetConsole.Node/Grpc/NodeService.cs @@ -20,6 +20,8 @@ internal sealed class NodeService(GrpcChannel channel) public event EventHandler? Stopped; + public NodeInfo Info { get; private set; } + public void Dispose() { if (_isDisposed is false) @@ -34,7 +36,7 @@ public void Dispose() } } - public async Task StartAsync(CancellationToken cancellationToken) + public async Task InitializeAsync(CancellationToken cancellationToken) { if (_connection is not null) { @@ -53,9 +55,10 @@ public async Task StartAsync(CancellationToken cancellationToken) await Task.WhenAll( _startedReceiver.StartAsync(cancellationToken), _stoppedReceiver.StartAsync(cancellationToken)); + Info = (await GetInfoAsync(new(), cancellationToken: cancellationToken)).NodeInfo; } - public async Task StopAsync(CancellationToken cancellationToken) + public async Task ReleaseAsync(CancellationToken cancellationToken) { if (_connection is null) { @@ -79,6 +82,66 @@ public async Task StopAsync(CancellationToken cancellationToken) _connection = null; } + public override AsyncUnaryCall StartAsync( + StartRequest request, CallOptions options) + { + if (_startedReceiver is null) + { + throw new InvalidOperationException($"{nameof(NodeService)} is not initialized."); + } + + var call = base.StartAsync(request, options); + return new AsyncUnaryCall( + responseAsync: ResponseAsync(), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + + async Task ResponseAsync() + { + await _startedReceiver.StopAsync(default); + try + { + return await call.ResponseAsync; + } + finally + { + await _startedReceiver.StartAsync(default); + } + } + } + + public override AsyncUnaryCall StopAsync( + StopRequest request, CallOptions options) + { + if (_stoppedReceiver is null) + { + throw new InvalidOperationException($"{nameof(NodeService)} is not initialized."); + } + + var call = base.StopAsync(request, options); + return new AsyncUnaryCall( + responseAsync: ResponseAsync(), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + + async Task ResponseAsync() + { + await _stoppedReceiver.StopAsync(default); + try + { + return await call.ResponseAsync; + } + finally + { + await _stoppedReceiver.StartAsync(default); + } + } + } + private static async Task CheckConnectionAsync( NodeService nodeService, CancellationToken cancellationToken) { diff --git a/src/shared/LibplanetConsole.Node/NodeInfo.cs b/src/shared/LibplanetConsole.Node/NodeInfo.cs index d13a56c1..9198c31a 100644 --- a/src/shared/LibplanetConsole.Node/NodeInfo.cs +++ b/src/shared/LibplanetConsole.Node/NodeInfo.cs @@ -1,3 +1,4 @@ +using LibplanetConsole.Blockchain; using LibplanetConsole.Node.Grpc; namespace LibplanetConsole.Node; @@ -16,7 +17,7 @@ public readonly record struct NodeInfo public BlockHash GenesisHash { get; init; } - public BlockHash TipHash { get; init; } + public BlockInfo Tip { get; init; } public bool IsRunning { get; init; } @@ -26,6 +27,7 @@ public readonly record struct NodeInfo AppProtocolVersion = string.Empty, SwarmEndPoint = string.Empty, ConsensusEndPoint = string.Empty, + Tip = BlockInfo.Empty, }; public static implicit operator NodeInfo(NodeInformation nodeInfo) @@ -38,7 +40,12 @@ public static implicit operator NodeInfo(NodeInformation nodeInfo) ConsensusEndPoint = nodeInfo.ConsensusEndPoint, Address = new Address(nodeInfo.Address), GenesisHash = BlockHash.FromString(nodeInfo.GenesisHash), - TipHash = BlockHash.FromString(nodeInfo.TipHash), + Tip = new BlockInfo + { + Height = nodeInfo.TipHeight, + Hash = BlockHash.FromString(nodeInfo.TipHash), + Miner = new Address(nodeInfo.TipMiner), + }, IsRunning = nodeInfo.IsRunning, }; } @@ -53,7 +60,9 @@ public static implicit operator NodeInformation(NodeInfo nodeInfo) ConsensusEndPoint = nodeInfo.ConsensusEndPoint, Address = nodeInfo.Address.ToHex(), GenesisHash = nodeInfo.GenesisHash.ToString(), - TipHash = nodeInfo.TipHash.ToString(), + TipHash = nodeInfo.Tip.Hash.ToString(), + TipHeight = nodeInfo.Tip.Height, + TipMiner = nodeInfo.Tip.Miner.ToHex(), IsRunning = nodeInfo.IsRunning, }; } diff --git a/src/shared/LibplanetConsole.Node/Protos/NodeGrpcService.proto b/src/shared/LibplanetConsole.Node/Protos/NodeGrpcService.proto index eacdd1bc..48331c18 100644 --- a/src/shared/LibplanetConsole.Node/Protos/NodeGrpcService.proto +++ b/src/shared/LibplanetConsole.Node/Protos/NodeGrpcService.proto @@ -22,7 +22,9 @@ message NodeInformation { string address = 5; string genesis_hash = 6; string tip_hash = 7; - bool is_running = 8; + int64 tip_height = 8; + string tip_miner = 9; + bool is_running = 10; } message PingRequest {