diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index abe4c7d13..678281ca5 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -8,7 +8,10 @@ env: jobs: Test: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v1 @@ -17,26 +20,29 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Check format + if: runner.os == 'Linux' run: | dotnet tool install --version 3.2.111002 --tool-path ./ dotnet-format --add-source https://dotnet.myget.org/F/format/api/v3/index.json ./dotnet-format --check --dry-run -v diagnostic - - name: Build + - name: Build CLI + if: runner.os == 'Linux' run: | dotnet publish -o ./out -c Release neo-cli find ./out -name 'config.json' | xargs perl -pi -e 's|LevelDBStore|MemoryStore|g' - name: Install dependencies + if: runner.os == 'Linux' run: sudo apt-get install libleveldb-dev expect - name: Run tests with expect + if: runner.os == 'Linux' run: expect ./.github/workflows/test-neo-cli.expect - - Test_GUI: - runs-on: windows-latest - steps: - - name: Chectout - uses: actions/checkout@v1 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Build - run: dotnet build -c Release neo-gui + - name: Run Unit Tests + if: runner.os == 'Windows' + run: | + forfiles /p tests /m *.csproj /s /c "cmd /c dotnet add @PATH package coverlet.msbuild" + dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=${GITHUB_WORKSPACE}/coverage/lcov + - name: Coveralls + if: runner.os == 'Windows' + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: \coverage\lcov.info diff --git a/Neo.ConsoleService/CommandQuoteToken.cs b/Neo.ConsoleService/CommandQuoteToken.cs new file mode 100644 index 000000000..e0e1f5443 --- /dev/null +++ b/Neo.ConsoleService/CommandQuoteToken.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics; + +namespace Neo.ConsoleService +{ + [DebuggerDisplay("Value={Value}, Value={Value}")] + internal class CommandQuoteToken : CommandToken + { + /// + /// Constructor + /// + /// Offset + /// Value + public CommandQuoteToken(int offset, char value) : base(CommandTokenType.Quote, offset) + { + if (value != '\'' && value != '"') + { + throw new ArgumentException("Not valid quote"); + } + + Value = value.ToString(); + } + + /// + /// Parse command line quotes + /// + /// Command line + /// Index + /// CommandQuoteToken + internal static CommandQuoteToken Parse(string commandLine, ref int index) + { + var c = commandLine[index]; + + if (c == '\'' || c == '"') + { + index++; + return new CommandQuoteToken(index - 1, c); + } + + throw new ArgumentException("No quote found"); + } + } +} diff --git a/Neo.ConsoleService/CommandSpaceToken.cs b/Neo.ConsoleService/CommandSpaceToken.cs new file mode 100644 index 000000000..5d32fa258 --- /dev/null +++ b/Neo.ConsoleService/CommandSpaceToken.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; + +namespace Neo.ConsoleService +{ + [DebuggerDisplay("Value={Value}, Count={Count}")] + internal class CommandSpaceToken : CommandToken + { + /// + /// Count + /// + public int Count { get; } + + /// + /// Constructor + /// + /// Offset + /// Count + public CommandSpaceToken(int offset, int count) : base(CommandTokenType.Space, offset) + { + Value = "".PadLeft(count, ' '); + Count = count; + } + + /// + /// Parse command line spaces + /// + /// Command line + /// Index + /// CommandSpaceToken + internal static CommandSpaceToken Parse(string commandLine, ref int index) + { + int offset = index; + int count = 0; + + for (int ix = index, max = commandLine.Length; ix < max; ix++) + { + if (commandLine[ix] == ' ') + { + count++; + } + else + { + break; + } + } + + if (count == 0) throw new ArgumentException("No spaces found"); + + index += count; + return new CommandSpaceToken(offset, count); + } + } +} diff --git a/Neo.ConsoleService/CommandStringToken.cs b/Neo.ConsoleService/CommandStringToken.cs new file mode 100644 index 000000000..9c01abe9b --- /dev/null +++ b/Neo.ConsoleService/CommandStringToken.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; + +namespace Neo.ConsoleService +{ + [DebuggerDisplay("Value={Value}, RequireQuotes={RequireQuotes}")] + internal class CommandStringToken : CommandToken + { + /// + /// Require quotes + /// + public bool RequireQuotes { get; } + + /// + /// Constructor + /// + /// Offset + /// Value + public CommandStringToken(int offset, string value) : base(CommandTokenType.String, offset) + { + Value = value; + RequireQuotes = value.IndexOfAny(new char[] { '\'', '"' }) != -1; + } + + /// + /// Parse command line spaces + /// + /// Command line + /// Index + /// Quote (could be null) + /// CommandSpaceToken + internal static CommandStringToken Parse(string commandLine, ref int index, CommandQuoteToken quote) + { + int end; + int offset = index; + + if (quote != null) + { + var ix = index; + + do + { + end = commandLine.IndexOf(quote.Value[0], ix + 1); + + if (end == -1) + { + throw new ArgumentException("String not closed"); + } + + if (IsScaped(commandLine, end - 1)) + { + ix = end; + end = -1; + } + } + while (end < 0); + } + else + { + end = commandLine.IndexOf(' ', index + 1); + } + + if (end == -1) + { + end = commandLine.Length; + } + + var ret = new CommandStringToken(offset, commandLine.Substring(index, end - index)); + index += end - index; + return ret; + } + + private static bool IsScaped(string commandLine, int index) + { + // TODO: Scape the scape + + return (commandLine[index] == '\\'); + } + } +} diff --git a/Neo.ConsoleService/CommandToken.cs b/Neo.ConsoleService/CommandToken.cs new file mode 100644 index 000000000..657cb1a1f --- /dev/null +++ b/Neo.ConsoleService/CommandToken.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Neo.ConsoleService +{ + internal abstract class CommandToken + { + /// + /// Offset + /// + public int Offset { get; } + + /// + /// Type + /// + public CommandTokenType Type { get; } + + /// + /// Value + /// + public string Value { get; protected set; } + + /// + /// Constructor + /// + /// Type + /// Offset + protected CommandToken(CommandTokenType type, int offset) + { + Type = type; + Offset = offset; + } + + /// + /// Parse command line + /// + /// Command line + /// + public static IEnumerable Parse(string commandLine) + { + CommandToken lastToken = null; + + for (int index = 0, count = commandLine.Length; index < count;) + { + switch (commandLine[index]) + { + case ' ': + { + lastToken = CommandSpaceToken.Parse(commandLine, ref index); + yield return lastToken; + break; + } + case '"': + case '\'': + { + if (lastToken is CommandQuoteToken quote) + { + // "'" + + if (quote.Value[0] != commandLine[index]) + { + goto default; + } + } + + lastToken = CommandQuoteToken.Parse(commandLine, ref index); + yield return lastToken; + break; + } + default: + { + lastToken = CommandStringToken.Parse(commandLine, ref index, + lastToken is CommandQuoteToken quote ? quote : null); + + yield return lastToken; + break; + } + } + } + } + + /// + /// Create string arguments + /// + /// Tokens + /// Remove escape + /// Arguments + public static string[] ToArguments(IEnumerable tokens, bool removeEscape = true) + { + var list = new List(); + + CommandToken lastToken = null; + + foreach (var token in tokens) + { + if (token is CommandStringToken str) + { + if (removeEscape && lastToken is CommandQuoteToken quote) + { + // Remove escape + + list.Add(str.Value.Replace("\\" + quote.Value, quote.Value)); + } + else + { + list.Add(str.Value); + } + } + + lastToken = token; + } + + return list.ToArray(); + } + + /// + /// Create a string from token list + /// + /// Tokens + /// String + public static string ToString(IEnumerable tokens) + { + var sb = new StringBuilder(); + + foreach (var token in tokens) + { + sb.Append(token.Value); + } + + return sb.ToString(); + } + + /// + /// Trim + /// + /// Args + public static void Trim(List args) + { + // Trim start + + while (args.Count > 0 && args[0].Type == CommandTokenType.Space) + { + args.RemoveAt(0); + } + + // Trim end + + while (args.Count > 0 && args[args.Count - 1].Type == CommandTokenType.Space) + { + args.RemoveAt(args.Count - 1); + } + } + + /// + /// Read String + /// + /// Args + /// Consume all if not quoted + /// String + public static string ReadString(List args, bool consumeAll) + { + Trim(args); + + var quoted = false; + + if (args.Count > 0 && args[0].Type == CommandTokenType.Quote) + { + quoted = true; + args.RemoveAt(0); + } + else + { + if (consumeAll) + { + // Return all if it's not quoted + + var ret = ToString(args); + args.Clear(); + + return ret; + } + } + + if (args.Count > 0) + { + switch (args[0]) + { + case CommandQuoteToken _: + { + if (quoted) + { + args.RemoveAt(0); + return ""; + } + + throw new ArgumentException(); + } + case CommandSpaceToken _: throw new ArgumentException(); + case CommandStringToken str: + { + args.RemoveAt(0); + + if (quoted && args.Count > 0 && args[0].Type == CommandTokenType.Quote) + { + // Remove last quote + + args.RemoveAt(0); + } + + return str.Value; + } + } + } + + return null; + } + } +} diff --git a/Neo.ConsoleService/CommandTokenType.cs b/Neo.ConsoleService/CommandTokenType.cs new file mode 100644 index 000000000..44f518f23 --- /dev/null +++ b/Neo.ConsoleService/CommandTokenType.cs @@ -0,0 +1,9 @@ +namespace Neo.ConsoleService +{ + internal enum CommandTokenType : byte + { + String, + Space, + Quote, + } +} diff --git a/Neo.ConsoleService/ConsoleCommandAttribute.cs b/Neo.ConsoleService/ConsoleCommandAttribute.cs new file mode 100644 index 000000000..57ebb91a4 --- /dev/null +++ b/Neo.ConsoleService/ConsoleCommandAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; +using System.Linq; + +namespace Neo.ConsoleService +{ + [DebuggerDisplay("Verbs={string.Join(' ',Verbs)}")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class ConsoleCommandAttribute : Attribute + { + /// + /// Verbs + /// + public string[] Verbs { get; } + + /// + /// Category + /// + public string Category { get; set; } + + /// + /// Description + /// + public string Description { get; set; } + + /// + /// Constructor + /// + /// Verbs + public ConsoleCommandAttribute(string verbs) + { + Verbs = verbs.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(u => u.ToLowerInvariant()).ToArray(); + } + } +} diff --git a/Neo.ConsoleService/ConsoleCommandMethod.cs b/Neo.ConsoleService/ConsoleCommandMethod.cs new file mode 100644 index 000000000..2c64eb82b --- /dev/null +++ b/Neo.ConsoleService/ConsoleCommandMethod.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace Neo.ConsoleService +{ + [DebuggerDisplay("Key={Key}")] + internal class ConsoleCommandMethod + { + /// + /// Verbs + /// + public string[] Verbs { get; } + + /// + /// Key + /// + public string Key => string.Join(' ', Verbs); + + /// + /// Help category + /// + public string HelpCategory { get; set; } + + /// + /// Help message + /// + public string HelpMessage { get; set; } + + /// + /// Instance + /// + public object Instance { get; } + + /// + /// Method + /// + public MethodInfo Method { get; } + + /// + /// Set instance command + /// + /// Instance + /// Method + /// Verbs + public ConsoleCommandMethod(object instance, MethodInfo method, ConsoleCommandAttribute attribute) + { + Method = method; + Instance = instance; + Verbs = attribute.Verbs; + HelpCategory = attribute.Category; + HelpMessage = attribute.Description; + } + + /// + /// Is this command + /// + /// Tokens + /// Consumed Arguments + /// True if is this command + public bool IsThisCommand(CommandToken[] tokens, out int consumedArgs) + { + int checks = Verbs.Length; + bool quoted = false; + var tokenList = new List(tokens); + + while (checks > 0 && tokenList.Count > 0) + { + switch (tokenList[0]) + { + case CommandSpaceToken _: + { + tokenList.RemoveAt(0); + break; + } + case CommandQuoteToken _: + { + quoted = !quoted; + tokenList.RemoveAt(0); + break; + } + case CommandStringToken str: + { + if (Verbs[^checks] != str.Value.ToLowerInvariant()) + { + consumedArgs = 0; + return false; + } + + checks--; + tokenList.RemoveAt(0); + break; + } + } + } + + if (quoted && tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Quote) + { + tokenList.RemoveAt(0); + } + + // Trim start + + while (tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Space) tokenList.RemoveAt(0); + + consumedArgs = tokens.Length - tokenList.Count; + return checks == 0; + } + } +} diff --git a/Neo.ConsoleService/ConsoleServiceBase.cs b/Neo.ConsoleService/ConsoleServiceBase.cs new file mode 100644 index 000000000..26d157ed0 --- /dev/null +++ b/Neo.ConsoleService/ConsoleServiceBase.cs @@ -0,0 +1,627 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.Loader; +using System.Security; +using System.ServiceProcess; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.ConsoleService +{ + public abstract class ConsoleServiceBase + { + protected virtual string Depends => null; + protected virtual string Prompt => "service"; + + public abstract string ServiceName { get; } + + protected bool ShowPrompt { get; set; } = true; + public bool ReadingPassword { get; set; } = false; + + private bool _running; + private readonly CancellationTokenSource _shutdownTokenSource = new CancellationTokenSource(); + private readonly CountdownEvent _shutdownAcknowledged = new CountdownEvent(1); + private readonly Dictionary> _verbs = new Dictionary>(); + private readonly Dictionary _instances = new Dictionary(); + private readonly Dictionary, bool, object>> _handlers = new Dictionary, bool, object>>(); + + private bool OnCommand(string commandLine) + { + if (string.IsNullOrEmpty(commandLine)) + { + return true; + } + + string possibleHelp = null; + var tokens = CommandToken.Parse(commandLine).ToArray(); + var commandArgs = CommandToken.Parse(commandLine).ToArray(); + var availableCommands = new List<(ConsoleCommandMethod Command, object[] Arguments)>(); + + foreach (var entries in _verbs.Values) + { + foreach (var command in entries) + { + if (command.IsThisCommand(commandArgs, out var consumedArgs)) + { + var arguments = new List(); + var args = commandArgs.Skip(consumedArgs).ToList(); + + CommandSpaceToken.Trim(args); + + try + { + var parameters = command.Method.GetParameters(); + + foreach (var arg in parameters) + { + // Parse argument + + if (TryProcessValue(arg.ParameterType, args, arg == parameters.Last(), out var value)) + { + arguments.Add(value); + } + else + { + if (arg.HasDefaultValue) + { + arguments.Add(arg.DefaultValue); + } + else + { + throw new ArgumentException(arg.Name); + } + } + } + + availableCommands.Add((command, arguments.ToArray())); + } + catch + { + // Skip parse errors + possibleHelp = command.Key; + } + } + } + } + + switch (availableCommands.Count) + { + case 0: + { + if (!string.IsNullOrEmpty(possibleHelp)) + { + OnHelpCommand(possibleHelp); + return true; + } + + return false; + } + case 1: + { + var (command, arguments) = availableCommands[0]; + command.Method.Invoke(command.Instance, arguments); + return true; + } + default: + { + // Show Ambiguous call + + throw new ArgumentException("Ambiguous calls for: " + string.Join(',', availableCommands.Select(u => u.Command.Key).Distinct())); + } + } + } + + private bool TryProcessValue(Type parameterType, List args, bool canConsumeAll, out object value) + { + if (args.Count > 0) + { + if (_handlers.TryGetValue(parameterType, out var handler)) + { + value = handler(args, canConsumeAll); + return true; + } + + if (parameterType.IsEnum) + { + var arg = CommandToken.ReadString(args, canConsumeAll); + value = Enum.Parse(parameterType, arg.Trim(), true); + return true; + } + } + + value = null; + return false; + } + + #region Commands + + /// + /// Process "help" command + /// + [ConsoleCommand("help", Category = "Base Commands")] + protected void OnHelpCommand(string key) + { + var withHelp = new List(); + + // Try to find a plugin with this name + + if (_instances.TryGetValue(key.Trim().ToLowerInvariant(), out var instance)) + { + // Filter only the help of this plugin + + key = ""; + foreach (var commands in _verbs.Values.Select(u => u)) + { + withHelp.AddRange + ( + commands.Where(u => !string.IsNullOrEmpty(u.HelpCategory) && u.Instance == instance) + ); + } + } + else + { + // Fetch commands + + foreach (var commands in _verbs.Values.Select(u => u)) + { + withHelp.AddRange(commands.Where(u => !string.IsNullOrEmpty(u.HelpCategory))); + } + } + + // Sort and show + + withHelp.Sort((a, b) => + { + var cate = a.HelpCategory.CompareTo(b.HelpCategory); + if (cate == 0) + { + cate = a.Key.CompareTo(b.Key); + } + return cate; + }); + + if (string.IsNullOrEmpty(key) || key.Equals("help", StringComparison.InvariantCultureIgnoreCase)) + { + string last = null; + foreach (var command in withHelp) + { + if (last != command.HelpCategory) + { + Console.WriteLine($"{command.HelpCategory}:"); + last = command.HelpCategory; + } + + Console.Write($"\t{command.Key}"); + Console.WriteLine(" " + string.Join(' ', + command.Method.GetParameters() + .Select(u => u.HasDefaultValue ? $"[{u.Name}={(u.DefaultValue == null ? "null" : u.DefaultValue.ToString())}]" : $"<{u.Name}>")) + ); + } + } + else + { + // Show help for this specific command + + string last = null; + string lastKey = null; + bool found = false; + + foreach (var command in withHelp.Where(u => u.Key == key)) + { + found = true; + + if (last != command.HelpMessage) + { + Console.WriteLine($"{command.HelpMessage}"); + last = command.HelpMessage; + } + + if (lastKey != command.Key) + { + Console.WriteLine($"You can call this command like this:"); + lastKey = command.Key; + } + + Console.Write($"\t{command.Key}"); + Console.WriteLine(" " + string.Join(' ', + command.Method.GetParameters() + .Select(u => u.HasDefaultValue ? $"[{u.Name}={u.DefaultValue?.ToString() ?? "null"}]" : $"<{u.Name}>")) + ); + } + + if (!found) + { + throw new ArgumentException($"Command not found."); + } + } + } + + /// + /// Process "clear" command + /// + [ConsoleCommand("clear", Category = "Base Commands", Description = "Clear is used in order to clean the console output.")] + protected void OnClear() + { + Console.Clear(); + } + + /// + /// Process "version" command + /// + [ConsoleCommand("version", Category = "Base Commands", Description = "Show the current version.")] + protected void OnVersion() + { + Console.WriteLine(Assembly.GetEntryAssembly().GetName().Version); + } + + /// + /// Process "exit" command + /// + [ConsoleCommand("exit", Category = "Base Commands", Description = "Exit the node.")] + protected void OnExit() + { + _running = false; + } + + #endregion + + public virtual void OnStart(string[] args) + { + // Register sigterm event handler + AssemblyLoadContext.Default.Unloading += SigTermEventHandler; + // Register sigint event handler + Console.CancelKeyPress += CancelHandler; + } + + public virtual void OnStop() + { + _shutdownAcknowledged.Signal(); + } + + public string ReadUserInput(string prompt, bool password = false) + { + const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + StringBuilder sb = new StringBuilder(); + ConsoleKeyInfo key; + + if (!string.IsNullOrEmpty(prompt)) + { + Console.Write(prompt + ": "); + } + + if (password) ReadingPassword = true; + var prevForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + + if (Console.IsInputRedirected) + { + // neo-gui Console require it + sb.Append(Console.ReadLine()); + } + else + { + do + { + key = Console.ReadKey(true); + + if (t.IndexOf(key.KeyChar) != -1) + { + sb.Append(key.KeyChar); + if (password) + { + Console.Write('*'); + } + else + { + Console.Write(key.KeyChar); + } + } + else if (key.Key == ConsoleKey.Backspace && sb.Length > 0) + { + sb.Length--; + Console.Write("\b \b"); + } + } while (key.Key != ConsoleKey.Enter); + } + + Console.ForegroundColor = prevForeground; + if (password) ReadingPassword = false; + Console.WriteLine(); + return sb.ToString(); + } + + public SecureString ReadSecureString(string prompt) + { + const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + SecureString securePwd = new SecureString(); + ConsoleKeyInfo key; + + if (!string.IsNullOrEmpty(prompt)) + { + Console.Write(prompt + ": "); + } + + ReadingPassword = true; + Console.ForegroundColor = ConsoleColor.Yellow; + + do + { + key = Console.ReadKey(true); + if (t.IndexOf(key.KeyChar) != -1) + { + securePwd.AppendChar(key.KeyChar); + Console.Write('*'); + } + else if (key.Key == ConsoleKey.Backspace && securePwd.Length > 0) + { + securePwd.RemoveAt(securePwd.Length - 1); + Console.Write(key.KeyChar); + Console.Write(' '); + Console.Write(key.KeyChar); + } + } while (key.Key != ConsoleKey.Enter); + + Console.ForegroundColor = ConsoleColor.White; + ReadingPassword = false; + Console.WriteLine(); + securePwd.MakeReadOnly(); + return securePwd; + } + + private void TriggerGracefulShutdown() + { + if (!_running) return; + _running = false; + _shutdownTokenSource.Cancel(); + // Wait for us to have triggered shutdown. + _shutdownAcknowledged.Wait(); + } + + private void SigTermEventHandler(AssemblyLoadContext obj) + { + TriggerGracefulShutdown(); + } + + private void CancelHandler(object sender, ConsoleCancelEventArgs e) + { + e.Cancel = true; + TriggerGracefulShutdown(); + } + + /// + /// Constructor + /// + protected ConsoleServiceBase() + { + // Register self commands + + RegisterCommandHander((args, canConsumeAll) => + { + return CommandToken.ReadString(args, canConsumeAll); + }); + + RegisterCommandHander((args, canConsumeAll) => + { + if (canConsumeAll) + { + var ret = CommandToken.ToString(args); + args.Clear(); + return ret.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + } + else + { + return CommandToken.ReadString(args, false).Split(',', ' '); + } + }); + + RegisterCommandHander(false, (str) => byte.Parse(str)); + RegisterCommandHander(false, (str) => str == "1" || str == "yes" || str == "y" || bool.Parse(str)); + RegisterCommandHander(false, (str) => ushort.Parse(str)); + RegisterCommandHander(false, (str) => uint.Parse(str)); + RegisterCommandHander(false, (str) => IPAddress.Parse(str)); + } + + /// + /// Register command handler + /// + /// Return type + /// Handler + private void RegisterCommandHander(Func, bool, object> handler) + { + _handlers[typeof(TRet)] = handler; + } + + /// + /// Register command handler + /// + /// Base type + /// Return type + /// Can consume all + /// Handler + public void RegisterCommandHander(bool canConsumeAll, Func handler) + { + _handlers[typeof(TRet)] = (args, cosumeAll) => + { + var value = (T)_handlers[typeof(T)](args, canConsumeAll); + return handler(value); + }; + } + + /// + /// Register command handler + /// + /// Base type + /// Return type + /// Handler + public void RegisterCommandHander(Func handler) + { + _handlers[typeof(TRet)] = (args, cosumeAll) => + { + var value = (T)_handlers[typeof(T)](args, cosumeAll); + return handler(value); + }; + } + + /// + /// Register commands + /// + /// Instance + /// Name + public void RegisterCommand(object instance, string name = null) + { + if (!string.IsNullOrEmpty(name)) + { + _instances.Add(name, instance); + } + + foreach (var method in instance.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + foreach (var attribute in method.GetCustomAttributes()) + { + // Check handlers + + if (!method.GetParameters().All(u => u.ParameterType.IsEnum || _handlers.ContainsKey(u.ParameterType))) + { + throw new ArgumentException("Handler not found for the command: " + method.ToString()); + } + + // Add command + + var command = new ConsoleCommandMethod(instance, method, attribute); + + if (!_verbs.TryGetValue(command.Key, out var commands)) + { + _verbs.Add(command.Key, new List(new[] { command })); + } + else + { + commands.Add(command); + } + } + } + } + + public void Run(string[] args) + { + if (Environment.UserInteractive) + { + if (args.Length > 0 && args[0] == "/install") + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + Console.WriteLine("Only support for installing services on Windows."); + return; + } + string arguments = string.Format("create {0} start= auto binPath= \"{1}\"", ServiceName, Process.GetCurrentProcess().MainModule.FileName); + if (!string.IsNullOrEmpty(Depends)) + { + arguments += string.Format(" depend= {0}", Depends); + } + Process process = Process.Start(new ProcessStartInfo + { + Arguments = arguments, + FileName = Path.Combine(Environment.SystemDirectory, "sc.exe"), + RedirectStandardOutput = true, + UseShellExecute = false + }); + process.WaitForExit(); + Console.Write(process.StandardOutput.ReadToEnd()); + } + else if (args.Length > 0 && args[0] == "/uninstall") + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + Console.WriteLine("Only support for installing services on Windows."); + return; + } + Process process = Process.Start(new ProcessStartInfo + { + Arguments = string.Format("delete {0}", ServiceName), + FileName = Path.Combine(Environment.SystemDirectory, "sc.exe"), + RedirectStandardOutput = true, + UseShellExecute = false + }); + process.WaitForExit(); + Console.Write(process.StandardOutput.ReadToEnd()); + } + else + { + OnStart(args); + RunConsole(); + OnStop(); + } + } + else + { + ServiceBase.Run(new ServiceProxy(this)); + } + } + + protected string ReadLine() + { + Task readLineTask = Task.Run(() => Console.ReadLine()); + + try + { + readLineTask.Wait(_shutdownTokenSource.Token); + } + catch (OperationCanceledException) + { + return null; + } + + return readLineTask.Result; + } + + public virtual void RunConsole() + { + _running = true; + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + try + { + Console.Title = ServiceName; + } + catch { } + + Console.ForegroundColor = ConsoleColor.DarkGreen; + + while (_running) + { + if (ShowPrompt) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.Write($"{Prompt}> "); + } + + Console.ForegroundColor = ConsoleColor.Yellow; + string line = ReadLine()?.Trim(); + if (line == null) break; + Console.ForegroundColor = ConsoleColor.White; + + try + { + if (!OnCommand(line)) + { + Console.WriteLine("error: Command not found"); + } + } + catch (TargetInvocationException ex) + { + Console.WriteLine($"error: {ex.InnerException.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"error: {ex.Message}"); + } + } + + Console.ResetColor(); + } + } +} diff --git a/Neo.ConsoleService/Neo.ConsoleService.csproj b/Neo.ConsoleService/Neo.ConsoleService.csproj new file mode 100644 index 000000000..a1df1fc69 --- /dev/null +++ b/Neo.ConsoleService/Neo.ConsoleService.csproj @@ -0,0 +1,19 @@ + + + + 2015-2020 The Neo Project + 1.0.0 + The Neo Project + netstandard2.1 + https://github.com/neo-project/neo-node + MIT + git + https://github.com/neo-project/neo-node.git + + + + + + + + diff --git a/Neo.ConsoleService/Properties/AssemblyInfo.cs b/Neo.ConsoleService/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8e519c188 --- /dev/null +++ b/Neo.ConsoleService/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Neo.ConsoleService.Tests")] diff --git a/neo-cli/Services/ServiceProxy.cs b/Neo.ConsoleService/ServiceProxy.cs similarity index 83% rename from neo-cli/Services/ServiceProxy.cs rename to Neo.ConsoleService/ServiceProxy.cs index 47e36f142..a05cfa159 100644 --- a/neo-cli/Services/ServiceProxy.cs +++ b/Neo.ConsoleService/ServiceProxy.cs @@ -1,10 +1,10 @@ using System.ServiceProcess; -namespace Neo.Services +namespace Neo.ConsoleService { internal class ServiceProxy : ServiceBase { - private ConsoleServiceBase service; + private readonly ConsoleServiceBase service; public ServiceProxy(ConsoleServiceBase service) { diff --git a/neo-cli/CLI/MainService.Blockchain.cs b/neo-cli/CLI/MainService.Blockchain.cs new file mode 100644 index 000000000..10b323a4a --- /dev/null +++ b/neo-cli/CLI/MainService.Blockchain.cs @@ -0,0 +1,34 @@ +using Neo.ConsoleService; +using Neo.Ledger; +using System; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "export blocks" command + /// + /// Start + /// Number of blocks + /// Path + [ConsoleCommand("export blocks", Category = "Blockchain Commands")] + private void OnExportBlocksStartCountCommand(uint start, uint count = uint.MaxValue, string path = null) + { + if (Blockchain.Singleton.Height < start) + { + Console.WriteLine("error: invalid start height."); + return; + } + + count = Math.Min(count, Blockchain.Singleton.Height - start + 1); + + if (string.IsNullOrEmpty(path)) + { + path = $"chain.{start}.acc"; + } + + WriteBlocks(start, count, path, true); + } + } +} diff --git a/neo-cli/CLI/MainService.Consensus.cs b/neo-cli/CLI/MainService.Consensus.cs new file mode 100644 index 000000000..6785deeac --- /dev/null +++ b/neo-cli/CLI/MainService.Consensus.cs @@ -0,0 +1,18 @@ +using Neo.ConsoleService; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "start consensus" command + /// + [ConsoleCommand("start consensus", Category = "Consensus Commands")] + private void OnStartConsensusCommand() + { + if (NoWallet()) return; + ShowPrompt = false; + NeoSystem.StartConsensus(CurrentWallet); + } + } +} diff --git a/neo-cli/CLI/MainService.Contracts.cs b/neo-cli/CLI/MainService.Contracts.cs new file mode 100644 index 000000000..f3f4f3d56 --- /dev/null +++ b/neo-cli/CLI/MainService.Contracts.cs @@ -0,0 +1,133 @@ +using Neo.ConsoleService; +using Neo.IO.Json; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.VM; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "deploy" command + /// + /// File path + /// Manifest path + [ConsoleCommand("deploy", Category = "Contract Commands")] + private void OnDeployCommand(string filePath, string manifestPath = null) + { + if (NoWallet()) return; + byte[] script = LoadDeploymentScript(filePath, manifestPath, out var scriptHash); + + Transaction tx; + try + { + tx = CurrentWallet.MakeTransaction(script); + } + catch (InvalidOperationException) + { + Console.WriteLine("Engine faulted."); + return; + } + Console.WriteLine($"Script hash: {scriptHash.ToString()}"); + Console.WriteLine($"Gas: {new BigDecimal(tx.SystemFee, NativeContract.GAS.Decimals)}"); + Console.WriteLine(); + SignAndSendTx(tx); + } + + /// + /// Process "invoke" command + /// + /// Script hash + /// Operation + /// Contract parameters + /// Witness address + [ConsoleCommand("invoke", Category = "Contract Commands")] + private void OnInvokeCommand(UInt160 scriptHash, string operation, JArray contractParameters = null, UInt160[] witnessAddress = null) + { + List parameters = new List(); + List signCollection = new List(); + + if (!NoWallet() && witnessAddress != null) + { + using (SnapshotView snapshot = Blockchain.Singleton.GetSnapshot()) + { + UInt160[] accounts = CurrentWallet.GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash).Where(p => NativeContract.GAS.BalanceOf(snapshot, p).Sign > 0).ToArray(); + foreach (var signAccount in accounts) + { + if (witnessAddress is null) + { + break; + } + foreach (var witness in witnessAddress) + { + if (witness.Equals(signAccount)) + { + signCollection.Add(new Cosigner() { Account = signAccount }); + break; + } + } + } + } + } + + if (contractParameters != null) + { + foreach (var contractParameter in contractParameters) + { + parameters.Add(ContractParameter.FromJson(contractParameter)); + } + } + + Transaction tx = new Transaction + { + Sender = UInt160.Zero, + Attributes = Array.Empty(), + Witnesses = Array.Empty(), + Cosigners = signCollection.ToArray() + }; + + using (ScriptBuilder scriptBuilder = new ScriptBuilder()) + { + scriptBuilder.EmitAppCall(scriptHash, operation, parameters.ToArray()); + tx.Script = scriptBuilder.ToArray(); + Console.WriteLine($"Invoking script with: '{tx.Script.ToHexString()}'"); + } + + using (ApplicationEngine engine = ApplicationEngine.Run(tx.Script, tx, testMode: true)) + { + Console.WriteLine($"VM State: {engine.State}"); + Console.WriteLine($"Gas Consumed: {new BigDecimal(engine.GasConsumed, NativeContract.GAS.Decimals)}"); + Console.WriteLine($"Evaluation Stack: {new JArray(engine.ResultStack.Select(p => p.ToParameter().ToJson()))}"); + Console.WriteLine(); + if (engine.State.HasFlag(VMState.FAULT)) + { + Console.WriteLine("Engine faulted."); + return; + } + } + + if (NoWallet()) return; + try + { + tx = CurrentWallet.MakeTransaction(tx.Script, null, tx.Attributes, tx.Cosigners); + } + catch (InvalidOperationException) + { + Console.WriteLine("Error: insufficient balance."); + return; + } + if (!ReadUserInput("relay tx(no|yes)").IsYes()) + { + return; + } + SignAndSendTx(tx); + } + } +} diff --git a/neo-cli/CLI/MainService.Network.cs b/neo-cli/CLI/MainService.Network.cs new file mode 100644 index 000000000..b26f41ecf --- /dev/null +++ b/neo-cli/CLI/MainService.Network.cs @@ -0,0 +1,153 @@ +using Akka.Actor; +using Neo.ConsoleService; +using Neo.IO; +using Neo.IO.Json; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Capabilities; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using System; +using System.Net; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "broadcast addr" command + /// + /// Payload + /// Port + [ConsoleCommand("broadcast addr", Category = "Network Commands")] + private void OnBroadcastAddressCommand(IPAddress payload, ushort port) + { + if (payload == null) + { + Console.WriteLine("You must input the payload to relay."); + return; + } + + OnBroadcastCommand(MessageCommand.Addr, + AddrPayload.Create( + NetworkAddressWithTime.Create( + payload, DateTime.UtcNow.ToTimestamp(), + new FullNodeCapability(), + new ServerCapability(NodeCapabilityType.TcpServer, port)) + )); + } + + /// + /// Process "broadcast block" command + /// + /// Hash + [ConsoleCommand("broadcast block", Category = "Network Commands")] + private void OnBroadcastGetBlocksByHashCommand(UInt256 hash) + { + OnBroadcastCommand(MessageCommand.Block, Blockchain.Singleton.GetBlock(hash)); + } + + /// + /// Process "broadcast block" command + /// + /// Block index + [ConsoleCommand("broadcast block", Category = "Network Commands")] + private void OnBroadcastGetBlocksByHeightCommand(uint height) + { + OnBroadcastCommand(MessageCommand.Block, Blockchain.Singleton.GetBlock(height)); + } + + /// + /// Process "broadcast getblocks" command + /// + /// Hash + [ConsoleCommand("broadcast getblocks", Category = "Network Commands")] + private void OnBroadcastGetBlocksCommand(UInt256 hash) + { + OnBroadcastCommand(MessageCommand.GetBlocks, GetBlocksPayload.Create(hash)); + } + + /// + /// Process "broadcast getheaders" command + /// + /// Hash + [ConsoleCommand("broadcast getheaders", Category = "Network Commands")] + private void OnBroadcastGetHeadersCommand(UInt256 hash) + { + OnBroadcastCommand(MessageCommand.GetHeaders, GetBlocksPayload.Create(hash)); + } + + /// + /// Process "broadcast getdata" command + /// + /// Type + /// Payload + [ConsoleCommand("broadcast getdata", Category = "Network Commands")] + private void OnBroadcastGetDataCommand(InventoryType type, UInt256[] payload) + { + OnBroadcastCommand(MessageCommand.GetData, InvPayload.Create(type, payload)); + } + + /// + /// Process "broadcast inv" command + /// + /// Type + /// Payload + [ConsoleCommand("broadcast inv", Category = "Network Commands")] + private void OnBroadcastInvCommand(InventoryType type, UInt256[] payload) + { + OnBroadcastCommand(MessageCommand.Inv, InvPayload.Create(type, payload)); + } + + /// + /// Process "broadcast transaction" command + /// + /// Hash + [ConsoleCommand("broadcast transaction", Category = "Network Commands")] + private void OnBroadcastTransactionCommand(UInt256 hash) + { + OnBroadcastCommand(MessageCommand.Transaction, Blockchain.Singleton.GetTransaction(hash)); + } + + private void OnBroadcastCommand(MessageCommand command, ISerializable ret) + { + NeoSystem.LocalNode.Tell(Message.Create(command, ret)); + } + + /// + /// Process "relay" command + /// + /// Json object + [ConsoleCommand("relay", Category = "Network Commands")] + private void OnRelayCommand(JObject jsonObjectToRelay) + { + if (jsonObjectToRelay == null) + { + Console.WriteLine("You must input JSON object to relay."); + return; + } + + try + { + ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToRelay.ToString()); + if (!context.Completed) + { + Console.WriteLine("The signature is incomplete."); + return; + } + if (!(context.Verifiable is Transaction tx)) + { + Console.WriteLine($"Only support to relay transaction."); + return; + } + tx.Witnesses = context.GetWitnesses(); + NeoSystem.LocalNode.Tell(new LocalNode.Relay { Inventory = tx }); + Console.WriteLine($"Data relay success, the hash is shown as follows:{Environment.NewLine}{tx.Hash}"); + } + catch (Exception e) + { + Console.WriteLine($"One or more errors occurred:{Environment.NewLine}{e.Message}"); + } + } + } +} diff --git a/neo-cli/CLI/MainService.Node.cs b/neo-cli/CLI/MainService.Node.cs new file mode 100644 index 000000000..dc892d0d5 --- /dev/null +++ b/neo-cli/CLI/MainService.Node.cs @@ -0,0 +1,90 @@ +using Akka.Actor; +using Neo.ConsoleService; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "show pool" command + /// + [ConsoleCommand("show pool", Category = "Node Commands", Description = "Show the current state of the mempool")] + private void OnShowPoolCommand(bool verbose = false) + { + if (verbose) + { + Blockchain.Singleton.MemPool.GetVerifiedAndUnverifiedTransactions( + out IEnumerable verifiedTransactions, + out IEnumerable unverifiedTransactions); + Console.WriteLine("Verified Transactions:"); + foreach (Transaction tx in verifiedTransactions) + Console.WriteLine($" {tx.Hash} {tx.GetType().Name} {tx.NetworkFee} GAS_NetFee"); + Console.WriteLine("Unverified Transactions:"); + foreach (Transaction tx in unverifiedTransactions) + Console.WriteLine($" {tx.Hash} {tx.GetType().Name} {tx.NetworkFee} GAS_NetFee"); + } + Console.WriteLine($"total: {Blockchain.Singleton.MemPool.Count}, verified: {Blockchain.Singleton.MemPool.VerifiedCount}, unverified: {Blockchain.Singleton.MemPool.UnVerifiedCount}"); + } + + /// + /// Process "show state" command + /// + [ConsoleCommand("show state", Category = "Node Commands", Description = "Show the current state of the node")] + private void OnShowStateCommand() + { + var cancel = new CancellationTokenSource(); + + Console.CursorVisible = false; + Console.Clear(); + Task broadcast = Task.Run(async () => + { + while (!cancel.Token.IsCancellationRequested) + { + NeoSystem.LocalNode.Tell(Message.Create(MessageCommand.Ping, PingPayload.Create(Blockchain.Singleton.Height))); + await Task.Delay(Blockchain.TimePerBlock, cancel.Token); + } + }); + Task task = Task.Run(async () => + { + int maxLines = 0; + + while (!cancel.Token.IsCancellationRequested) + { + Console.SetCursorPosition(0, 0); + WriteLineWithoutFlicker($"block: {Blockchain.Singleton.Height}/{Blockchain.Singleton.HeaderHeight} connected: {LocalNode.Singleton.ConnectedCount} unconnected: {LocalNode.Singleton.UnconnectedCount}", Console.WindowWidth - 1); + + int linesWritten = 1; + foreach (RemoteNode node in LocalNode.Singleton.GetRemoteNodes().OrderByDescending(u => u.LastBlockIndex).Take(Console.WindowHeight - 2).ToArray()) + { + Console.WriteLine( + $" ip: {node.Remote.Address.ToString().PadRight(15)}\tport: {node.Remote.Port.ToString().PadRight(5)}\tlisten: {node.ListenerTcpPort.ToString().PadRight(5)}\theight: {node.LastBlockIndex.ToString().PadRight(7)}"); + linesWritten++; + } + + maxLines = Math.Max(maxLines, linesWritten); + + while (linesWritten < maxLines) + { + WriteLineWithoutFlicker("", Console.WindowWidth - 1); + maxLines--; + } + + await Task.Delay(500, cancel.Token); + } + }); + ReadLine(); + cancel.Cancel(); + try { Task.WaitAll(task, broadcast); } catch { } + Console.WriteLine(); + Console.CursorVisible = true; + } + } +} diff --git a/neo-cli/CLI/MainService.Plugins.cs b/neo-cli/CLI/MainService.Plugins.cs new file mode 100644 index 000000000..aee450600 --- /dev/null +++ b/neo-cli/CLI/MainService.Plugins.cs @@ -0,0 +1,110 @@ +using Neo.ConsoleService; +using Neo.Plugins; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "install" command + /// + /// Plugin name + [ConsoleCommand("install", Category = "Plugin Commands")] + private void OnInstallCommand(string pluginName) + { + bool isTemp; + string fileName; + + if (!File.Exists(pluginName)) + { + if (string.IsNullOrEmpty(Settings.Default.PluginURL)) + { + Console.WriteLine("You must define `PluginURL` in your `config.json`"); + return; + } + + var address = string.Format(Settings.Default.PluginURL, pluginName, typeof(Plugin).Assembly.GetVersion()); + fileName = Path.Combine(Path.GetTempPath(), $"{pluginName}.zip"); + isTemp = true; + + Console.WriteLine($"Downloading from {address}"); + using (WebClient wc = new WebClient()) + { + wc.DownloadFile(address, fileName); + } + } + else + { + fileName = pluginName; + isTemp = false; + } + + try + { + ZipFile.ExtractToDirectory(fileName, "."); + } + catch (IOException) + { + Console.WriteLine($"Plugin already exist."); + return; + } + finally + { + if (isTemp) + { + File.Delete(fileName); + } + } + + Console.WriteLine($"Install successful, please restart neo-cli."); + } + + /// + /// Process "uninstall" command + /// + /// Plugin name + [ConsoleCommand("uninstall", Category = "Plugin Commands")] + private void OnUnInstallCommand(string pluginName) + { + var plugin = Plugin.Plugins.FirstOrDefault(p => p.Name == pluginName); + if (plugin is null) + { + Console.WriteLine("Plugin not found"); + return; + } + + File.Delete(plugin.Path); + File.Delete(plugin.ConfigFile); + try + { + Directory.Delete(Path.GetDirectoryName(plugin.ConfigFile), false); + } + catch (IOException) + { + } + Console.WriteLine($"Uninstall successful, please restart neo-cli."); + } + + /// + /// Process "plugins" command + /// + [ConsoleCommand("plugins", Category = "Plugin Commands")] + private void OnPluginsCommand() + { + if (Plugin.Plugins.Count > 0) + { + Console.WriteLine("Loaded plugins:"); + Plugin.Plugins.ForEach(p => Console.WriteLine("\t" + p.Name)); + } + else + { + Console.WriteLine("No loaded plugins"); + } + } + } +} diff --git a/neo-cli/CLI/MainService.Wallet.cs b/neo-cli/CLI/MainService.Wallet.cs new file mode 100644 index 000000000..a2d114118 --- /dev/null +++ b/neo-cli/CLI/MainService.Wallet.cs @@ -0,0 +1,488 @@ +using Akka.Actor; +using Neo.ConsoleService; +using Neo.Cryptography.ECC; +using Neo.IO.Json; +using Neo.Ledger; +using Neo.Network.P2P; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.Wallets; +using Neo.Wallets.NEP6; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace Neo.CLI +{ + partial class MainService + { + /// + /// Process "open wallet" command + /// + /// Path + [ConsoleCommand("open wallet", Category = "Wallet Commands")] + private void OnOpenWallet(string path) + { + if (!File.Exists(path)) + { + Console.WriteLine($"File does not exist"); + return; + } + string password = ReadUserInput("password", true); + if (password.Length == 0) + { + Console.WriteLine("cancelled"); + return; + } + try + { + OpenWallet(path, password); + } + catch (System.Security.Cryptography.CryptographicException) + { + Console.WriteLine($"failed to open file \"{path}\""); + } + } + + /// + /// Process "close wallet" command + /// + [ConsoleCommand("close wallet", Category = "Wallet Commands")] + private void OnCloseWalletCommand() + { + if (CurrentWallet == null) + { + Console.WriteLine($"Wallet is not opened"); + return; + } + CurrentWallet = null; + Console.WriteLine($"Wallet is closed"); + } + + /// + /// Process "upgrade wallet" command + /// + [ConsoleCommand("upgrade wallet", Category = "Wallet Commands")] + private void OnUpgradeWalletCommand(string path) + { + if (Path.GetExtension(path).ToLowerInvariant() != ".db3") + { + Console.WriteLine("Can't upgrade the wallet file."); + return; + } + if (!File.Exists(path)) + { + Console.WriteLine("File does not exist."); + return; + } + string password = ReadUserInput("password", true); + if (password.Length == 0) + { + Console.WriteLine("cancelled"); + return; + } + string path_new = Path.ChangeExtension(path, ".json"); + if (File.Exists(path_new)) + { + Console.WriteLine($"File '{path_new}' already exists"); + return; + } + NEP6Wallet.Migrate(path_new, path, password).Save(); + Console.WriteLine($"Wallet file upgrade complete. New wallet file has been auto-saved at: {path_new}"); + } + + /// + /// Process "create address" command + /// + /// Count + [ConsoleCommand("create address", Category = "Wallet Commands")] + private void OnCreateAddressCommand(ushort count = 1) + { + if (NoWallet()) return; + + string path = "address.txt"; + if (File.Exists(path)) + { + if (!ReadUserInput($"The file '{path}' already exists, do you want to overwrite it? (yes|no)", false).IsYes()) + { + return; + } + } + + List addresses = new List(); + using (var percent = new ConsolePercent(0, count)) + { + Parallel.For(0, count, (i) => + { + WalletAccount account = CurrentWallet.CreateAccount(); + lock (addresses) + { + addresses.Add(account.Address); + percent.Value++; + } + }); + } + + if (CurrentWallet is NEP6Wallet wallet) + wallet.Save(); + + Console.WriteLine($"export addresses to {path}"); + File.WriteAllLines(path, addresses); + } + + /// + /// Process "export key" command + /// + /// Path + /// ScriptHash + [ConsoleCommand("export key", Category = "Wallet Commands")] + private void OnExportKeyCommand(string path = null, UInt160 scriptHash = null) + { + if (NoWallet()) return; + if (path != null && File.Exists(path)) + { + Console.WriteLine($"Error: File '{path}' already exists"); + return; + } + string password = ReadUserInput("password", true); + if (password.Length == 0) + { + Console.WriteLine("cancelled"); + return; + } + if (!CurrentWallet.VerifyPassword(password)) + { + Console.WriteLine("Incorrect password"); + return; + } + IEnumerable keys; + if (scriptHash == null) + keys = CurrentWallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey()); + else + keys = new[] { CurrentWallet.GetAccount(scriptHash).GetKey() }; + if (path == null) + foreach (KeyPair key in keys) + Console.WriteLine(key.Export()); + else + File.WriteAllLines(path, keys.Select(p => p.Export())); + } + + /// + /// Process "create wallet" command + /// + [ConsoleCommand("create wallet", Category = "Wallet Commands")] + private void OnCreateWalletCommand(string path) + { + string password = ReadUserInput("password", true); + if (password.Length == 0) + { + Console.WriteLine("cancelled"); + return; + } + string password2 = ReadUserInput("password", true); + if (password != password2) + { + Console.WriteLine("error"); + return; + } + if (!File.Exists(path)) + { + CreateWallet(path, password); + } + else + { + Console.WriteLine("This wallet already exists, please create another one."); + } + } + + /// + /// Process "import multisigaddress" command + /// + /// Required signatures + /// Public keys + [ConsoleCommand("import multisigaddress", Category = "Wallet Commands")] + private void OnImportMultisigAddress(ushort m, ECPoint[] publicKeys) + { + if (NoWallet()) return; + + int n = publicKeys.Length; + + if (m < 1 || m > n || n > 1024) + { + Console.WriteLine("Error. Invalid parameters."); + return; + } + + Contract multiSignContract = Contract.CreateMultiSigContract(m, publicKeys); + KeyPair keyPair = CurrentWallet.GetAccounts().FirstOrDefault(p => p.HasKey && publicKeys.Contains(p.GetKey().PublicKey))?.GetKey(); + + WalletAccount account = CurrentWallet.CreateAccount(multiSignContract, keyPair); + if (CurrentWallet is NEP6Wallet wallet) + wallet.Save(); + + Console.WriteLine("Multisig. Addr.: " + multiSignContract.Address); + } + + /// + /// Process "import key" command + /// + [ConsoleCommand("import key", Category = "Wallet Commands")] + private void OnImportKeyCommand(string wifOrFile) + { + byte[] prikey = null; + try + { + prikey = Wallet.GetPrivateKeyFromWIF(wifOrFile); + } + catch (FormatException) { } + if (prikey == null) + { + var fileInfo = new FileInfo(wifOrFile); + + if (!fileInfo.Exists) + { + Console.WriteLine($"Error: File '{fileInfo.FullName}' doesn't exists"); + return; + } + + if (wifOrFile.Length > 1024 * 1024) + { + if (!ReadUserInput($"The file '{fileInfo.FullName}' is too big, do you want to continue? (yes|no)", false).IsYes()) + { + return; + } + } + + string[] lines = File.ReadAllLines(fileInfo.FullName).Where(u => !string.IsNullOrEmpty(u)).ToArray(); + using (var percent = new ConsolePercent(0, lines.Length)) + { + for (int i = 0; i < lines.Length; i++) + { + if (lines[i].Length == 64) + prikey = lines[i].HexToBytes(); + else + prikey = Wallet.GetPrivateKeyFromWIF(lines[i]); + CurrentWallet.CreateAccount(prikey); + Array.Clear(prikey, 0, prikey.Length); + percent.Value++; + } + } + } + else + { + WalletAccount account = CurrentWallet.CreateAccount(prikey); + Array.Clear(prikey, 0, prikey.Length); + Console.WriteLine($"address: {account.Address}"); + Console.WriteLine($" pubkey: {account.GetKey().PublicKey.EncodePoint(true).ToHexString()}"); + } + if (CurrentWallet is NEP6Wallet wallet) + wallet.Save(); + } + + /// + /// Process "list address" command + /// + [ConsoleCommand("list address", Category = "Wallet Commands")] + private void OnListAddressCommand() + { + if (NoWallet()) return; + + using (var snapshot = Blockchain.Singleton.GetSnapshot()) + { + foreach (Contract contract in CurrentWallet.GetAccounts().Where(p => !p.WatchOnly).Select(p => p.Contract)) + { + var type = "Nonstandard"; + + if (contract.Script.IsMultiSigContract()) + { + type = "MultiSignature"; + } + else if (contract.Script.IsSignatureContract()) + { + type = "Standard"; + } + else if (snapshot.Contracts.TryGet(contract.ScriptHash) != null) + { + type = "Deployed-Nonstandard"; + } + + Console.WriteLine($"{contract.Address}\t{type}"); + } + } + } + + /// + /// Process "list asset" command + /// + [ConsoleCommand("list asset", Category = "Wallet Commands")] + private void OnListAssetCommand() + { + if (NoWallet()) return; + foreach (UInt160 account in CurrentWallet.GetAccounts().Select(p => p.ScriptHash)) + { + Console.WriteLine(account.ToAddress()); + Console.WriteLine($"NEO: {CurrentWallet.GetBalance(NativeContract.NEO.Hash, account)}"); + Console.WriteLine($"GAS: {CurrentWallet.GetBalance(NativeContract.GAS.Hash, account)}"); + Console.WriteLine(); + } + Console.WriteLine("----------------------------------------------------"); + Console.WriteLine("Total: " + "NEO: " + CurrentWallet.GetAvailable(NativeContract.NEO.Hash) + " GAS: " + CurrentWallet.GetAvailable(NativeContract.GAS.Hash)); + Console.WriteLine(); + Console.WriteLine("NEO hash: " + NativeContract.NEO.Hash); + Console.WriteLine("GAS hash: " + NativeContract.GAS.Hash); + } + + /// + /// Process "list key" command + /// + [ConsoleCommand("list key", Category = "Wallet Commands")] + private void OnListKeyCommand() + { + if (NoWallet()) return; + foreach (KeyPair key in CurrentWallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey())) + { + Console.WriteLine(key.PublicKey); + } + } + + /// + /// Process "sign" command + /// + /// Json object to sign + [ConsoleCommand("sign", Category = "Wallet Commands")] + private void OnSignCommand(JObject jsonObjectToSign) + { + if (NoWallet()) return; + + if (jsonObjectToSign == null) + { + Console.WriteLine("You must input JSON object pending signature data."); + return; + } + try + { + ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToSign.ToString()); + if (!CurrentWallet.Sign(context)) + { + Console.WriteLine("The private key that can sign the data is not found."); + return; + } + Console.WriteLine($"Signed Output:{Environment.NewLine}{context}"); + } + catch (Exception e) + { + Console.WriteLine($"One or more errors occurred:{Environment.NewLine}{e.Message}"); + } + } + + /// + /// Process "send" command + /// + /// Asset id + /// To + /// Amount + [ConsoleCommand("send", Category = "Wallet Commands")] + private void OnSendCommand(UInt160 asset, UInt160 to, string amount) + { + if (NoWallet()) return; + string password = ReadUserInput("password", true); + if (password.Length == 0) + { + Console.WriteLine("cancelled"); + return; + } + if (!CurrentWallet.VerifyPassword(password)) + { + Console.WriteLine("Incorrect password"); + return; + } + + Transaction tx; + AssetDescriptor descriptor = new AssetDescriptor(asset); + if (!BigDecimal.TryParse(amount, descriptor.Decimals, out BigDecimal decimalAmount) || decimalAmount.Sign <= 0) + { + Console.WriteLine("Incorrect Amount Format"); + return; + } + tx = CurrentWallet.MakeTransaction(new[] + { + new TransferOutput + { + AssetId = asset, + Value = decimalAmount, + ScriptHash = to + } + }); + + if (tx == null) + { + Console.WriteLine("Insufficient funds"); + return; + } + + ContractParametersContext context = new ContractParametersContext(tx); + CurrentWallet.Sign(context); + if (context.Completed) + { + tx.Witnesses = context.GetWitnesses(); + NeoSystem.LocalNode.Tell(new LocalNode.Relay { Inventory = tx }); + Console.WriteLine($"TXID: {tx.Hash}"); + } + else + { + Console.WriteLine("SignatureContext:"); + Console.WriteLine(context.ToString()); + } + } + + /// + /// Process "show gas" command + /// + [ConsoleCommand("show gas", Category = "Wallet Commands")] + private void OnShowGasCommand() + { + if (NoWallet()) return; + BigInteger gas = BigInteger.Zero; + using (SnapshotView snapshot = Blockchain.Singleton.GetSnapshot()) + foreach (UInt160 account in CurrentWallet.GetAccounts().Select(p => p.ScriptHash)) + { + gas += NativeContract.NEO.UnclaimedGas(snapshot, account, snapshot.Height + 1); + } + Console.WriteLine($"unclaimed gas: {new BigDecimal(gas, NativeContract.GAS.Decimals)}"); + } + + private void SignAndSendTx(Transaction tx) + { + ContractParametersContext context; + try + { + context = new ContractParametersContext(tx); + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"Error creating contract params: {ex}"); + throw; + } + CurrentWallet.Sign(context); + string msg; + if (context.Completed) + { + tx.Witnesses = context.GetWitnesses(); + + NeoSystem.LocalNode.Tell(new LocalNode.Relay { Inventory = tx }); + + msg = $"Signed and relayed transaction with hash={tx.Hash}"; + Console.WriteLine(msg); + return; + } + + msg = $"Failed sending transaction with hash={tx.Hash}"; + Console.WriteLine(msg); + } + } +} diff --git a/neo-cli/CLI/MainService.cs b/neo-cli/CLI/MainService.cs index 8f857f7be..f543dba16 100644 --- a/neo-cli/CLI/MainService.cs +++ b/neo-cli/CLI/MainService.cs @@ -1,18 +1,15 @@ using Akka.Actor; using Microsoft.Extensions.Configuration; -using Neo.Consensus; +using Neo.ConsoleService; +using Neo.Cryptography.ECC; using Neo.IO; using Neo.IO.Json; using Neo.Ledger; using Neo.Network.P2P; -using Neo.Network.P2P.Capabilities; using Neo.Network.P2P.Payloads; -using Neo.Persistence; using Neo.Plugins; -using Neo.Services; using Neo.SmartContract; using Neo.SmartContract.Manifest; -using Neo.SmartContract.Native; using Neo.VM; using Neo.Wallets; using Neo.Wallets.NEP6; @@ -23,18 +20,14 @@ using System.IO.Compression; using System.Linq; using System.Net; -using System.Numerics; -using System.Security.Cryptography; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; -using System.Threading.Tasks; -using ECCurve = Neo.Cryptography.ECC.ECCurve; -using ECPoint = Neo.Cryptography.ECC.ECPoint; namespace Neo.CLI { - public class MainService : ConsoleServiceBase + public partial class MainService : ConsoleServiceBase { public event EventHandler WalletChanged; @@ -68,6 +61,84 @@ private set protected override string Prompt => "neo"; public override string ServiceName => "NEO-CLI"; + /// + /// Constructor + /// + public MainService() : base() + { + RegisterCommandHander(false, (str) => + { + switch (str.ToLowerInvariant()) + { + case "neo": return SmartContract.Native.NativeContract.NEO.Hash; + case "gas": return SmartContract.Native.NativeContract.GAS.Hash; + } + + // Try to parse as UInt160 + + if (UInt160.TryParse(str, out var addr)) + { + return addr; + } + + // Accept wallet format + + return str.ToScriptHash(); + }); + + RegisterCommandHander(false, (str) => UInt256.Parse(str)); + RegisterCommandHander((str) => str.Select(u => UInt256.Parse(u.Trim())).ToArray()); + RegisterCommandHander((arr) => + { + return arr.Select(str => + { + switch (str.ToLowerInvariant()) + { + case "neo": return SmartContract.Native.NativeContract.NEO.Hash; + case "gas": return SmartContract.Native.NativeContract.GAS.Hash; + } + + // Try to parse as UInt160 + + if (UInt160.TryParse(str, out var addr)) + { + return addr; + } + + // Accept wallet format + + return str.ToScriptHash(); + }) + .ToArray(); + }); + + RegisterCommandHander((str) => str.Select(u => ECPoint.Parse(u.Trim(), ECCurve.Secp256r1)).ToArray()); + RegisterCommandHander((str) => JObject.Parse(str)); + RegisterCommandHander((obj) => (JArray)obj); + + RegisterCommand(this); + + foreach (var plugin in Plugin.Plugins) + { + // Register plugins commands + + RegisterCommand(plugin, plugin.Name); + } + } + + public override void RunConsole() + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + + var cliV = Assembly.GetAssembly(typeof(Program)).GetVersion(); + var neoV = Assembly.GetAssembly(typeof(NeoSystem)).GetVersion(); + var vmV = Assembly.GetAssembly(typeof(ExecutionEngine)).GetVersion(); + Console.WriteLine($"{ServiceName} v{cliV} - NEO v{neoV} - NEO-VM v{vmV}"); + Console.WriteLine(); + + base.RunConsole(); + } + public void CreateWallet(string path, string password) { switch (Path.GetExtension(path)) @@ -166,178 +237,6 @@ private bool NoWallet() return true; } - protected override bool OnCommand(string[] args) - { - if (Plugin.SendMessage(args)) return true; - switch (args[0].ToLower()) - { - case "broadcast": - return OnBroadcastCommand(args); - case "relay": - return OnRelayCommand(args); - case "sign": - return OnSignCommand(args); - case "change": - return OnChangeCommand(args); - case "create": - return OnCreateCommand(args); - case "export": - return OnExportCommand(args); - case "help": - return OnHelpCommand(args); - case "plugins": - return OnPluginsCommand(args); - case "import": - return OnImportCommand(args); - case "list": - return OnListCommand(args); - case "open": - return OnOpenCommand(args); - case "close": - return OnCloseCommand(args); - case "send": - return OnSendCommand(args); - case "show": - return OnShowCommand(args); - case "start": - return OnStartCommand(args); - case "upgrade": - return OnUpgradeCommand(args); - case "deploy": - return OnDeployCommand(args); - case "invoke": - return OnInvokeCommand(args); - case "install": - return OnInstallCommand(args); - case "uninstall": - return OnUnInstallCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnBroadcastCommand(string[] args) - { - if (!Enum.TryParse(args[1], true, out MessageCommand command)) - { - Console.WriteLine($"Command \"{args[1]}\" is not supported."); - return true; - } - ISerializable payload = null; - switch (command) - { - case MessageCommand.Addr: - payload = AddrPayload.Create(NetworkAddressWithTime.Create(IPAddress.Parse(args[2]), DateTime.UtcNow.ToTimestamp(), new FullNodeCapability(), new ServerCapability(NodeCapabilityType.TcpServer, ushort.Parse(args[3])))); - break; - case MessageCommand.Block: - if (args[2].Length == 64 || args[2].Length == 66) - payload = Blockchain.Singleton.GetBlock(UInt256.Parse(args[2])); - else - payload = Blockchain.Singleton.GetBlock(uint.Parse(args[2])); - break; - case MessageCommand.GetBlocks: - case MessageCommand.GetHeaders: - payload = GetBlocksPayload.Create(UInt256.Parse(args[2])); - break; - case MessageCommand.GetData: - case MessageCommand.Inv: - payload = InvPayload.Create(Enum.Parse(args[2], true), args.Skip(3).Select(UInt256.Parse).ToArray()); - break; - case MessageCommand.Transaction: - payload = Blockchain.Singleton.GetTransaction(UInt256.Parse(args[2])); - break; - default: - Console.WriteLine($"Command \"{command}\" is not supported."); - return true; - } - NeoSystem.LocalNode.Tell(Message.Create(command, payload)); - return true; - } - - private bool OnDeployCommand(string[] args) - { - if (NoWallet()) return true; - byte[] script = LoadDeploymentScript( - /* filePath */ args[1], - /* manifest */ args.Length == 2 ? "" : args[2], - /* scriptHash */ out var scriptHash); - - Transaction tx; - try - { - tx = CurrentWallet.MakeTransaction(script); - } - catch (InvalidOperationException) - { - Console.WriteLine("Engine faulted."); - return true; - } - Console.WriteLine($"Script hash: {scriptHash.ToString()}"); - Console.WriteLine($"Gas: {new BigDecimal(tx.SystemFee, NativeContract.GAS.Decimals)}"); - Console.WriteLine(); - return SignAndSendTx(tx); - } - - private bool OnInvokeCommand(string[] args) - { - var scriptHash = UInt160.Parse(args[1]); - - List contractParameters = new List(); - for (int i = 3; i < args.Length; i++) - { - contractParameters.Add(new ContractParameter() - { - // TODO: support contract params of type other than string. - Type = ContractParameterType.String, - Value = args[i] - }); - } - - Transaction tx = new Transaction - { - Sender = UInt160.Zero, - Attributes = new TransactionAttribute[0], - Cosigners = new Cosigner[0], - Witnesses = new Witness[0] - }; - - using (ScriptBuilder scriptBuilder = new ScriptBuilder()) - { - scriptBuilder.EmitAppCall(scriptHash, args[2], contractParameters.ToArray()); - tx.Script = scriptBuilder.ToArray(); - Console.WriteLine($"Invoking script with: '{tx.Script.ToHexString()}'"); - } - - using (ApplicationEngine engine = ApplicationEngine.Run(tx.Script, tx, testMode: true)) - { - Console.WriteLine($"VM State: {engine.State}"); - Console.WriteLine($"Gas Consumed: {new BigDecimal(engine.GasConsumed, NativeContract.GAS.Decimals)}"); - Console.WriteLine($"Evaluation Stack: {new JArray(engine.ResultStack.Select(p => p.ToParameter().ToJson()))}"); - Console.WriteLine(); - if (engine.State.HasFlag(VMState.FAULT)) - { - Console.WriteLine("Engine faulted."); - return true; - } - } - - if (NoWallet()) return true; - try - { - tx = CurrentWallet.MakeTransaction(tx.Script); - } - catch (InvalidOperationException) - { - Console.WriteLine("Error: insufficient balance."); - return true; - } - if (!ReadUserInput("relay tx(no|yes)").IsYes()) - { - return true; - } - return SignAndSendTx(tx); - } - private byte[] LoadDeploymentScript(string nefFilePath, string manifestFilePath, out UInt160 scriptHash) { if (string.IsNullOrEmpty(manifestFilePath)) @@ -414,941 +313,18 @@ private byte[] LoadDeploymentScript(string nefFilePath, string manifestFilePath, } } - private bool SignAndSendTx(Transaction tx) - { - ContractParametersContext context; - try - { - context = new ContractParametersContext(tx); - } - catch (InvalidOperationException ex) - { - Console.WriteLine($"Error creating contract params: {ex}"); - throw; - } - CurrentWallet.Sign(context); - string msg; - if (context.Completed) - { - tx.Witnesses = context.GetWitnesses(); - - NeoSystem.LocalNode.Tell(new LocalNode.Relay { Inventory = tx }); - - msg = $"Signed and relayed transaction with hash={tx.Hash}"; - Console.WriteLine(msg); - return true; - } - - msg = $"Failed sending transaction with hash={tx.Hash}"; - Console.WriteLine(msg); - return true; - } - - private bool OnRelayCommand(string[] args) - { - if (args.Length < 2) - { - Console.WriteLine("You must input JSON object to relay."); - return true; - } - var jsonObjectToRelay = string.Join(string.Empty, args.Skip(1)); - if (string.IsNullOrWhiteSpace(jsonObjectToRelay)) - { - Console.WriteLine("You must input JSON object to relay."); - return true; - } - try - { - ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToRelay); - if (!context.Completed) - { - Console.WriteLine("The signature is incomplete."); - return true; - } - if (!(context.Verifiable is Transaction tx)) - { - Console.WriteLine($"Only support to relay transaction."); - return true; - } - tx.Witnesses = context.GetWitnesses(); - NeoSystem.LocalNode.Tell(new LocalNode.Relay { Inventory = tx }); - Console.WriteLine($"Data relay success, the hash is shown as follows:{Environment.NewLine}{tx.Hash}"); - } - catch (Exception e) - { - Console.WriteLine($"One or more errors occurred:{Environment.NewLine}{e.Message}"); - } - return true; - } - - private bool OnSignCommand(string[] args) - { - if (NoWallet()) return true; - - if (args.Length < 2) - { - Console.WriteLine("You must input JSON object pending signature data."); - return true; - } - var jsonObjectToSign = string.Join(string.Empty, args.Skip(1)); - if (string.IsNullOrWhiteSpace(jsonObjectToSign)) - { - Console.WriteLine("You must input JSON object pending signature data."); - return true; - } - try - { - ContractParametersContext context = ContractParametersContext.Parse(jsonObjectToSign); - if (!CurrentWallet.Sign(context)) - { - Console.WriteLine("The private key that can sign the data is not found."); - return true; - } - Console.WriteLine($"Signed Output:{Environment.NewLine}{context}"); - } - catch (Exception e) - { - Console.WriteLine($"One or more errors occurred:{Environment.NewLine}{e.Message}"); - } - return true; - } - - private bool OnChangeCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "view": - return OnChangeViewCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnChangeViewCommand(string[] args) - { - if (args.Length != 3) return false; - if (!byte.TryParse(args[2], out byte viewnumber)) return false; - NeoSystem.Consensus?.Tell(new ConsensusService.SetViewNumber { ViewNumber = viewnumber }); - return true; - } - - private bool OnCreateCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "address": - return OnCreateAddressCommand(args); - case "wallet": - return OnCreateWalletCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnCreateAddressCommand(string[] args) - { - if (NoWallet()) return true; - if (args.Length > 3) - { - Console.WriteLine("error"); - return true; - } - - string path = "address.txt"; - if (File.Exists(path)) - { - if (!ReadUserInput($"The file '{path}' already exists, do you want to overwrite it? (yes|no)", false).IsYes()) - { - return true; - } - } - - ushort count; - if (args.Length >= 3) - count = ushort.Parse(args[2]); - else - count = 1; - - List addresses = new List(); - using (var percent = new ConsolePercent(0, count)) - { - Parallel.For(0, count, (i) => - { - WalletAccount account = CurrentWallet.CreateAccount(); - lock (addresses) - { - addresses.Add(account.Address); - percent.Value++; - } - }); - } - - if (CurrentWallet is NEP6Wallet wallet) - wallet.Save(); - - Console.WriteLine($"export addresses to {path}"); - File.WriteAllLines(path, addresses); - return true; - } - - private bool OnCreateWalletCommand(string[] args) - { - if (args.Length < 3) - { - Console.WriteLine("error"); - return true; - } - string path = args[2]; - string password = ReadUserInput("password", true); - if (password.Length == 0) - { - Console.WriteLine("cancelled"); - return true; - } - string password2 = ReadUserInput("password", true); - if (password != password2) - { - Console.WriteLine("error"); - return true; - } - CreateWallet(path, password); - return true; - } - - private bool OnExportCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "block": - case "blocks": - return OnExportBlocksCommand(args); - case "key": - return OnExportKeyCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnExportBlocksCommand(string[] args) - { - bool writeStart; - uint count; - string path; - if (args.Length >= 3 && uint.TryParse(args[2], out uint start)) - { - if (Blockchain.Singleton.Height < start) return true; - count = args.Length >= 4 ? uint.Parse(args[3]) : uint.MaxValue; - count = Math.Min(count, Blockchain.Singleton.Height - start + 1); - path = $"chain.{start}.acc"; - writeStart = true; - } - else - { - start = 0; - count = Blockchain.Singleton.Height - start + 1; - path = args.Length >= 3 ? args[2] : "chain.acc"; - writeStart = false; - } - - WriteBlocks(start, count, path, writeStart); - return true; - } - - private bool OnExportKeyCommand(string[] args) - { - if (NoWallet()) return true; - if (args.Length < 2 || args.Length > 4) - { - Console.WriteLine("error"); - return true; - } - UInt160 scriptHash = null; - string path = null; - if (args.Length == 3) - { - try - { - scriptHash = args[2].ToScriptHash(); - } - catch (FormatException) - { - path = args[2]; - } - } - else if (args.Length == 4) - { - scriptHash = args[2].ToScriptHash(); - path = args[3]; - } - if (File.Exists(path)) - { - Console.WriteLine($"Error: File '{path}' already exists"); - return true; - } - string password = ReadUserInput("password", true); - if (password.Length == 0) - { - Console.WriteLine("cancelled"); - return true; - } - if (!CurrentWallet.VerifyPassword(password)) - { - Console.WriteLine("Incorrect password"); - return true; - } - IEnumerable keys; - if (scriptHash == null) - keys = CurrentWallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey()); - else - keys = new[] { CurrentWallet.GetAccount(scriptHash).GetKey() }; - if (path == null) - foreach (KeyPair key in keys) - Console.WriteLine(key.Export()); - else - File.WriteAllLines(path, keys.Select(p => p.Export())); - return true; - } - - private bool OnHelpCommand(string[] args) - { - Console.WriteLine("Normal Commands:"); - Console.WriteLine("\tversion"); - Console.WriteLine("\thelp [plugin-name]"); - Console.WriteLine("\tclear"); - Console.WriteLine("\texit"); - Console.WriteLine("Wallet Commands:"); - Console.WriteLine("\tcreate wallet "); - Console.WriteLine("\topen wallet "); - Console.WriteLine("\tclose wallet"); - Console.WriteLine("\tupgrade wallet "); - Console.WriteLine("\tlist address"); - Console.WriteLine("\tlist asset"); - Console.WriteLine("\tlist key"); - Console.WriteLine("\tshow gas"); - Console.WriteLine("\tcreate address [n=1]"); - Console.WriteLine("\timport key "); - Console.WriteLine("\texport key [address] [path]"); - Console.WriteLine("\timport multisigaddress m pubkeys..."); - Console.WriteLine("\tsend
"); - Console.WriteLine("\tsign "); - Console.WriteLine("Contract Commands:"); - Console.WriteLine("\tdeploy [manifestFile]"); - Console.WriteLine("\tinvoke [optionally quoted params separated by space]"); - Console.WriteLine("Node Commands:"); - Console.WriteLine("\tshow state"); - Console.WriteLine("\tshow pool [verbose]"); - Console.WriteLine("\trelay "); - Console.WriteLine("Plugin Commands:"); - Console.WriteLine("\tplugins"); - Console.WriteLine("\tinstall "); - Console.WriteLine("\tuninstall "); - Console.WriteLine("Advanced Commands:"); - Console.WriteLine("\texport blocks "); - Console.WriteLine("\tstart consensus"); - return true; - } - - private bool OnPluginsCommand(string[] args) - { - if (Plugin.Plugins.Count > 0) - { - Console.WriteLine("Loaded plugins:"); - Plugin.Plugins.ForEach(p => Console.WriteLine("\t" + p.Name)); - } - else - { - Console.WriteLine("No loaded plugins"); - } - return true; - } - - private bool OnImportCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "key": - return OnImportKeyCommand(args); - case "multisigaddress": - return OnImportMultisigAddress(args); - default: - return base.OnCommand(args); - } - } - - private bool OnImportMultisigAddress(string[] args) - { - if (NoWallet()) return true; - - if (args.Length < 4) - { - Console.WriteLine("Error. Invalid parameters."); - return true; - } - - int m = int.Parse(args[2]); - int n = args.Length - 3; - - if (m < 1 || m > n || n > 1024) - { - Console.WriteLine("Error. Invalid parameters."); - return true; - } - - ECPoint[] publicKeys = args.Skip(3).Select(p => ECPoint.Parse(p, ECCurve.Secp256r1)).ToArray(); - - Contract multiSignContract = Contract.CreateMultiSigContract(m, publicKeys); - KeyPair keyPair = CurrentWallet.GetAccounts().FirstOrDefault(p => p.HasKey && publicKeys.Contains(p.GetKey().PublicKey))?.GetKey(); - - WalletAccount account = CurrentWallet.CreateAccount(multiSignContract, keyPair); - if (CurrentWallet is NEP6Wallet wallet) - wallet.Save(); - - Console.WriteLine("Multisig. Addr.: " + multiSignContract.Address); - - return true; - } - - private bool OnImportKeyCommand(string[] args) - { - if (args.Length > 3) - { - Console.WriteLine("error"); - return true; - } - byte[] prikey = null; - try - { - prikey = Wallet.GetPrivateKeyFromWIF(args[2]); - } - catch (FormatException) { } - if (prikey == null) - { - var file = new FileInfo(args[2]); - - if (!file.Exists) - { - Console.WriteLine($"Error: File '{file.FullName}' doesn't exists"); - return true; - } - - if (file.Length > 1024 * 1024) - { - if (!ReadUserInput($"The file '{file.FullName}' is too big, do you want to continue? (yes|no)", false).IsYes()) - { - return true; - } - } - - string[] lines = File.ReadAllLines(args[2]).Where(u => !string.IsNullOrEmpty(u)).ToArray(); - using (var percent = new ConsolePercent(0, lines.Length)) - { - for (int i = 0; i < lines.Length; i++) - { - if (lines[i].Length == 64) - prikey = lines[i].HexToBytes(); - else - prikey = Wallet.GetPrivateKeyFromWIF(lines[i]); - CurrentWallet.CreateAccount(prikey); - Array.Clear(prikey, 0, prikey.Length); - percent.Value++; - } - } - } - else - { - WalletAccount account = CurrentWallet.CreateAccount(prikey); - Array.Clear(prikey, 0, prikey.Length); - Console.WriteLine($"address: {account.Address}"); - Console.WriteLine($" pubkey: {account.GetKey().PublicKey.EncodePoint(true).ToHexString()}"); - } - if (CurrentWallet is NEP6Wallet wallet) - wallet.Save(); - return true; - } - - private bool OnListCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "address": - return OnListAddressCommand(args); - case "asset": - return OnListAssetCommand(args); - case "key": - return OnListKeyCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnShowGasCommand(string[] args) - { - if (NoWallet()) return true; - BigInteger gas = BigInteger.Zero; - using (SnapshotView snapshot = Blockchain.Singleton.GetSnapshot()) - foreach (UInt160 account in CurrentWallet.GetAccounts().Select(p => p.ScriptHash)) - { - gas += NativeContract.NEO.UnclaimedGas(snapshot, account, snapshot.Height + 1); - } - Console.WriteLine($"unclaimed gas: {new BigDecimal(gas, NativeContract.GAS.Decimals)}"); - return true; - } - - private bool OnListKeyCommand(string[] args) - { - if (NoWallet()) return true; - foreach (KeyPair key in CurrentWallet.GetAccounts().Where(p => p.HasKey).Select(p => p.GetKey())) - { - Console.WriteLine(key.PublicKey); - } - return true; - } - - private bool OnListAddressCommand(string[] args) - { - if (NoWallet()) return true; - - using (var snapshot = Blockchain.Singleton.GetSnapshot()) - { - foreach (Contract contract in CurrentWallet.GetAccounts().Where(p => !p.WatchOnly).Select(p => p.Contract)) - { - var type = "Nonstandard"; - - if (contract.Script.IsMultiSigContract()) - { - type = "MultiSignature"; - } - else if (contract.Script.IsSignatureContract()) - { - type = "Standard"; - } - else if (snapshot.Contracts.TryGet(contract.ScriptHash) != null) - { - type = "Deployed-Nonstandard"; - } - - Console.WriteLine($"{contract.Address}\t{type}"); - } - } - - return true; - } - - private bool OnListAssetCommand(string[] args) - { - if (NoWallet()) return true; - foreach (UInt160 account in CurrentWallet.GetAccounts().Select(p => p.ScriptHash)) - { - Console.WriteLine(account.ToAddress()); - Console.WriteLine($"NEO: {CurrentWallet.GetBalance(NativeContract.NEO.Hash, account)}"); - Console.WriteLine($"GAS: {CurrentWallet.GetBalance(NativeContract.GAS.Hash, account)}"); - Console.WriteLine(); - } - Console.WriteLine("----------------------------------------------------"); - Console.WriteLine("Total: " + "NEO: " + CurrentWallet.GetAvailable(NativeContract.NEO.Hash) + " GAS: " + CurrentWallet.GetAvailable(NativeContract.GAS.Hash)); - Console.WriteLine(); - Console.WriteLine("NEO hash: " + NativeContract.NEO.Hash); - Console.WriteLine("GAS hash: " + NativeContract.GAS.Hash); - return true; - } - - private bool OnOpenCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "wallet": - return OnOpenWalletCommand(args); - default: - return base.OnCommand(args); - } - } - - //TODO: 目前没有想到其它安全的方法来保存密码 - //所以只能暂时手动输入,但如此一来就不能以服务的方式启动了 - //未来再想想其它办法,比如采用智能卡之类的 - private bool OnOpenWalletCommand(string[] args) - { - if (args.Length < 3) - { - Console.WriteLine("error"); - return true; - } - string path = args[2]; - if (!File.Exists(path)) - { - Console.WriteLine($"File does not exist"); - return true; - } - string password = ReadUserInput("password", true); - if (password.Length == 0) - { - Console.WriteLine("cancelled"); - return true; - } - try - { - OpenWallet(path, password); - } - catch (CryptographicException) - { - Console.WriteLine($"failed to open file \"{path}\""); - } - return true; - } - - /// - /// process "close" command - /// - /// - /// - private bool OnCloseCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "wallet": - return OnCloseWalletCommand(args); - default: - return base.OnCommand(args); - } - } - - /// - /// process "close wallet" command - /// - /// - /// - private bool OnCloseWalletCommand(string[] args) - { - if (CurrentWallet == null) - { - Console.WriteLine($"Wallet is not opened"); - return true; - } - CurrentWallet = null; - Console.WriteLine($"Wallet is closed"); - return true; - } - - private bool OnSendCommand(string[] args) - { - if (args.Length != 4) - { - Console.WriteLine("error"); - return true; - } - if (NoWallet()) return true; - string password = ReadUserInput("password", true); - if (password.Length == 0) - { - Console.WriteLine("cancelled"); - return true; - } - if (!CurrentWallet.VerifyPassword(password)) - { - Console.WriteLine("Incorrect password"); - return true; - } - UInt160 assetId; - switch (args[1].ToLower()) - { - case "neo": - assetId = NativeContract.NEO.Hash; - break; - case "gas": - assetId = NativeContract.GAS.Hash; - break; - default: - assetId = UInt160.Parse(args[1]); - break; - } - UInt160 to = args[2].ToScriptHash(); - Transaction tx; - AssetDescriptor descriptor = new AssetDescriptor(assetId); - if (!BigDecimal.TryParse(args[3], descriptor.Decimals, out BigDecimal amount) || amount.Sign <= 0) - { - Console.WriteLine("Incorrect Amount Format"); - return true; - } - tx = CurrentWallet.MakeTransaction(new[] - { - new TransferOutput - { - AssetId = assetId, - Value = amount, - ScriptHash = to - } - }); - - if (tx == null) - { - Console.WriteLine("Insufficient funds"); - return true; - } - - ContractParametersContext context = new ContractParametersContext(tx); - CurrentWallet.Sign(context); - if (context.Completed) - { - tx.Witnesses = context.GetWitnesses(); - NeoSystem.LocalNode.Tell(new LocalNode.Relay { Inventory = tx }); - Console.WriteLine($"TXID: {tx.Hash}"); - } - else - { - Console.WriteLine("SignatureContext:"); - Console.WriteLine(context.ToString()); - } - - return true; - } - - private bool OnShowCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "gas": - return OnShowGasCommand(args); - case "pool": - return OnShowPoolCommand(args); - case "state": - return OnShowStateCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnShowPoolCommand(string[] args) - { - bool verbose = args.Length >= 3 && args[2] == "verbose"; - if (verbose) - { - Blockchain.Singleton.MemPool.GetVerifiedAndUnverifiedTransactions( - out IEnumerable verifiedTransactions, - out IEnumerable unverifiedTransactions); - Console.WriteLine("Verified Transactions:"); - foreach (Transaction tx in verifiedTransactions) - Console.WriteLine($" {tx.Hash} {tx.GetType().Name} {tx.NetworkFee} GAS_NetFee"); - Console.WriteLine("Unverified Transactions:"); - foreach (Transaction tx in unverifiedTransactions) - Console.WriteLine($" {tx.Hash} {tx.GetType().Name} {tx.NetworkFee} GAS_NetFee"); - } - Console.WriteLine($"total: {Blockchain.Singleton.MemPool.Count}, verified: {Blockchain.Singleton.MemPool.VerifiedCount}, unverified: {Blockchain.Singleton.MemPool.UnVerifiedCount}"); - return true; - } - - private bool OnShowStateCommand(string[] args) - { - var cancel = new CancellationTokenSource(); - - Console.CursorVisible = false; - Console.Clear(); - Task broadcast = Task.Run(async () => - { - while (!cancel.Token.IsCancellationRequested) - { - NeoSystem.LocalNode.Tell(Message.Create(MessageCommand.Ping, PingPayload.Create(Blockchain.Singleton.Height))); - await Task.Delay(Blockchain.TimePerBlock, cancel.Token); - } - }); - Task task = Task.Run(async () => - { - int maxLines = 0; - - while (!cancel.Token.IsCancellationRequested) - { - Console.SetCursorPosition(0, 0); - WriteLineWithoutFlicker($"block: {Blockchain.Singleton.Height}/{Blockchain.Singleton.HeaderHeight} connected: {LocalNode.Singleton.ConnectedCount} unconnected: {LocalNode.Singleton.UnconnectedCount}", Console.WindowWidth - 1); - - int linesWritten = 1; - foreach (RemoteNode node in LocalNode.Singleton.GetRemoteNodes().OrderByDescending(u => u.LastBlockIndex).Take(Console.WindowHeight - 2).ToArray()) - { - Console.WriteLine( - $" ip: {node.Remote.Address.ToString().PadRight(15)}\tport: {node.Remote.Port.ToString().PadRight(5)}\tlisten: {node.ListenerTcpPort.ToString().PadRight(5)}\theight: {node.LastBlockIndex.ToString().PadRight(7)}"); - linesWritten++; - } - - maxLines = Math.Max(maxLines, linesWritten); - - while (linesWritten < maxLines) - { - WriteLineWithoutFlicker("", Console.WindowWidth - 1); - maxLines--; - } - - await Task.Delay(500, cancel.Token); - } - }); - ReadLine(); - cancel.Cancel(); - try { Task.WaitAll(task, broadcast); } catch { } - Console.WriteLine(); - Console.CursorVisible = true; - return true; - } - - protected internal override void OnStart(string[] args) + public override void OnStart(string[] args) { base.OnStart(args); Start(args); } - private bool OnStartCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "consensus": - return OnStartConsensusCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnStartConsensusCommand(string[] args) - { - if (NoWallet()) return true; - ShowPrompt = false; - NeoSystem.StartConsensus(CurrentWallet); - return true; - } - - protected internal override void OnStop() + public override void OnStop() { base.OnStop(); Stop(); } - private bool OnUpgradeCommand(string[] args) - { - switch (args[1].ToLower()) - { - case "wallet": - return OnUpgradeWalletCommand(args); - default: - return base.OnCommand(args); - } - } - - private bool OnInstallCommand(string[] args) - { - if (args.Length < 2) - { - Console.WriteLine("error"); - return true; - } - - bool isTemp; - string fileName; - var pluginName = args[1]; - - if (!File.Exists(pluginName)) - { - if (string.IsNullOrEmpty(Settings.Default.PluginURL)) - { - Console.WriteLine("You must define `PluginURL` in your `config.json`"); - return true; - } - - var address = string.Format(Settings.Default.PluginURL, pluginName, typeof(Plugin).Assembly.GetVersion()); - fileName = Path.Combine(Path.GetTempPath(), $"{pluginName}.zip"); - isTemp = true; - - Console.WriteLine($"Downloading from {address}"); - using (WebClient wc = new WebClient()) - { - wc.DownloadFile(address, fileName); - } - } - else - { - fileName = pluginName; - isTemp = false; - } - - try - { - ZipFile.ExtractToDirectory(fileName, "."); - } - catch (IOException) - { - Console.WriteLine($"Plugin already exist."); - return true; - } - finally - { - if (isTemp) - { - File.Delete(fileName); - } - } - - Console.WriteLine($"Install successful, please restart neo-cli."); - return true; - } - - private bool OnUnInstallCommand(string[] args) - { - if (args.Length < 2) - { - Console.WriteLine("error"); - return true; - } - - var pluginName = args[1]; - var plugin = Plugin.Plugins.FirstOrDefault(p => p.Name == pluginName); - if (plugin is null) - { - Console.WriteLine("Plugin not found"); - return true; - } - - File.Delete(plugin.Path); - File.Delete(plugin.ConfigFile); - try - { - Directory.Delete(Path.GetDirectoryName(plugin.ConfigFile), false); - } - catch (IOException) - { - } - Console.WriteLine($"Uninstall successful, please restart neo-cli."); - return true; - } - - private bool OnUpgradeWalletCommand(string[] args) - { - if (args.Length < 3) - { - Console.WriteLine("error"); - return true; - } - string path = args[2]; - if (Path.GetExtension(path) != ".db3") - { - Console.WriteLine("Can't upgrade the wallet file."); - return true; - } - if (!File.Exists(path)) - { - Console.WriteLine("File does not exist."); - return true; - } - string password = ReadUserInput("password", true); - if (password.Length == 0) - { - Console.WriteLine("cancelled"); - return true; - } - string path_new = Path.ChangeExtension(path, ".json"); - if (File.Exists(path_new)) - { - Console.WriteLine($"File '{path_new}' already exists"); - return true; - } - NEP6Wallet.Migrate(path_new, path, password).Save(); - Console.WriteLine($"Wallet file upgrade complete. New wallet file has been auto-saved at: {path_new}"); - return true; - } - public void OpenWallet(string path, string password) { if (!File.Exists(path)) @@ -1430,13 +406,13 @@ public async void Start(string[] args) { Console.WriteLine($"Warning: wallet file \"{Settings.Default.UnlockWallet.Path}\" not found."); } - catch (CryptographicException) + catch (System.Security.Cryptography.CryptographicException) { Console.WriteLine($"failed to open file \"{Settings.Default.UnlockWallet.Path}\""); } if (Settings.Default.UnlockWallet.StartConsensus && CurrentWallet != null) { - OnStartConsensusCommand(null); + OnStartConsensusCommand(); } } } diff --git a/neo-cli/Services/ConsoleServiceBase.cs b/neo-cli/Services/ConsoleServiceBase.cs deleted file mode 100644 index e022cc420..000000000 --- a/neo-cli/Services/ConsoleServiceBase.cs +++ /dev/null @@ -1,391 +0,0 @@ -using Neo.VM; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Runtime.Loader; -using System.Security; -using System.ServiceProcess; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo.Services -{ - public abstract class ConsoleServiceBase - { - protected virtual string Depends => null; - protected virtual string Prompt => "service"; - - public abstract string ServiceName { get; } - - protected bool ShowPrompt { get; set; } = true; - public bool ReadingPassword { get; set; } = false; - - private bool _running; - private readonly CancellationTokenSource _shutdownTokenSource = new CancellationTokenSource(); - private readonly CountdownEvent _shutdownAcknowledged = new CountdownEvent(1); - - protected virtual bool OnCommand(string[] args) - { - switch (args[0].ToLower()) - { - case "": - return true; - case "clear": - Console.Clear(); - return true; - case "exit": - return false; - case "version": - Console.WriteLine(Assembly.GetEntryAssembly().GetVersion()); - return true; - default: - Console.WriteLine("error: command not found " + args[0]); - return true; - } - } - - protected internal virtual void OnStart(string[] args) - { - // Register sigterm event handler - AssemblyLoadContext.Default.Unloading += SigTermEventHandler; - // Register sigint event handler - Console.CancelKeyPress += CancelHandler; - } - - protected internal virtual void OnStop() - { - _shutdownAcknowledged.Signal(); - } - - private static string[] ParseCommandLine(string line) - { - List outputArgs = new List(); - using (StringReader reader = new StringReader(line)) - { - while (true) - { - switch (reader.Peek()) - { - case -1: - return outputArgs.ToArray(); - case ' ': - reader.Read(); - break; - case '\"': - outputArgs.Add(ParseCommandLineString(reader)); - break; - default: - outputArgs.Add(ParseCommandLineArgument(reader)); - break; - } - } - } - } - - private static string ParseCommandLineArgument(TextReader reader) - { - StringBuilder sb = new StringBuilder(); - while (true) - { - int c = reader.Read(); - switch (c) - { - case -1: - case ' ': - return sb.ToString(); - default: - sb.Append((char)c); - break; - } - } - } - - private static string ParseCommandLineString(TextReader reader) - { - if (reader.Read() != '\"') throw new FormatException(); - StringBuilder sb = new StringBuilder(); - while (true) - { - int c = reader.Peek(); - switch (c) - { - case '\"': - reader.Read(); - return sb.ToString(); - case '\\': - sb.Append(ParseEscapeCharacter(reader)); - break; - default: - reader.Read(); - sb.Append((char)c); - break; - } - } - } - - private static char ParseEscapeCharacter(TextReader reader) - { - if (reader.Read() != '\\') throw new FormatException(); - int c = reader.Read(); - switch (c) - { - case -1: - throw new FormatException(); - case 'n': - return '\n'; - case 'r': - return '\r'; - case 't': - return '\t'; - case 'x': - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 2; i++) - { - int h = reader.Read(); - if (h >= '0' && h <= '9' || h >= 'A' && h <= 'F' || h >= 'a' && h <= 'f') - sb.Append((char)h); - else - throw new FormatException(); - } - return (char)byte.Parse(sb.ToString(), NumberStyles.AllowHexSpecifier); - default: - return (char)c; - } - } - - public string ReadUserInput(string prompt, bool password = false) - { - const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; - StringBuilder sb = new StringBuilder(); - ConsoleKeyInfo key; - - if (!string.IsNullOrEmpty(prompt)) - { - Console.Write(prompt + ": "); - } - - if (password) ReadingPassword = true; - var prevForeground = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Yellow; - - if (Console.IsInputRedirected) - { - // neo-gui Console require it - sb.Append(Console.ReadLine()); - } - else - { - do - { - key = Console.ReadKey(true); - - if (t.IndexOf(key.KeyChar) != -1) - { - sb.Append(key.KeyChar); - if (password) - { - Console.Write('*'); - } - else - { - Console.Write(key.KeyChar); - } - } - else if (key.Key == ConsoleKey.Backspace && sb.Length > 0) - { - sb.Length--; - Console.Write("\b \b"); - } - } while (key.Key != ConsoleKey.Enter); - } - - Console.ForegroundColor = prevForeground; - if (password) ReadingPassword = false; - Console.WriteLine(); - return sb.ToString(); - } - - public SecureString ReadSecureString(string prompt) - { - const string t = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; - SecureString securePwd = new SecureString(); - ConsoleKeyInfo key; - - if (!string.IsNullOrEmpty(prompt)) - { - Console.Write(prompt + ": "); - } - - ReadingPassword = true; - Console.ForegroundColor = ConsoleColor.Yellow; - - do - { - key = Console.ReadKey(true); - if (t.IndexOf(key.KeyChar) != -1) - { - securePwd.AppendChar(key.KeyChar); - Console.Write('*'); - } - else if (key.Key == ConsoleKey.Backspace && securePwd.Length > 0) - { - securePwd.RemoveAt(securePwd.Length - 1); - Console.Write(key.KeyChar); - Console.Write(' '); - Console.Write(key.KeyChar); - } - } while (key.Key != ConsoleKey.Enter); - - Console.ForegroundColor = ConsoleColor.White; - ReadingPassword = false; - Console.WriteLine(); - securePwd.MakeReadOnly(); - return securePwd; - } - - private void TriggerGracefulShutdown() - { - if (!_running) return; - _running = false; - _shutdownTokenSource.Cancel(); - // Wait for us to have triggered shutdown. - _shutdownAcknowledged.Wait(); - } - - private void SigTermEventHandler(AssemblyLoadContext obj) - { - TriggerGracefulShutdown(); - } - - private void CancelHandler(object sender, ConsoleCancelEventArgs e) - { - e.Cancel = true; - TriggerGracefulShutdown(); - } - - public void Run(string[] args) - { - if (Environment.UserInteractive) - { - if (args.Length > 0 && args[0] == "/install") - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - { - Console.WriteLine("Only support for installing services on Windows."); - return; - } - string arguments = string.Format("create {0} start= auto binPath= \"{1}\"", ServiceName, Process.GetCurrentProcess().MainModule.FileName); - if (!string.IsNullOrEmpty(Depends)) - { - arguments += string.Format(" depend= {0}", Depends); - } - Process process = Process.Start(new ProcessStartInfo - { - Arguments = arguments, - FileName = Path.Combine(Environment.SystemDirectory, "sc.exe"), - RedirectStandardOutput = true, - UseShellExecute = false - }); - process.WaitForExit(); - Console.Write(process.StandardOutput.ReadToEnd()); - } - else if (args.Length > 0 && args[0] == "/uninstall") - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - { - Console.WriteLine("Only support for installing services on Windows."); - return; - } - Process process = Process.Start(new ProcessStartInfo - { - Arguments = string.Format("delete {0}", ServiceName), - FileName = Path.Combine(Environment.SystemDirectory, "sc.exe"), - RedirectStandardOutput = true, - UseShellExecute = false - }); - process.WaitForExit(); - Console.Write(process.StandardOutput.ReadToEnd()); - } - else - { - OnStart(args); - RunConsole(); - OnStop(); - } - } - else - { - ServiceBase.Run(new ServiceProxy(this)); - } - } - - protected string ReadLine() - { - Task readLineTask = Task.Run(() => Console.ReadLine()); - - try - { - readLineTask.Wait(_shutdownTokenSource.Token); - } - catch (OperationCanceledException) - { - return null; - } - - return readLineTask.Result; - } - - public void RunConsole() - { - _running = true; - string[] emptyarg = new string[] { "" }; - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - try - { - Console.Title = ServiceName; - } - catch { } - - Console.ForegroundColor = ConsoleColor.DarkGreen; - - var cliV = Assembly.GetAssembly(typeof(Program)).GetVersion(); - var neoV = Assembly.GetAssembly(typeof(NeoSystem)).GetVersion(); - var vmV = Assembly.GetAssembly(typeof(ExecutionEngine)).GetVersion(); - Console.WriteLine($"{ServiceName} v{cliV} - NEO v{neoV} - NEO-VM v{vmV}"); - Console.WriteLine(); - - while (_running) - { - if (ShowPrompt) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.Write($"{Prompt}> "); - } - - Console.ForegroundColor = ConsoleColor.Yellow; - string line = ReadLine()?.Trim(); - if (line == null) break; - Console.ForegroundColor = ConsoleColor.White; - - try - { - string[] args = ParseCommandLine(line); - if (args.Length == 0) - args = emptyarg; - - _running = OnCommand(args); - } - catch (Exception ex) - { - Console.WriteLine($"error: {ex.Message}"); - } - } - - Console.ResetColor(); - } - } -} diff --git a/neo-cli/neo-cli.csproj b/neo-cli/neo-cli.csproj index 76a11382f..3f67359d2 100644 --- a/neo-cli/neo-cli.csproj +++ b/neo-cli/neo-cli.csproj @@ -29,7 +29,10 @@ - + + + + diff --git a/neo-gui/IO/Actors/EventWrapper.cs b/neo-gui/IO/Actors/EventWrapper.cs index 03c861194..1a5960a2c 100644 --- a/neo-gui/IO/Actors/EventWrapper.cs +++ b/neo-gui/IO/Actors/EventWrapper.cs @@ -1,4 +1,4 @@ -using Akka.Actor; +using Akka.Actor; using System; namespace Neo.IO.Actors diff --git a/neo-node.sln b/neo-node.sln index 3f303d819..15d235a8f 100644 --- a/neo-node.sln +++ b/neo-node.sln @@ -1,31 +1,53 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.87 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo-cli", "neo-cli\neo-cli.csproj", "{900CA179-AEF0-43F3-9833-5DB060272D8E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "neo-gui", "neo-gui\neo-gui.csproj", "{1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.Build.0 = Release|Any CPU - {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {6C1293A1-8EC4-44E8-9EE9-67892696FE26} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29519.87 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo-cli", "neo-cli\neo-cli.csproj", "{900CA179-AEF0-43F3-9833-5DB060272D8E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "neo-gui", "neo-gui\neo-gui.csproj", "{1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.ConsoleService.Tests", "tests\Neo.ConsoleService.Tests\Neo.ConsoleService.Tests.csproj", "{CC845558-D7C2-412D-8014-15699DFBA530}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.ConsoleService", "Neo.ConsoleService\Neo.ConsoleService.csproj", "{8D2BC669-11AC-42DB-BE75-FD53FA2475C6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{705EBADA-05F7-45D1-9D63-D399E87525DB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.Build.0 = Release|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.Build.0 = Release|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Release|Any CPU.Build.0 = Release|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {900CA179-AEF0-43F3-9833-5DB060272D8E} = {705EBADA-05F7-45D1-9D63-D399E87525DB} + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7} = {705EBADA-05F7-45D1-9D63-D399E87525DB} + {CC845558-D7C2-412D-8014-15699DFBA530} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9} + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6} = {705EBADA-05F7-45D1-9D63-D399E87525DB} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6C1293A1-8EC4-44E8-9EE9-67892696FE26} + EndGlobalSection +EndGlobal diff --git a/tests/Neo.ConsoleService.Tests/CommandTokenTest.cs b/tests/Neo.ConsoleService.Tests/CommandTokenTest.cs new file mode 100644 index 000000000..233e486f8 --- /dev/null +++ b/tests/Neo.ConsoleService.Tests/CommandTokenTest.cs @@ -0,0 +1,92 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; + +namespace Neo.ConsoleService.Tests +{ + [TestClass] + public class CommandTokenTest + { + [TestMethod] + public void Test1() + { + var cmd = " "; + var args = CommandToken.Parse(cmd).ToArray(); + + AreEqual(args, new CommandSpaceToken(0, 1)); + Assert.AreEqual(cmd, CommandToken.ToString(args)); + } + + [TestMethod] + public void Test2() + { + var cmd = "show state"; + var args = CommandToken.Parse(cmd).ToArray(); + + AreEqual(args, new CommandStringToken(0, "show"), new CommandSpaceToken(4, 2), new CommandStringToken(6, "state")); + Assert.AreEqual(cmd, CommandToken.ToString(args)); + } + + [TestMethod] + public void Test3() + { + var cmd = "show \"hello world\""; + var args = CommandToken.Parse(cmd).ToArray(); + + AreEqual(args, + new CommandStringToken(0, "show"), + new CommandSpaceToken(4, 1), + new CommandQuoteToken(5, '"'), + new CommandStringToken(6, "hello world"), + new CommandQuoteToken(17, '"') + ); + Assert.AreEqual(cmd, CommandToken.ToString(args)); + } + + [TestMethod] + public void Test4() + { + var cmd = "show \"'\""; + var args = CommandToken.Parse(cmd).ToArray(); + + AreEqual(args, + new CommandStringToken(0, "show"), + new CommandSpaceToken(4, 1), + new CommandQuoteToken(5, '"'), + new CommandStringToken(6, "'"), + new CommandQuoteToken(7, '"') + ); + Assert.AreEqual(cmd, CommandToken.ToString(args)); + } + + [TestMethod] + public void Test5() + { + var cmd = "show \"123\\\"456\""; + var args = CommandToken.Parse(cmd).ToArray(); + + AreEqual(args, + new CommandStringToken(0, "show"), + new CommandSpaceToken(4, 1), + new CommandQuoteToken(5, '"'), + new CommandStringToken(6, "123\\\"456"), + new CommandQuoteToken(14, '"') + ); + Assert.AreEqual(cmd, CommandToken.ToString(args)); + } + + private void AreEqual(CommandToken[] args, params CommandToken[] compare) + { + Assert.AreEqual(compare.Length, args.Length); + + for (int x = 0; x < args.Length; x++) + { + var a = args[x]; + var b = compare[x]; + + Assert.AreEqual(a.Type, b.Type); + Assert.AreEqual(a.Value, b.Value); + Assert.AreEqual(a.Offset, b.Offset); + } + } + } +} diff --git a/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj b/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj new file mode 100644 index 000000000..94db44800 --- /dev/null +++ b/tests/Neo.ConsoleService.Tests/Neo.ConsoleService.Tests.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.0 + neo_cli.Tests + + + + + + + + + + + + +