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