diff --git a/.gitignore b/.gitignore
index 0dc4ba7a..8d248eae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
/projects
/archive
/release
+/applibs
/env.cmd
/*.zip
diff --git a/BenchManager/BenchCLI/ArgumentValidation.cs b/BenchManager/BenchCLI/ArgumentValidation.cs
new file mode 100644
index 00000000..19b3c473
--- /dev/null
+++ b/BenchManager/BenchCLI/ArgumentValidation.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Mastersign.Bench.Cli
+{
+ static class ArgumentValidation
+ {
+ public static bool ContainsOneOfChars(string v, char[] chars)
+ {
+ foreach (var c in chars)
+ {
+ if (v.Contains(new string(new[] { c }))) return true;
+ }
+ return false;
+ }
+
+ public static bool IsValidPath(string v)
+ {
+ return !ContainsOneOfChars(v, Path.GetInvalidPathChars());
+ }
+
+ public static bool IsIdString(string v)
+ {
+ return !string.IsNullOrEmpty(v) && !v.Contains(" ");
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/BenchCLI.csproj b/BenchManager/BenchCLI/BenchCLI.csproj
new file mode 100644
index 00000000..ad80307c
--- /dev/null
+++ b/BenchManager/BenchCLI/BenchCLI.csproj
@@ -0,0 +1,138 @@
+
+
+
+
+
+ Debug
+ AnyCPU
+ {64E94A41-026F-473C-BC48-70F8D5EB977A}
+ Exe
+ Properties
+ Mastersign.Bench.Cli
+ bench
+ v2.0
+ 512
+ true
+
+
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ logo.ico
+
+
+
+ ..\packages\Mastersign.Sequence.1.1.0\lib\net20\Mastersign.Sequence.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {3ff60d62-d733-40e8-b759-848fae5fea93}
+ BenchLib
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BenchManager/BenchCLI/CliTools/ArgumentCompletionConsoleDialog.cs b/BenchManager/BenchCLI/CliTools/ArgumentCompletionConsoleDialog.cs
new file mode 100644
index 00000000..9c53bdcb
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ArgumentCompletionConsoleDialog.cs
@@ -0,0 +1,359 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.Docs;
+
+namespace Mastersign.CliTools
+{
+ public class ArgumentCompletionConsoleDialog : ConsoleDialog
+ {
+ private const char HELP_MNEMONIC = '?';
+ private const char FLAG_OR_OPTION_MNEMONIC = '-';
+
+ private readonly ArgumentParser parser;
+
+ public ArgumentCompletionConsoleDialog(ArgumentParser parser)
+ {
+ this.parser = parser;
+ }
+
+ public ArgumentParsingResult ShowFor(ArgumentParsingResult prelimResult)
+ {
+ if (prelimResult.Type != ArgumentParsingResultType.MissingArgument &&
+ prelimResult.Type != ArgumentParsingResultType.NoCommand)
+ {
+ throw new InvalidOperationException("The arguments are already satisfying.");
+ }
+
+ var flags = prelimResult.Flags;
+ var optionValues = prelimResult.OptionValues;
+ var positionalValues = prelimResult.PositionalValues;
+
+ var positionalArgs = parser.GetPositionals();
+ var missingPositionalArgs = new List();
+ foreach (var pArg in positionalArgs)
+ {
+ if (prelimResult.GetPositionalValue(pArg.Name) == null)
+ {
+ missingPositionalArgs.Add(pArg);
+ }
+ }
+ var hasMissingPositionalArguments = missingPositionalArgs.Count > 0;
+
+ var selectedCommand = prelimResult.Command;
+ var isCommandMissing = parser.GetCommands().Length > 0 && selectedCommand == null;
+
+ if (isCommandMissing)
+ {
+ CommandMenuResult result;
+ do
+ {
+ result = ShowCommandMenu(ref selectedCommand);
+ switch (result)
+ {
+ case CommandMenuResult.Escape:
+ return null;
+ case CommandMenuResult.Help:
+ return prelimResult.DeriveHelp();
+ case CommandMenuResult.Command:
+ if (selectedCommand != null)
+ {
+ Console.Write("Selected Command: ");
+ Console.ForegroundColor = ConsoleColor.White;
+ Console.Write(selectedCommand);
+ Console.ForegroundColor = ConsoleColor.Gray;
+ Console.WriteLine();
+ }
+ break;
+ case CommandMenuResult.FlagOrOption:
+ Argument arg;
+ var result2 = ShowFlagAndOptionMenu(flags, optionValues, out arg, MenuFollowUp.Return);
+ switch (result2)
+ {
+ case FlagAndOptionMenuResult.Exit:
+ break;
+ case FlagAndOptionMenuResult.Help:
+ return prelimResult.DeriveHelp();
+ case FlagAndOptionMenuResult.FlagOrOption:
+ PrintFlagOrOptionCompletion(flags, optionValues, arg);
+ break;
+ default:
+ throw new NotSupportedException();
+ }
+ break;
+ default:
+ throw new NotSupportedException();
+ }
+ } while (result == CommandMenuResult.FlagOrOption);
+ }
+ else if (hasMissingPositionalArguments &&
+ (parser.GetFlags().Length > 0 || parser.GetOptions().Length > 0))
+ {
+ Argument arg;
+ FlagAndOptionMenuResult result;
+ do
+ {
+ result = ShowFlagAndOptionMenu(flags, optionValues, out arg, MenuFollowUp.Proceed);
+ switch (result)
+ {
+ case FlagAndOptionMenuResult.Exit:
+ break;
+ case FlagAndOptionMenuResult.Help:
+ return prelimResult.DeriveHelp();
+ case FlagAndOptionMenuResult.FlagOrOption:
+ PrintFlagOrOptionCompletion(flags, optionValues, arg);
+ break;
+ default:
+ throw new NotSupportedException();
+ }
+ } while (result == FlagAndOptionMenuResult.FlagOrOption);
+ }
+
+ if (hasMissingPositionalArguments)
+ {
+ foreach (var arg in missingPositionalArgs)
+ {
+ positionalValues[arg.Name] = ReadArgumentValue(arg.Name,
+ arg.ValuePredicate, arg.Description, arg.PossibleValueInfo);
+ }
+ }
+
+ return prelimResult.DeriveInteractivelyCompleted(selectedCommand);
+ }
+
+ private void PrintFlagOrOptionCompletion(IDictionary flags, IDictionary optionValues, Argument arg)
+ {
+ if (arg is FlagArgument)
+ {
+ Write("Flag {0}: ", arg.Name);
+ Console.ForegroundColor = ConsoleColor.White;
+ WriteLine(flags.ContainsKey(arg.Name) && flags[arg.Name] ? "active" : "inactive");
+ Console.ForegroundColor = ConsoleColor.Gray;
+ }
+ else if (arg is OptionArgument)
+ {
+ Write("Option {0}: ", arg.Name);
+ Console.ForegroundColor = ConsoleColor.White;
+ WriteLine(optionValues[arg.Name]);
+ Console.ForegroundColor = ConsoleColor.Gray;
+ }
+ }
+
+ private enum CommandMenuResult
+ {
+ Escape,
+ Help,
+ Command,
+ FlagOrOption
+ }
+
+ private CommandMenuResult ShowCommandMenu(ref string selectedCommand)
+ {
+ var commandArgs = parser.GetCommands();
+ SortArgumentsByMnemonic(commandArgs);
+ var result = CommandMenuResult.Command;
+ Open();
+ WriteLine();
+ WriteLine("Choose one of the following commands:");
+ WriteLine();
+ var keyChars = new List();
+ var quitMnemonic = ArgumentParser.MenuQuitMnemonic;
+ keyChars.Add(ESC);
+ keyChars.Add(quitMnemonic);
+ keyChars.Add(HELP_MNEMONIC);
+ WriteMenuItem(HELP_MNEMONIC, "Display the help.");
+ if (parser.GetFlags().Length > 0 || parser.GetOptions().Length > 0)
+ {
+ keyChars.Add(FLAG_OR_OPTION_MNEMONIC);
+ WriteMenuItem(FLAG_OR_OPTION_MNEMONIC, "Specify a flag or an option value.");
+ }
+ foreach (var arg in commandArgs)
+ {
+ WriteMenuItem(arg.Mnemonic, arg.Description.ToString());
+ keyChars.Add(arg.Mnemonic);
+ }
+ WriteLine();
+ Write("Press a character key to choose a menu item or ESC/" + quitMnemonic + " to quit. ");
+ var selectedMnemonic = ReadExpectedChar(keyChars);
+ Close();
+ if (selectedMnemonic == ESC || selectedMnemonic == quitMnemonic)
+ {
+ result = CommandMenuResult.Escape;
+ }
+ else if (selectedMnemonic == HELP_MNEMONIC)
+ {
+ result = CommandMenuResult.Help;
+ }
+ else if (selectedMnemonic == FLAG_OR_OPTION_MNEMONIC)
+ {
+ result = CommandMenuResult.FlagOrOption;
+ }
+ else
+ {
+ foreach (var cmd in commandArgs)
+ {
+ if (cmd.Mnemonic == selectedMnemonic)
+ {
+ selectedCommand = cmd.Name;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ private enum FlagAndOptionMenuResult
+ {
+ Exit,
+ Help,
+ FlagOrOption
+ }
+
+ private enum MenuFollowUp
+ {
+ Return,
+ Proceed
+ }
+
+ private FlagAndOptionMenuResult ShowFlagAndOptionMenu(
+ IDictionary flags, IDictionary optionValues,
+ out Argument selectedArgument, MenuFollowUp followUp)
+ {
+ var flagArgs = parser.GetFlags();
+ SortArgumentsByMnemonic(flagArgs);
+ var optionArgs = parser.GetOptions();
+ SortArgumentsByMnemonic(optionArgs);
+ var hasFlags = flagArgs.Length > 0;
+ var hasOptions = optionArgs.Length > 0;
+ var result = FlagAndOptionMenuResult.FlagOrOption;
+ Open();
+ WriteLine();
+ if (hasFlags && hasOptions)
+ WriteLine("Choose one of the following flags or options:");
+ else if (hasFlags)
+ WriteLine("Choose one of the following flags:");
+ else if (hasOptions)
+ WriteLine("Choose one of the following options:");
+ WriteLine();
+ var keyChars = new List();
+ var quitMnemonic = ArgumentParser.MenuQuitMnemonic;
+ if (followUp == MenuFollowUp.Return)
+ {
+ keyChars.Add(ESC);
+ keyChars.Add(quitMnemonic);
+ }
+ else
+ keyChars.Add(ENTER);
+ keyChars.Add(HELP_MNEMONIC);
+ if (hasFlags)
+ {
+ WriteLine("Flags");
+ foreach (var arg in flagArgs)
+ {
+ WriteMenuItem(arg.Mnemonic, arg.Description.ToString());
+ keyChars.Add(arg.Mnemonic);
+ }
+ }
+ if (hasOptions)
+ {
+ WriteLine("Options");
+ foreach (var arg in optionArgs)
+ {
+ WriteMenuItem(arg.Mnemonic, arg.Description.ToString());
+ keyChars.Add(arg.Mnemonic);
+ }
+ }
+ WriteLine();
+ if (followUp == MenuFollowUp.Return)
+ Write("Press a character key to choose a menu item or ESC to go back. ");
+ else
+ Write("Press a character key to choose a menu item or ENTER to proceed. ");
+ var selectedMnemonic = ReadExpectedChar(keyChars);
+ Close();
+ selectedArgument = null;
+ if (selectedMnemonic == ESC || selectedMnemonic == quitMnemonic || selectedMnemonic == ENTER)
+ {
+ result = FlagAndOptionMenuResult.Exit;
+ }
+ else if (selectedMnemonic == HELP_MNEMONIC)
+ {
+ result = FlagAndOptionMenuResult.Help;
+ }
+ else
+ {
+ foreach (var arg in flagArgs)
+ {
+ if (arg.Mnemonic == selectedMnemonic)
+ {
+ selectedArgument = arg;
+ if (flags.ContainsKey(arg.Name) && flags[arg.Name])
+ flags.Remove(arg.Name);
+ else
+ flags[arg.Name] = true;
+ return result;
+ }
+ }
+ foreach (var arg in optionArgs)
+ {
+ if (arg.Mnemonic == selectedMnemonic)
+ {
+ selectedArgument = arg;
+ optionValues[arg.Name] = ReadArgumentValue(arg.Name,
+ arg.ValuePredicate, arg.Description, arg.PossibleValueInfo);
+ return result;
+ }
+ }
+ }
+ return result;
+ }
+
+ private string ReadArgumentValue(string name, ArgumentValuePredicate predicate,
+ Document description, Document possibleValueInfo)
+ {
+ string result = null;
+ var valid = true;
+ do
+ {
+ Open();
+ WriteLine();
+ WriteLine("Enter value for {0}", name);
+ WriteLine();
+ WriteLine("Description: " + description.ToString());
+ if (!valid)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ WriteLine("The given value for '{0}' is invalid, try again.", name);
+ WriteLine("Expected: " + possibleValueInfo.ToString());
+ Console.ForegroundColor = ConsoleColor.Gray;
+ }
+ else
+ {
+ WriteLine("Expected: " + possibleValueInfo.ToString());
+ }
+ WriteLine();
+ Write("Value for {0}: ", name);
+ result = ReadLine();
+ Close();
+ if (predicate != null)
+ {
+ valid = predicate(result);
+ }
+ } while (!valid);
+ return result;
+ }
+
+ private static void SortArgumentsByMnemonic(T[] args)
+ where T : NamedArgument
+ {
+ Array.Sort(args, (c1, c2) => c1.Mnemonic.CompareTo(c2.Mnemonic));
+ }
+
+ private void WriteMenuItem(char mnemonic, string description)
+ {
+ Console.ForegroundColor = ConsoleColor.White;
+ Write(" {0} ", mnemonic);
+ Console.ForegroundColor = ConsoleColor.Gray;
+ WriteLine(description);
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/ArgumentParser.cs b/BenchManager/BenchCLI/CliTools/ArgumentParser.cs
new file mode 100644
index 00000000..986f180a
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ArgumentParser.cs
@@ -0,0 +1,628 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.Docs;
+
+namespace Mastersign.CliTools
+{
+ public class ArgumentParser
+ {
+ public ArgumentParserType ParserType { get; set; }
+
+ public static string[] HelpIndicators = new[] { "/?", "-?", "-h", "--help" };
+
+ public static string MainHelpIndicator = "-?";
+
+ public static char MenuQuitMnemonic = 'q';
+
+ private readonly Dictionary arguments = new Dictionary();
+
+ public string Name { get; private set; }
+
+ public Document Description { get; private set; }
+
+ public ArgumentParser(string name, IEnumerable arguments)
+ {
+ Name = name;
+ ParserType = ArgumentParserType.CaseSensitive;
+ Description = new Document();
+ foreach (var arg in arguments)
+ {
+ RegisterArgument(arg);
+ }
+ }
+
+ public ArgumentParser(string name, params Argument[] arguments)
+ : this(name, (IEnumerable)arguments)
+ {
+ }
+
+ public void RegisterArgument(Argument arg)
+ {
+ switch (arg.Type)
+ {
+ case ArgumentType.Flag:
+ if (!(arg is FlagArgument))
+ throw new ArgumentException("Expected type " + nameof(FlagArgument) + ".");
+ if (MnemonicExists((NamedArgument)arg))
+ throw new ArgumentException("The arguments mnemonic is already in use.");
+ break;
+ case ArgumentType.Option:
+ if (!(arg is OptionArgument))
+ throw new ArgumentException("Expected type " + nameof(OptionArgument) + ".");
+ if (MnemonicExists((NamedArgument)arg))
+ throw new ArgumentException("The arguments mnemonic is already in use.");
+ break;
+ case ArgumentType.Command:
+ if (!(arg is CommandArgument))
+ throw new ArgumentException("Expected type " + nameof(CommandArgument) + ".");
+ if (MnemonicExists((NamedArgument)arg))
+ throw new ArgumentException("The arguments mnemonic is already in use.");
+ break;
+ case ArgumentType.Positional:
+ if (!(arg is PositionalArgument))
+ throw new ArgumentException("Expected type " + nameof(PositionalArgument) + ".");
+ break;
+ default:
+ throw new ArgumentException("Argument type not supported.");
+ }
+ arguments.Add(arg.Name, arg);
+ }
+
+ public void RegisterArguments(params Argument[] arguments)
+ {
+ foreach (var arg in arguments)
+ {
+ RegisterArgument(arg);
+ }
+ }
+
+ private T[] FilterArguments(ArgumentType type) where T : Argument
+ {
+ var res = new List();
+ foreach (var a in arguments.Values)
+ {
+ if (a.Type == type)
+ {
+ res.Add((T)a);
+ }
+ }
+ res.Sort((a, b) => a.Name.CompareTo(b.Name));
+ return res.ToArray();
+ }
+
+ private bool MnemonicExists(NamedArgument arg)
+ {
+ var m = arg.Mnemonic;
+ if (arg is CommandArgument)
+ {
+ foreach (var cmdArg in GetCommands())
+ if (cmdArg.Mnemonic == m) return true;
+ return false;
+ }
+ if (arg is FlagArgument || arg is OptionArgument)
+ {
+ foreach (var flagArg in GetFlags())
+ if (flagArg.Mnemonic == m) return true;
+ foreach (var optArg in GetOptions())
+ if (optArg.Mnemonic == m) return true;
+ return false;
+ }
+ return false;
+ }
+
+ public FlagArgument[] GetFlags()
+ => FilterArguments(ArgumentType.Flag);
+
+ public OptionArgument[] GetOptions()
+ => FilterArguments(ArgumentType.Option);
+
+ public PositionalArgument[] GetPositionals()
+ {
+ var list = new List(FilterArguments(ArgumentType.Positional));
+ list.Sort((a1, a2) => a1.OrderIndex.CompareTo(a2.OrderIndex));
+ return list.ToArray();
+ }
+
+ public CommandArgument[] GetCommands()
+ => FilterArguments(ArgumentType.Command);
+
+ private bool IsHelpIndicator(string v)
+ {
+ foreach (var i in HelpIndicators)
+ {
+ if (i.Equals(v, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private string[] MissingPositinalArguments(int foundArgumentsCount)
+ {
+ var positionalArgs = GetPositionals();
+ var missingArgs = Math.Max(0, positionalArgs.Length - foundArgumentsCount);
+ var list = new string[missingArgs];
+ for (int i = 0; i < missingArgs; i++)
+ {
+ list[i] = positionalArgs[foundArgumentsCount + i].Name;
+ }
+ return list;
+ }
+
+ public ArgumentParsingResult Parse(string[] args)
+ {
+ var index = new ArgumentIndex(
+ ParserType == ArgumentParserType.CaseSensitive,
+ arguments.Values);
+ IDictionary flagValues = new Dictionary();
+ IDictionary optionValues = new Dictionary();
+ IDictionary positionalValues = new Dictionary();
+ string command = null;
+ var position = 0;
+ var help = false;
+ string invalid = null;
+
+ while (position < args.Length && command == null)
+ {
+ var arg = args[position];
+ if (IsHelpIndicator(arg))
+ {
+ help = true;
+ position++;
+ continue;
+ }
+ var a = index.LookUp(args[position], positionalValues.Count);
+ if (a == null)
+ {
+ if (GetCommands().Length > 0)
+ {
+ invalid = args[position];
+ }
+ break;
+ }
+ switch (a.Type)
+ {
+ case ArgumentType.Flag:
+ flagValues[a.Name] = true;
+ break;
+ case ArgumentType.Option:
+ position++;
+ if (args.Length <= position)
+ {
+ invalid = args[position - 1] + " ???";
+ break;
+ }
+ var opt = (OptionArgument)a;
+ if (opt.ValuePredicate != null && !opt.ValuePredicate(args[position]))
+ {
+ invalid = args[position - 1] + " " + args[position];
+ break;
+ }
+ optionValues[a.Name] = args[position];
+ break;
+ case ArgumentType.Positional:
+ var pArg = (PositionalArgument)a;
+ if (pArg.ValuePredicate != null && !pArg.ValuePredicate(args[position]))
+ {
+ invalid = pArg.Name + ": " + args[position];
+ break;
+ }
+ positionalValues[a.Name] = args[position];
+ break;
+ case ArgumentType.Command:
+ command = a.Name;
+ break;
+ default:
+ throw new NotSupportedException();
+ }
+ if (invalid != null) break;
+ position++;
+ }
+ if (help)
+ {
+ return new ArgumentParsingResult(this, ArgumentParsingResultType.Help,
+ null, null, null, optionValues, flagValues, positionalValues);
+ }
+ if (invalid != null)
+ {
+ return new ArgumentParsingResult(this, ArgumentParsingResultType.InvalidArgument,
+ null, invalid, null, optionValues, flagValues, positionalValues);
+ }
+ var missingPositionalArguments = MissingPositinalArguments(positionalValues.Count);
+ if (missingPositionalArguments.Length > 0)
+ {
+ return new ArgumentParsingResult(this, ArgumentParsingResultType.MissingArgument,
+ null, string.Join(", ", missingPositionalArguments),
+ null, optionValues, flagValues, positionalValues);
+ }
+ var rest = new string[args.Length - position];
+ Array.Copy(args, position, rest, 0, rest.Length);
+ if (command != null)
+ {
+ return new ArgumentParsingResult(this, ArgumentParsingResultType.Command,
+ command, null, rest, optionValues, flagValues, positionalValues);
+ }
+ return new ArgumentParsingResult(this, ArgumentParsingResultType.NoCommand,
+ null, null, rest, optionValues, flagValues, positionalValues);
+ }
+ }
+
+ internal class ArgumentIndex
+ {
+ private readonly Dictionary flags = new Dictionary();
+ private readonly Dictionary flagMnemonics = new Dictionary();
+ private readonly Dictionary options = new Dictionary();
+ private readonly Dictionary optionMnemonics = new Dictionary();
+ private readonly Dictionary commands = new Dictionary();
+ private readonly Dictionary commandMnemonics = new Dictionary();
+ private readonly List positionals = new List();
+
+ private readonly bool CaseSensitive;
+
+ public ArgumentIndex(bool caseSensitive, IEnumerable args)
+ {
+ CaseSensitive = caseSensitive;
+ foreach (var arg in args)
+ {
+ AddArgument(arg);
+ }
+ }
+
+ private string PrepareArgument(string arg)
+ {
+ if (arg == null) throw new ArgumentNullException();
+ return CaseSensitive ? arg : arg.ToLowerInvariant();
+ }
+
+ private char PrepareArgument(char arg)
+ {
+ return CaseSensitive ? arg : char.ToLowerInvariant(arg);
+ }
+
+ private void AddArgument(Argument arg)
+ {
+ var namedArg = arg as NamedArgument;
+ switch (arg.Type)
+ {
+ case ArgumentType.Flag:
+ flags[PrepareArgument(arg.Name)] = arg;
+ foreach (var alias in namedArg.Aliases)
+ {
+ flags[PrepareArgument(alias)] = arg;
+ }
+ flagMnemonics[PrepareArgument(namedArg.Mnemonic)] = arg;
+ break;
+ case ArgumentType.Option:
+ options[PrepareArgument(arg.Name)] = arg;
+ foreach (var alias in namedArg.Aliases)
+ {
+ options[PrepareArgument(alias)] = arg;
+ }
+ optionMnemonics[PrepareArgument(namedArg.Mnemonic)] = arg;
+ break;
+ case ArgumentType.Command:
+ commands[PrepareArgument(arg.Name)] = arg;
+ foreach (var alias in namedArg.Aliases)
+ {
+ commands[PrepareArgument(alias)] = arg;
+ }
+ commandMnemonics[PrepareArgument(namedArg.Mnemonic)] = arg;
+ break;
+ case ArgumentType.Positional:
+ positionals.Add(arg);
+ break;
+ default:
+ throw new NotSupportedException();
+ }
+ positionals.Sort((a1, a2) =>
+ ((PositionalArgument)a1).OrderIndex.CompareTo(((PositionalArgument)a2).OrderIndex));
+ }
+
+ public Argument LookUp(string v, int consumedPositionals)
+ {
+ Argument a;
+ v = PrepareArgument(v);
+ if (v.StartsWith("--"))
+ {
+ var name = v.Substring(2);
+ if (flags.TryGetValue(name, out a)) return a;
+ if (options.TryGetValue(name, out a)) return a;
+ return null;
+ }
+ if (v.Length == 2 && v.StartsWith("-"))
+ {
+ var mnemonic = v[1];
+ if (flagMnemonics.TryGetValue(mnemonic, out a)) return a;
+ if (optionMnemonics.TryGetValue(mnemonic, out a)) return a;
+ return null;
+ }
+ if (commands.TryGetValue(v, out a)) return a;
+ if (v.Length == 1 && commandMnemonics.TryGetValue(v[0], out a)) return a;
+
+ if (consumedPositionals < positionals.Count) return positionals[consumedPositionals];
+
+ return null;
+ }
+ }
+
+ public enum ArgumentParserType
+ {
+ CaseSensitive,
+ CaseInsensitive
+ }
+
+ public enum ArgumentType
+ {
+ Flag,
+ Option,
+ Positional,
+ Command
+ }
+
+ public abstract class Argument
+ {
+ public ArgumentType Type { get; private set; }
+
+ public string Name { get; private set; }
+
+
+ public Document Description { get; private set; }
+
+ protected Argument(ArgumentType type, string name)
+ {
+ Type = type;
+ Name = name;
+ Description = new Document();
+ }
+ }
+
+ public abstract class NamedArgument : Argument
+ {
+ public char Mnemonic { get; private set; }
+
+ public string[] Aliases { get; private set; }
+
+ protected NamedArgument(ArgumentType type, string name, char mnemonic,
+ params string[] aliases)
+ : base(type, name)
+ {
+ if (mnemonic == ArgumentParser.MenuQuitMnemonic)
+ {
+ throw new ArgumentException("The mnemonic equals the menu quit mnemonic.");
+ }
+ Mnemonic = mnemonic;
+ Aliases = aliases;
+ }
+ }
+
+ public class FlagArgument : NamedArgument
+ {
+ public FlagArgument(string name, char mnemonic,
+ params string[] aliases)
+ : base(ArgumentType.Flag, name, mnemonic, aliases)
+ {
+ }
+ }
+
+ public delegate bool ArgumentValuePredicate(string value);
+
+ public class OptionArgument : NamedArgument
+ {
+ public Document PossibleValueInfo { get; private set; }
+
+ public Document DefaultValueInfo { get; private set; }
+
+ public ArgumentValuePredicate ValuePredicate { get; private set; }
+
+ public OptionArgument(string name, char mnemonic,
+ ArgumentValuePredicate valuePredicate,
+ params string[] aliases)
+ : base(ArgumentType.Option, name, mnemonic, aliases)
+ {
+ PossibleValueInfo = new Document();
+ DefaultValueInfo = new Document();
+ ValuePredicate = valuePredicate;
+ }
+ }
+
+ public class EnumOptionArgument : OptionArgument
+ {
+ private static bool IsEnumMember(string v)
+ {
+ try
+ {
+ Enum.Parse(typeof(T), v, true);
+ return true;
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ }
+
+ public T DefaultValue { get; private set; }
+
+ public EnumOptionArgument(string name, char mnemonic,
+ T defaultValue, params string[] aliases)
+ : base(name, mnemonic, IsEnumMember)
+ {
+ DefaultValue = defaultValue;
+ var enumNames = Enum.GetNames(typeof(T));
+ for (int i = 0; i < enumNames.Length; i++)
+ {
+ if (i > 0) PossibleValueInfo.Text(" | ");
+ PossibleValueInfo.Keyword(enumNames[i]);
+ }
+ DefaultValueInfo.Keyword(defaultValue.ToString());
+ }
+ }
+
+ public class PositionalArgument : Argument
+ {
+ public Document PossibleValueInfo { get; private set; }
+
+ public int OrderIndex { get; private set; }
+
+ public ArgumentValuePredicate ValuePredicate { get; private set; }
+
+ public PositionalArgument(string name,
+ ArgumentValuePredicate valuePredicate, int position)
+ : base(ArgumentType.Positional, name)
+ {
+ PossibleValueInfo = new Document();
+ OrderIndex = position;
+ ValuePredicate = valuePredicate;
+ }
+ }
+
+ public class CommandArgument : NamedArgument
+ {
+ public Document SyntaxInfo { get; private set; }
+
+ public CommandArgument(string name, char mnemonic,
+ params string[] aliases)
+ : base(ArgumentType.Command, name, mnemonic, aliases)
+ {
+ SyntaxInfo = new Document();
+ }
+ }
+
+ public class ArgumentParsingResult
+ {
+ public ArgumentParser Parser { get; private set; }
+
+ public ArgumentParsingResultType Type { get; private set; }
+
+ public string Command { get; private set; }
+
+ public string ErrorMessage { get; private set; }
+
+ public string[] Rest { get; private set; }
+
+ private readonly IDictionary options;
+
+ public IDictionary OptionValues => options;
+
+ private readonly IDictionary flags;
+
+ public IDictionary Flags => flags;
+
+ private readonly IDictionary positionals;
+
+ public IDictionary PositionalValues => positionals;
+
+ public bool IsCompletedInteractively { get; private set; }
+
+ public ArgumentParsingResult(ArgumentParser parser,
+ ArgumentParsingResultType type,
+ string command, string errorMessage, string[] rest,
+ IDictionary options,
+ IDictionary flags,
+ IDictionary positionals)
+ {
+ Parser = parser;
+ Type = type;
+ Command = command;
+ ErrorMessage = errorMessage;
+ Rest = rest;
+ this.options = options ?? new Dictionary();
+ this.flags = flags ?? new Dictionary();
+ this.positionals = positionals ?? new Dictionary();
+ }
+
+ public ArgumentParsingResult DeriveHelp()
+ {
+ return new ArgumentParsingResult(Parser, ArgumentParsingResultType.Help,
+ Command, null, Rest, options, flags, positionals);
+ }
+
+ public ArgumentParsingResult DeriveInteractivelyCompleted(
+ string command)
+ {
+ return new ArgumentParsingResult(Parser,
+ command != null
+ ? ArgumentParsingResultType.Command
+ : ArgumentParsingResultType.NoCommand,
+ command, null, Rest, options, flags, positionals)
+ { IsCompletedInteractively = true };
+ }
+
+ public string GetOptionValue(string name, string def = null)
+ {
+ string res;
+ return options.TryGetValue(name, out res) ? res : def;
+ }
+
+ public bool GetFlag(string name)
+ {
+ return flags.ContainsKey(name);
+ }
+
+ public string GetPositionalValue(string name, string def = null)
+ {
+ string res;
+ return positionals.TryGetValue(name, out res) ? res : def;
+ }
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("Result Type: " + Type);
+ switch (Type)
+ {
+ case ArgumentParsingResultType.InvalidArgument:
+ sb.AppendLine("Invalid Argument: " + ErrorMessage);
+ break;
+ case ArgumentParsingResultType.MissingArgument:
+ sb.AppendLine("Missing Argument(s): " + ErrorMessage);
+ break;
+ case ArgumentParsingResultType.Command:
+ case ArgumentParsingResultType.NoCommand:
+ if (flags.Count > 0)
+ {
+ var flagNames = new List(flags.Keys);
+ sb.AppendLine("Flags: " + string.Join(", ", flagNames.ToArray()));
+ }
+ if (options.Count > 0)
+ {
+ sb.AppendLine("Options:");
+ var optionNames = new List(options.Keys);
+ optionNames.Sort();
+ foreach (var n in optionNames)
+ {
+ sb.AppendLine(" * " + n + " = " + options[n]);
+ }
+ }
+ if (positionals.Count > 0)
+ {
+ sb.AppendLine("Positionals:");
+ var positionalNames = new List(positionals.Keys);
+ positionalNames.Sort();
+ foreach (var n in positionalNames)
+ {
+ sb.AppendLine(" * " + n + " = " + options[n]);
+ }
+ }
+ if (Rest != null && Rest.Length > 0)
+ {
+ sb.AppendLine("Rest: " + string.Join(" ", Rest));
+ }
+ break;
+ default:
+ break;
+ }
+ return sb.ToString();
+ }
+ }
+
+ public enum ArgumentParsingResultType
+ {
+ InvalidArgument,
+ MissingArgument,
+ Help,
+ Command,
+ NoCommand
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/CommandBase.cs b/BenchManager/BenchCLI/CliTools/CommandBase.cs
new file mode 100644
index 00000000..5527ef4e
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/CommandBase.cs
@@ -0,0 +1,450 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.Docs;
+
+namespace Mastersign.CliTools
+{
+ public abstract class CommandBase
+ {
+ private ArgumentParser argParser;
+
+ public ArgumentParser ArgumentParser
+ {
+ get
+ {
+ if (argParser == null)
+ {
+ argParser = new ArgumentParser(Name);
+ InitializeArgumentParser(argParser);
+ }
+ return argParser;
+ }
+ }
+
+ protected abstract void InitializeArgumentParser(ArgumentParser parser);
+
+ protected ArgumentParsingResult Arguments { get; set; }
+
+ public abstract string Name { get; }
+
+ private CommandBase parent;
+
+ public CommandBase Parent { get; protected set; }
+
+ public IDictionary SubCommands { get; private set; }
+
+ protected CommandBase()
+ {
+ SubCommands = new Dictionary();
+ }
+
+ protected void RegisterSubCommand(CommandBase subCommand)
+ {
+ SubCommands[subCommand.Name] = subCommand;
+ subCommand.Parent = this;
+ }
+
+ #region Bubbeling Properties
+
+ private string toolName;
+
+ public string ToolName
+ {
+ get { return Parent != null ? Parent.ToolName : toolName; }
+ protected set { toolName = value; }
+ }
+
+ private string toolVersion;
+
+ public string ToolVersion
+ {
+ get { return Parent != null ? Parent.ToolVersion : toolVersion; }
+ protected set { toolVersion = value; }
+ }
+
+ private Document toolDescription;
+
+ public Document ToolDescription
+ {
+ get
+ {
+ if (Parent != null) return Parent.ToolDescription;
+ if (toolDescription == null) toolDescription = new Document();
+ return toolDescription;
+ }
+ }
+
+ private bool verbose;
+
+ public bool Verbose
+ {
+ get { return Parent != null ? Parent.Verbose : verbose; }
+ protected set { verbose = value; }
+ }
+
+ private bool noAssurance;
+
+ public bool NoAssurance
+ {
+ get { return Parent != null ? Parent.NoAssurance : noAssurance; }
+ protected set { noAssurance = value; }
+ }
+
+ private DocumentOutputFormat helpFormat;
+
+ public DocumentOutputFormat HelpFormat
+ {
+ get { return Parent != null ? Parent.HelpFormat : helpFormat; }
+ set { helpFormat = value; }
+ }
+
+ #endregion
+
+ #region Console Output
+
+ protected void WriteLine(string message)
+ => Console.WriteLine(message);
+
+ protected void WriteLine(string format, params object[] args)
+ => Console.WriteLine(format, args);
+
+ protected void WriteError(string message)
+ {
+ var colorBackup = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine("[ERROR] (cli) " + message);
+ Console.ForegroundColor = colorBackup;
+ }
+
+ protected void WriteError(string format, params object[] args)
+ => WriteError(string.Format(format, args));
+
+ protected void WriteInfo(string message)
+ {
+ if (!Verbose) return;
+ var colorBackup = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("[INFO] (cli) " + message);
+ Console.ForegroundColor = colorBackup;
+ }
+
+ protected void WriteInfo(string format, params object[] args)
+ => WriteInfo(string.Format(format, args));
+
+ protected void WriteDetail(string message)
+ {
+ if (!Verbose) return;
+ var colorBackup = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine("[VERBOSE] (cli) " + message);
+ Console.ForegroundColor = colorBackup;
+ }
+
+ protected void WriteDetail(string format, params object[] args)
+ => WriteDetail(string.Format(format, args));
+
+ private void Backspace(int l)
+ {
+ Console.Write("".PadRight(l, (char)0x08));
+ }
+
+ protected virtual bool AskForAssurance(string question)
+ {
+ if (NoAssurance) return true;
+
+ var colorBackup = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ var extent = "(y/N)";
+ Console.Write(question + " " + extent);
+ bool? result = null;
+ while (result == null)
+ {
+ var key = Console.ReadKey(true);
+ if (key.Key == ConsoleKey.Enter)
+ result = false;
+ else if (key.Key == ConsoleKey.N)
+ result = false;
+ else if (key.Key == ConsoleKey.Y)
+ result = true;
+ }
+ Backspace(extent.Length);
+ Console.Write("<- ");
+ if (result.Value)
+ {
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.Write("Yes");
+ }
+ else
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Write("No");
+ }
+ Console.ForegroundColor = colorBackup;
+ Console.WriteLine();
+ return result.Value;
+ }
+
+ #endregion
+
+ #region Help
+
+ public CommandBase[] CommandChain(bool withRoot = true)
+ {
+ var cmds = new List();
+ var cmd = this;
+ while (cmd != null && (withRoot || cmd.Parent != null))
+ {
+ cmds.Add(cmd);
+ cmd = cmd.Parent;
+ }
+ cmds.Reverse();
+ return cmds.ToArray();
+ }
+
+ private string[] CommandChainNames(bool withRoot = true)
+ {
+ var names = new List();
+ foreach (var cmd in CommandChain(withRoot))
+ {
+ names.Add(cmd.Name);
+ }
+ return names.ToArray();
+ }
+
+ public string CommandChain(string separator, bool withRoot = true)
+ => string.Join(separator, CommandChainNames(withRoot));
+
+ protected CommandBase[] CommandHierarchyDepthSearch()
+ {
+ var result = new List();
+ CommandHierarchyDepthSearch(result);
+ return result.ToArray();
+ }
+
+ private void CommandHierarchyDepthSearch(ICollection coll)
+ {
+ coll.Add(this);
+ if (SubCommands.Count > 0)
+ {
+ var children = new List(SubCommands.Values);
+ children.Sort((c1, c2) => c1.Name.CompareTo(c2.Name));
+ foreach (var item in children)
+ {
+ item.CommandHierarchyDepthSearch(coll);
+ }
+ }
+ }
+
+ public CommandBase RootCommand
+ {
+ get
+ {
+ var cmd = this;
+ while (cmd.Parent != null) cmd = cmd.Parent;
+ return cmd;
+ }
+ }
+
+ protected virtual void PrintHelpHint()
+ {
+ WriteLine("Use '{0} {1}' to display the help.",
+ CommandChain(" "),
+ ArgumentParser.MainHelpIndicator);
+ }
+
+ protected virtual void PrintInvalidArgumentWarning(string arg)
+ {
+ WriteError("Invalid Argument: " + arg);
+ PrintHelpHint();
+ }
+
+ protected virtual void PrintMissingArgumentWarning(string arg)
+ {
+ WriteError("Missing Argument(s): " + arg);
+ PrintHelpHint();
+ }
+
+ protected virtual void PrintHelp()
+ {
+ using (var w = DocumentWriterFactory.Create(HelpFormat))
+ {
+ PrintHelp(w);
+ }
+ }
+
+ protected virtual void PrintHelp(DocumentWriter w)
+ {
+ w.Begin(BlockType.Document);
+ w.Title("{0} v{1}", ToolName, ToolVersion);
+ PrintCommandHelp(w, withHelpSection: true, withCommandLinks: false);
+ w.End(BlockType.Document);
+ }
+
+ public void PrintFullHelp(DocumentWriter w,
+ bool withTitle = true, bool withVersion = true, bool withIndex = true)
+ {
+ w.Begin(BlockType.Document);
+ if (withTitle) w.Title(ToolName);
+ if (withVersion) w.Paragraph("Version: {0}", ToolVersion);
+ w.Append(ToolDescription);
+
+ var commands = CommandHierarchyDepthSearch();
+ if (withIndex)
+ {
+ w.Headline2("index", "Commands");
+ w.Begin(BlockType.List);
+ foreach (var cmd in commands)
+ {
+ w.Begin(BlockType.ListItem)
+ .Begin(BlockType.Link)
+ .LinkTarget("#" + HelpFormatter.CommandAnchor(cmd))
+ .Begin(BlockType.LinkContent)
+ .Append(HelpFormatter.SlimCommandChain, cmd)
+ .End(BlockType.LinkContent)
+ .End(BlockType.Link)
+ .End(BlockType.ListItem);
+ }
+ w.End(BlockType.List);
+ }
+ foreach (var cmd in commands)
+ {
+ w.Headline1(HelpFormatter.CommandAnchor(cmd), cmd.CommandChain(" ", true));
+ cmd.PrintCommandHelp(w, withHelpSection: cmd == this, withCommandLinks: true);
+ }
+
+ w.End(BlockType.Document);
+ }
+
+ private void PrintCommandHelp(DocumentWriter w,
+ bool withHelpSection = true, bool withCommandLinks = false)
+ {
+ if (Parent != null)
+ {
+ w.Begin(BlockType.Paragraph);
+ w.Text("Command: ");
+ var chain = CommandChainNames();
+ for (int i = 0; i < chain.Length; i++)
+ {
+ if (i > 0) w.Text(" ");
+ w.Keyword(chain[i]);
+ }
+ w.End(BlockType.Paragraph);
+ }
+ HelpFormatter.WriteHelp(w, this, withHelpSection, withCommandLinks);
+ }
+
+ private ArgumentParsingResult CompleteInteractively(ArgumentParsingResult arguments)
+ {
+ var dialog = new ArgumentCompletionConsoleDialog(ArgumentParser);
+ return dialog.ShowFor(Arguments);
+ }
+
+ #endregion
+
+ #region Execution
+
+ public virtual bool Process(string[] args)
+ {
+ WriteDetail("Arguments: {0}", string.Join(" ", args));
+ return Process(ArgumentParser.Parse(args));
+ }
+
+ protected bool Process(ArgumentParsingResult arguments)
+ {
+ Arguments = arguments;
+ if (Arguments.Type == ArgumentParsingResultType.Help ||
+ Arguments.Type == ArgumentParsingResultType.NoCommand ||
+ Arguments.Type == ArgumentParsingResultType.Command)
+ {
+ try
+ {
+ if (!ValidateArguments())
+ return false;
+ }
+ catch (Exception e)
+ {
+ WriteError(e.Message);
+ return false;
+ }
+ }
+ if (Arguments.Type == ArgumentParsingResultType.Help)
+ {
+ PrintHelp();
+ return true;
+ }
+ if (Arguments.Type == ArgumentParsingResultType.InvalidArgument)
+ {
+ PrintInvalidArgumentWarning(Arguments.ErrorMessage);
+ return false;
+ }
+ if (Arguments.Type == ArgumentParsingResultType.MissingArgument)
+ {
+ if (Arguments.IsCompletedInteractively)
+ {
+ PrintMissingArgumentWarning(arguments.ErrorMessage);
+ return false;
+ }
+ var arguments2 = CompleteInteractively(Arguments);
+ if (arguments2 == null)
+ {
+ WriteDetail("Canceled by user.");
+ return false;
+ }
+ return Process(arguments2);
+ }
+ if (Arguments.Type == ArgumentParsingResultType.NoCommand)
+ {
+ return ExecuteCommand(Arguments.Rest);
+ }
+ if (Arguments.Type == ArgumentParsingResultType.Command)
+ {
+ WriteDetail("Command: {0}", Arguments.Command);
+ return ExecuteSubCommand(Arguments.Command, Arguments.Rest);
+ }
+
+ WriteError("Argument parsing result not supported.");
+ return false;
+ }
+
+ protected virtual bool ValidateArguments()
+ {
+ return true;
+ }
+
+ protected virtual bool ExecuteSubCommand(string command, string[] args)
+ {
+ CommandBase cmd;
+ if (SubCommands.TryGetValue(command, out cmd))
+ return cmd.Process(args);
+ else
+ return ExecuteUnknownSubCommand(command, args);
+ }
+
+ protected virtual bool ExecuteUnknownSubCommand(string command, string[] args)
+ {
+ WriteError("The sub-command '{0}' is not implemented.", command);
+ PrintHelpHint();
+ return false;
+ }
+
+ protected virtual bool ExecuteCommand(string[] args)
+ {
+ if (Arguments.IsCompletedInteractively)
+ {
+ WriteError("This command has no meaning on its own. Try specifying a sub-command.");
+ PrintHelpHint();
+ return false;
+ }
+ var arguments2 = CompleteInteractively(Arguments);
+ if (arguments2 == null)
+ {
+ WriteDetail("Canceled by user.");
+ return false;
+ }
+ return Process(arguments2);
+ }
+
+ #endregion
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/ConsoleDialog.cs b/BenchManager/BenchCLI/CliTools/ConsoleDialog.cs
new file mode 100644
index 00000000..fa12ec7b
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ConsoleDialog.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public class ConsoleDialog : ConsoleOperation
+ {
+ protected const char ESC = (char)27;
+ protected const char ENTER = (char)13;
+
+ private int lines;
+
+ protected void Write(string text)
+ {
+ Console.Write(text);
+ }
+
+ protected void WriteLine(string text = null)
+ {
+ Console.WriteLine(text ?? string.Empty);
+ lines++;
+ }
+
+ protected string ReadLine()
+ {
+ lines++;
+ return Console.ReadLine();
+ }
+
+ protected void Write(string format, params object[] args)
+ => Write(string.Format(format, args));
+
+ protected void WriteLine(string format, params object[] args)
+ => WriteLine(string.Format(format, args));
+
+ protected void ClearLine(int row)
+ {
+ Console.SetCursorPosition(0, row);
+ Console.Write(new string(' ', Console.BufferWidth));
+ }
+
+ public void Open()
+ {
+ lines = 0;
+ }
+
+ public void Close()
+ {
+ var bottom = Console.CursorTop;
+ var top = Math.Max(0, bottom - lines);
+ Console.SetCursorPosition(0, top);
+ BackupState();
+ for (int r = top; r <= bottom; r++) ClearLine(r);
+ RestoreState();
+ }
+
+ protected char ReadExpectedChar(IList expected)
+ {
+ char k;
+ do
+ {
+ k = Console.ReadKey(true).KeyChar;
+ } while (!expected.Contains(k));
+ return k;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/ConsoleMapWriter.cs b/BenchManager/BenchCLI/CliTools/ConsoleMapWriter.cs
new file mode 100644
index 00000000..7afff284
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ConsoleMapWriter.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public class ConsoleMapWriter : IMapWriter
+ {
+ public void Dispose()
+ {
+ }
+
+ public void Write(string key, object value)
+ {
+ if (value == null) WriteNull(key);
+ else if (value is bool) WriteValue(key, (bool)value);
+ else if (value is string) WriteValue(key, (string)value);
+ else if (value is string[]) WriteValue(key, (string[])value);
+ else if (value is IDictionary) WriteValue(key, (IDictionary)value);
+ else WriteUnknown(key);
+ }
+
+ private string EscapeString(string value)
+ {
+ return value != null
+ ? "\"" + value.Replace(@"\", @"\\").Replace("\"", "\\\"") + "\""
+ : null;
+ }
+
+ public void WriteValue(string key, IDictionary value)
+ {
+ var pairs = new List();
+ foreach (var kvp in (Dictionary)value)
+ {
+ pairs.Add(string.Format("{0}: {1}",
+ EscapeString(kvp.Key), EscapeString(kvp.Value)));
+ }
+ Console.WriteLine(key + " = {" + string.Join(", ", pairs.ToArray()) + "}");
+ }
+
+ public void WriteValue(string key, string[] value)
+ {
+ var items = new List();
+ foreach (var item in value)
+ {
+ items.Add(EscapeString(item));
+ }
+ Console.WriteLine("{0} = [{1}]", key, string.Join(", ", items.ToArray()));
+ }
+
+ public void WriteValue(string key, string value)
+ {
+ Console.WriteLine("{0} = {1}", key, EscapeString(value.ToString()));
+ }
+
+ public void WriteValue(string key, bool value)
+ {
+ Console.WriteLine("{0} = {1}", key, value ? "True" : "False");
+ }
+
+ public void WriteNull(string key)
+ {
+ Console.WriteLine("{0} = Null", key);
+ }
+
+ public void WriteUnknown(string key)
+ {
+ Console.WriteLine("{0} = Unsupported Data Type", key);
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/ConsoleOperation.cs b/BenchManager/BenchCLI/CliTools/ConsoleOperation.cs
new file mode 100644
index 00000000..05200963
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ConsoleOperation.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public class ConsoleOperation
+ {
+ public static ConsoleColor DefaultForegroundColor = Console.ForegroundColor;
+ public static ConsoleColor DefaultBackgroundColor = Console.BackgroundColor;
+
+ private int backupRow;
+ private int backupColumn;
+ private ConsoleColor backupForegroundColor;
+ private ConsoleColor backupBackgroundColor;
+
+ public int StoredCursorTop => backupRow;
+ public int StoredCursorLeft => backupColumn;
+
+ protected void BackupState()
+ {
+ backupRow = Console.CursorTop;
+ backupColumn = Console.CursorLeft;
+ backupForegroundColor = Console.ForegroundColor;
+ backupBackgroundColor = Console.BackgroundColor;
+ Console.CursorVisible = false;
+ }
+
+ protected void RestoreState()
+ {
+ Console.ForegroundColor = backupForegroundColor;
+ Console.BackgroundColor = backupBackgroundColor;
+ Console.SetCursorPosition(backupColumn, backupRow);
+ Console.CursorVisible = true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/ConsoleTableWriter.cs b/BenchManager/BenchCLI/CliTools/ConsoleTableWriter.cs
new file mode 100644
index 00000000..2f90235e
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ConsoleTableWriter.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ class ConsoleTableWriter : ITableWriter
+ {
+ private string[] columns;
+ private List rows;
+ private bool isDisposed;
+
+ public void Initialize(params string[] columns)
+ {
+ this.columns = columns;
+ this.rows = new List();
+ }
+
+ public void Write(params object[] values)
+ {
+ if (isDisposed) throw new ObjectDisposedException(nameof(ConsoleTableWriter));
+ if (columns == null) throw new InvalidOperationException();
+ if (values.Length != columns.Length) throw new ArgumentException("Incorrect number of values.");
+ var row = new List();
+ for (int i = 0; i < values.Length; i++)
+ {
+ row.Add(Format(values[i]));
+ }
+ rows.Add(row.ToArray());
+ }
+
+ private string Format(object value)
+ {
+ if (value == null) return string.Empty;
+ if (value is bool) return ((bool)value) ? "TRUE" : "FALSE";
+ if (value is string) return (string)value;
+ return "UNSUPPORTED TYPE";
+ }
+
+ private void WriteTable()
+ {
+ var c = columns.Length;
+ var lengths = new int[c];
+ for (int i = 0; i < c; i++) lengths[i] = columns[i].Length;
+ foreach (var row in rows)
+ {
+ for (int i = 0; i < c; i++) lengths[i] = Math.Max(lengths[i], row[i].Length);
+ }
+ for (int i = 0; i < c; i++)
+ {
+ if (i > 0) Write(" | ");
+ Write(columns[i].PadRight(lengths[i]));
+ }
+ NewLine();
+ for (int i = 0; i < c; i++)
+ {
+ if (i > 0) Write("-|-");
+ Write(new string('-', lengths[i]));
+ }
+ NewLine();
+ foreach (var row in rows)
+ {
+ for (int i = 0; i < c; i++)
+ {
+ if (i > 0) Write(" | ");
+ Write(row[i].PadRight(lengths[i]));
+ }
+ NewLine();
+ }
+ }
+
+ private int lineLength = 0;
+ private void Write(string value)
+ {
+ var w = Console.WindowWidth;
+ if (lineLength >= w) return;
+ if (lineLength + value.Length < w)
+ {
+ Console.Write(value);
+ lineLength += value.Length;
+ }
+ else
+ {
+ Console.Write(value.Substring(0, w - lineLength));
+ lineLength = w;
+ }
+ }
+
+ private void NewLine()
+ {
+ Console.WriteLine();
+ lineLength = 0;
+ }
+
+ public void Dispose()
+ {
+ if (isDisposed) return;
+ WriteTable();
+ isDisposed = true;
+ columns = null;
+ rows = null;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/DataOutputFormat.cs b/BenchManager/BenchCLI/CliTools/DataOutputFormat.cs
new file mode 100644
index 00000000..6175cd6c
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/DataOutputFormat.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public enum DataOutputFormat
+ {
+ Plain,
+ Markdown,
+ // Json,
+ // Xml,
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/HelpFormatter.cs b/BenchManager/BenchCLI/CliTools/HelpFormatter.cs
new file mode 100644
index 00000000..0b44f6da
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/HelpFormatter.cs
@@ -0,0 +1,366 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.Docs;
+
+namespace Mastersign.CliTools
+{
+ public static class HelpFormatter
+ {
+ private static void FormatFlag(DocumentWriter w, NamedArgument a)
+ {
+ w.Keyword("--" + a.Name);
+ foreach (var alias in a.Aliases)
+ {
+ w.Text(" | ");
+ w.Keyword("--" + alias);
+ }
+ w.Text(" | ");
+ w.Keyword("-" + a.Mnemonic);
+ }
+
+ private static void FormatOption(DocumentWriter w, OptionArgument a)
+ {
+ FormatFlag(w, a);
+ w.Text(" ");
+ w.Variable("value");
+ }
+
+ private static void FormatPositional(DocumentWriter w, PositionalArgument a)
+ {
+ w.Variable(a.Name);
+ }
+
+ private static void FormatCommand(DocumentWriter w, CommandArgument a)
+ {
+ w.Keyword(a.Name);
+ foreach (var alias in a.Aliases)
+ {
+ w.Text(", ");
+ w.Keyword(alias);
+ }
+ w.Text(", ");
+ w.Keyword(new string(a.Mnemonic, 1));
+ }
+
+ private static bool HasFlags(ArgumentParser p)
+ {
+ return p.GetFlags().Length > 0;
+ }
+
+ private static bool HasOptions(ArgumentParser p)
+ {
+ return p.GetOptions().Length > 0;
+ }
+
+ private static bool HasCommands(ArgumentParser p)
+ {
+ return p.GetCommands().Length > 0;
+ }
+
+ private static bool HasPositionals(ArgumentParser p)
+ {
+ return p.GetPositionals().Length > 0;
+ }
+
+ public static void FlagsAndOptionsGeneric(DocumentWriter w, ArgumentParser p)
+ {
+ var hasFlags = HasFlags(p);
+ var hasOptions = HasOptions(p);
+ if (hasFlags && hasOptions)
+ {
+ w.Text(" (").Variable("flag").Text(" | ")
+ .Variable("option").Text(")*");
+ }
+ else
+ {
+ if (hasFlags)
+ {
+ w.Text(" ").Variable("flag").Text("*");
+ }
+ if (hasOptions)
+ {
+ w.Text(" ").Variable("option").Text("*");
+ }
+ }
+ foreach (var a in p.GetPositionals())
+ {
+ w.Text(" ").Append(FormatPositional, a);
+ }
+ }
+
+ public static void FullCommandChain(DocumentWriter w, CommandBase cmd)
+ {
+ var cmdChain = cmd.CommandChain();
+ for (int i = 0; i < cmdChain.Length; i++)
+ {
+ if (i > 0) w.Text(" ");
+ var c = cmdChain[i];
+ w.Keyword(c.Name).Append(FlagsAndOptionsGeneric, c.ArgumentParser);
+ }
+ }
+
+ public static void CommandSyntax(DocumentWriter w, CommandBase cmd)
+ {
+ var cmdChain = cmd.CommandChain();
+ for (int i = 0; i < cmdChain.Length; i++)
+ {
+ if (i > 0) w.Text(" ");
+ var c = cmdChain[i];
+ w.Keyword(c.Name);
+ }
+ if (cmdChain.Length > 0)
+ {
+ var p = cmdChain[cmdChain.Length - 1].ArgumentParser;
+ w.Append(FlagsAndOptionsGeneric, p);
+ }
+ if (cmd.ArgumentParser.GetCommands().Length > 0)
+ {
+ w.Text(" ").Variable("sub-command");
+ }
+ }
+
+ public static void SlimCommandChain(DocumentWriter w, CommandBase cmd)
+ {
+ var cmdChain = cmd.CommandChain();
+ for (int i = 0; i < cmdChain.Length; i++)
+ {
+ if (i > 0) w.Text(" ");
+ var c = cmdChain[i];
+ w.Keyword(c.Name);
+ }
+ }
+
+ public static string CommandAnchor(CommandBase cmd)
+ {
+ return "cmd_" + cmd.CommandChain("-");
+ }
+
+ public static string CommandAnchor(CommandBase cmd, string subCmd)
+ {
+ return CommandAnchor(cmd) + "-" + subCmd;
+ }
+
+ private static Document allHelpIndicators;
+ private static Document AllHelpIndicators
+ {
+ get
+ {
+ if (allHelpIndicators == null)
+ {
+ var d = new Document();
+ var helpIndicators = ArgumentParser.HelpIndicators;
+ for (int i = 0; i < helpIndicators.Length; i++)
+ {
+ if (i > 0) d.Text(", ");
+ d.Keyword(helpIndicators[i]);
+ }
+ allHelpIndicators = d;
+ }
+ return allHelpIndicators;
+ }
+ }
+
+ public static void WriteHelp(DocumentWriter w, CommandBase cmd,
+ bool withHelpSection = true, bool withCommandLinks = false)
+ {
+ var parser = cmd.ArgumentParser;
+ w.Append(parser.Description);
+ WriteUsage(w, cmd, withHelp: !withHelpSection);
+ if (withHelpSection) WriteHelpUsage(w, cmd);
+ WriteFlags(w, cmd);
+ WriteOptions(w, cmd);
+ WritePositionals(w, cmd);
+ WriteCommands(w, cmd, withCommandLinks);
+ }
+
+ private static void WriteUsage(DocumentWriter w, CommandBase cmd,
+ bool withHelp = false)
+ {
+ w.Headline2(CommandAnchor(cmd) + "_usage", "Usage");
+
+ w.Begin(BlockType.List);
+ if (withHelp)
+ {
+ w.Begin(BlockType.ListItem)
+ .Append(SlimCommandChain, cmd).Text(" ")
+ .Keyword(ArgumentParser.MainHelpIndicator);
+ w.End(BlockType.ListItem);
+ }
+ w.ListItem(FullCommandChain, cmd);
+ if (HasCommands(cmd.ArgumentParser))
+ {
+ w.Begin(BlockType.ListItem)
+ .Append(FullCommandChain, cmd)
+ .Text(" ").Variable("command").Text(" ...");
+ w.End(BlockType.ListItem);
+ }
+ w.End(BlockType.List);
+ }
+
+ private static void WriteHelpUsage(DocumentWriter w, CommandBase cmd)
+ {
+ w.Headline2(CommandAnchor(cmd) + "_help", "Help");
+ w.Begin(BlockType.Paragraph)
+ .Text("Showing the help can be triggered by one of the following flags: ")
+ .Append(AllHelpIndicators)
+ .Text(".");
+ w.End(BlockType.Paragraph);
+ w.Begin(BlockType.List);
+ w.Begin(BlockType.ListItem)
+ .Append(SlimCommandChain, cmd).Text(" ")
+ .Keyword(ArgumentParser.MainHelpIndicator);
+ w.End(BlockType.ListItem);
+ if (HasCommands(cmd.ArgumentParser))
+ {
+ w.Begin(BlockType.ListItem)
+ .Append(SlimCommandChain, cmd)
+ .Text(" ").Variable("command").Text(" ")
+ .Keyword(ArgumentParser.MainHelpIndicator);
+ w.End(BlockType.ListItem);
+ }
+ w.End(BlockType.List);
+ }
+
+ private static void WriteFlags(DocumentWriter w, CommandBase cmd)
+ {
+ var flags = cmd.ArgumentParser.GetFlags();
+ if (flags.Length > 0)
+ {
+ w.Headline2(CommandAnchor(cmd) + "_flags", "Flags");
+ w.Begin(BlockType.DefinitionList);
+ foreach (var flag in flags)
+ {
+ w.Begin(BlockType.Definition);
+ w.DefinitionTopic(FormatFlag, flag);
+ w.DefinitionContent(flag.Description);
+ w.End(BlockType.Definition);
+ }
+ w.End(BlockType.DefinitionList);
+ }
+ }
+
+ private static void WriteOptions(DocumentWriter w, CommandBase cmd)
+ {
+ var options = cmd.ArgumentParser.GetOptions();
+ if (options.Length > 0)
+ {
+ w.Headline2(CommandAnchor(cmd) + "_options", "Options");
+ w.Begin(BlockType.DefinitionList);
+ foreach (var option in options)
+ {
+ var hasDefinitions = option.PossibleValueInfo != null || option.DefaultValueInfo != null;
+ w.Begin(BlockType.Definition);
+ w.DefinitionTopic(FormatOption, option);
+ w.Begin(BlockType.DefinitionContent);
+ if (hasDefinitions)
+ {
+ if (!option.Description.IsEmpty)
+ {
+ w.Paragraph(option.Description);
+ }
+ w.Begin(BlockType.PropertyList);
+ if (!option.PossibleValueInfo.IsEmpty)
+ {
+ w.Property("Expected", option.PossibleValueInfo);
+ }
+ if (!option.DefaultValueInfo.IsEmpty)
+ {
+ w.Property("Default", option.DefaultValueInfo);
+ }
+ w.End(BlockType.PropertyList);
+ }
+ else if (!option.Description.IsEmpty)
+ {
+ w.Append(option.Description);
+ }
+ w.End(BlockType.DefinitionContent);
+ w.End(BlockType.Definition);
+ }
+ w.End(BlockType.DefinitionList);
+ }
+ }
+
+ private static void WritePositionals(DocumentWriter w, CommandBase cmd)
+ {
+ var positionals = cmd.ArgumentParser.GetPositionals();
+ if (positionals.Length > 0)
+ {
+ w.Headline2(CommandAnchor(cmd) + "_positionals", "Positional Arguments");
+ w.Begin(BlockType.DefinitionList);
+ foreach (var pArg in positionals)
+ {
+ var hasDefinitions = pArg.PossibleValueInfo != null;
+ w.Begin(BlockType.Definition);
+ w.Begin(BlockType.DefinitionTopic)
+ .Text(pArg.OrderIndex.ToString().PadLeft(2) + ". ")
+ .Text(pArg.Name)
+ .End(BlockType.DefinitionTopic);
+ w.Begin(BlockType.DefinitionContent);
+ if (hasDefinitions)
+ {
+ if (!pArg.Description.IsEmpty)
+ {
+ w.Paragraph(pArg.Description);
+ }
+ w.Begin(BlockType.PropertyList);
+ if (!pArg.PossibleValueInfo.IsEmpty)
+ {
+ w.Property("Expected", pArg.PossibleValueInfo);
+ }
+ w.End(BlockType.PropertyList);
+ }
+ else if (!pArg.Description.IsEmpty)
+ {
+ w.Append(pArg.Description);
+ }
+ w.End(BlockType.DefinitionContent);
+ w.End(BlockType.Definition);
+ }
+ w.End(BlockType.DefinitionList);
+ }
+ }
+
+ private static void WriteCommands(DocumentWriter w, CommandBase cmd, bool withLinks = false)
+ {
+ var commands = cmd.ArgumentParser.GetCommands();
+ if (commands.Length > 0)
+ {
+ w.Headline2(CommandAnchor(cmd) + "_commands", "Commands");
+ w.Begin(BlockType.DefinitionList);
+ foreach (var cmdArg in commands)
+ {
+ w.Begin(BlockType.Definition);
+ w.Begin(BlockType.DefinitionTopic);
+ if (withLinks)
+ {
+ w.Begin(BlockType.Link);
+ w.LinkTarget("#" + CommandAnchor(cmd, cmdArg.Name));
+ w.Begin(BlockType.LinkContent);
+ }
+ w.Append(FormatCommand, cmdArg);
+ if (withLinks)
+ {
+ w.End(BlockType.LinkContent);
+ w.End(BlockType.Link);
+ }
+ w.End(BlockType.DefinitionTopic);
+ w.Begin(BlockType.DefinitionContent);
+ if (!cmdArg.Description.IsEmpty)
+ {
+ w.Paragraph(cmdArg.Description);
+ }
+ if (!cmdArg.SyntaxInfo.IsEmpty)
+ {
+ w.Begin(BlockType.PropertyList);
+ w.Property("Syntax", cmdArg.SyntaxInfo);
+ w.End(BlockType.PropertyList);
+ }
+ w.End(BlockType.DefinitionContent);
+ w.End(BlockType.Definition);
+ }
+ w.End(BlockType.DefinitionList);
+ }
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/IMapWriter.cs b/BenchManager/BenchCLI/CliTools/IMapWriter.cs
new file mode 100644
index 00000000..9bf62652
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/IMapWriter.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public interface IMapWriter : IDisposable
+ {
+ void Write(string key, object value);
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/ITableWriter.cs b/BenchManager/BenchCLI/CliTools/ITableWriter.cs
new file mode 100644
index 00000000..80e06967
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/ITableWriter.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public interface ITableWriter : IDisposable
+ {
+ void Initialize(params string[] columns);
+
+ void Write(params object[] values);
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/MapWriterFactory.cs b/BenchManager/BenchCLI/CliTools/MapWriterFactory.cs
new file mode 100644
index 00000000..a8560cf8
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/MapWriterFactory.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public static class MapWriterFactory
+ {
+ public static IMapWriter Create(DataOutputFormat format)
+ {
+ switch (format)
+ {
+ case DataOutputFormat.Plain:
+ return new ConsoleMapWriter();
+ case DataOutputFormat.Markdown:
+ return new MarkdownMapWriter(Console.OpenStandardOutput());
+ //case OutputFormat.JSON:
+ // return new JsonPropertyWriter(Console.OpenStandardOutput());
+ //case OutputFormat.XML:
+ // return new XmlPropertyWriter(Console.OpenStandardOutput());
+ default:
+ throw new NotSupportedException();
+ }
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/MarkdownMapWriter.cs b/BenchManager/BenchCLI/CliTools/MarkdownMapWriter.cs
new file mode 100644
index 00000000..9022fe35
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/MarkdownMapWriter.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public class MarkdownMapWriter : IMapWriter
+ {
+ private TextWriter writer;
+
+ public MarkdownMapWriter(Stream stream)
+ {
+ writer = new StreamWriter(stream, new UTF8Encoding(false));
+ }
+
+ public void Dispose()
+ {
+ if (writer != null)
+ {
+ writer.Flush();
+ writer.Dispose();
+ }
+ writer = null;
+ }
+
+ public void Write(string key, object value)
+ {
+ if (value == null) WriteNull(key);
+ else if (value is bool) WriteValue(key, (bool)value);
+ else if (value is string) WriteValue(key, (string)value);
+ else if (value is string[]) WriteValue(key, (string[])value);
+ else if (value is IDictionary) WriteValue(key, (IDictionary)value);
+ else WriteUnknown(key);
+ }
+
+
+ private string EscapeValue(string value)
+ {
+ if (value == null)
+ return "";
+ if (Uri.IsWellFormedUriString(value, UriKind.Absolute))
+ return "<" + value + ">";
+ return "`" + value + "`";
+ }
+
+ private string EscapeValue(bool value)
+ {
+ return "`" + (value ? "true" : "false") + "`";
+ }
+
+ private void WriteValue(string key, IDictionary value)
+ {
+ writer.WriteLine("* `{0}`:", key);
+ foreach (var kvp in value)
+ {
+ writer.WriteLine(" + `{0}`: {1}", kvp.Key, EscapeValue(kvp.Value));
+ }
+ }
+
+ private void WriteValue(string key, string[] value)
+ {
+ var sum = 0;
+ var list = new List(value);
+ for (int i = 0; i < list.Count; i++)
+ {
+ sum += list[i].Length + 3;
+ list[i] = "`" + list[i] + "`";
+ }
+ if (sum <= 100)
+ {
+ writer.WriteLine("* `{0}`: {1}", key, string.Join(", ", list.ToArray()));
+ }
+ else
+ {
+ writer.WriteLine("* `{0}`:", key);
+ foreach (var item in value)
+ {
+ writer.WriteLine(" + {0}", EscapeValue(item));
+ }
+ }
+ }
+
+ private void WriteValue(string key, string value)
+ {
+ writer.WriteLine("* `{0}`: {1}", key, EscapeValue(value));
+ }
+
+ private void WriteValue(string key, bool value)
+ {
+ writer.WriteLine("* `{0}`: {1}", key, EscapeValue(value));
+ }
+
+ private void WriteNull(string key)
+ {
+ writer.WriteLine("* `{0}`:", key);
+ }
+
+ private void WriteUnknown(string key)
+ {
+ writer.WriteLine("* `{0}`: _Unsupported Data Type_", key);
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/MarkdownTableWriter.cs b/BenchManager/BenchCLI/CliTools/MarkdownTableWriter.cs
new file mode 100644
index 00000000..a9579729
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/MarkdownTableWriter.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ class MarkdownTableWriter : ITableWriter
+ {
+ private string[] columns;
+ private List rows;
+
+ private TextWriter writer;
+
+ public MarkdownTableWriter(TextWriter writer)
+ {
+ if (writer == null) throw new ArgumentNullException();
+ this.writer = writer;
+ }
+
+ public MarkdownTableWriter(Stream target)
+ : this(new StreamWriter(target, new UTF8Encoding(false)))
+ {
+ }
+
+ public void Initialize(params string[] columns)
+ {
+ this.columns = columns;
+ this.rows = new List();
+ }
+
+ public void Write(params object[] values)
+ {
+ if (writer == null) throw new ObjectDisposedException(nameof(ConsoleTableWriter));
+ if (columns == null) throw new InvalidOperationException();
+ if (values.Length != columns.Length) throw new ArgumentException("Incorrect number of values.");
+ var row = new List();
+ for (int i = 0; i < values.Length; i++)
+ {
+ row.Add(Format(values[i]));
+ }
+ rows.Add(row.ToArray());
+ }
+
+ private string Format(object value)
+ {
+ if (value == null) return string.Empty;
+ if (value is bool) return ((bool)value) ? "`true`" : "`false`";
+ if (value is string) return (string)value;
+ return "_UNSUPPORTED TYPE_";
+ }
+
+ private void WriteTable()
+ {
+ var c = columns.Length;
+ var lengths = new int[c];
+ for (int i = 0; i < c; i++) lengths[i] = columns[i].Length;
+ foreach (var row in rows)
+ {
+ for (int i = 0; i < c; i++) lengths[i] = Math.Max(lengths[i], row[i].Length);
+ }
+ Write("| ");
+ for (int i = 0; i < c; i++)
+ {
+ if (i > 0) Write(" | ");
+ Write(columns[i].PadRight(lengths[i]));
+ }
+ Write(" |");
+ NewLine();
+ Write("|:");
+ for (int i = 0; i < c; i++)
+ {
+ if (i > 0) Write("-|:");
+ Write(new string('-', lengths[i]));
+ }
+ Write("-|");
+ NewLine();
+ foreach (var row in rows)
+ {
+ Write("| ");
+ for (int i = 0; i < c; i++)
+ {
+ if (i > 0) Write(" | ");
+ Write(row[i].PadRight(lengths[i]));
+ }
+ Write(" |");
+ NewLine();
+ }
+ }
+
+ private void Write(string value)
+ {
+ writer.Write(value);
+ }
+
+ private void NewLine()
+ {
+ writer.WriteLine();
+ }
+
+ public void Dispose()
+ {
+ if (writer == null) return;
+ WriteTable();
+ writer.Dispose();
+ writer = null;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/CliTools/TableWriterFactory.cs b/BenchManager/BenchCLI/CliTools/TableWriterFactory.cs
new file mode 100644
index 00000000..09c59092
--- /dev/null
+++ b/BenchManager/BenchCLI/CliTools/TableWriterFactory.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Mastersign.CliTools
+{
+ public static class TableWriterFactory
+ {
+ public static ITableWriter Create(DataOutputFormat format)
+ {
+ switch (format)
+ {
+ case DataOutputFormat.Plain:
+ return new ConsoleTableWriter();
+ case DataOutputFormat.Markdown:
+ return new MarkdownTableWriter(Console.OpenStandardOutput());
+ //case OutputFormat.JSON:
+ // return new JsonPropertyWriter(Console.OpenStandardOutput());
+ //case OutputFormat.XML:
+ // return new XmlPropertyWriter(Console.OpenStandardOutput());
+ default:
+ throw new NotSupportedException();
+ }
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppActivateCommand.cs b/BenchManager/BenchCLI/Commands/AppActivateCommand.cs
new file mode 100644
index 00000000..ec2d73fd
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppActivateCommand.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppActivateCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "activate";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command marks an app as activated.")
+ .End(BlockType.Paragraph)
+ .Begin(BlockType.Paragraph)
+ .Text("To actually install the app, you have to run the ").Keyword("setup").Text(" command.")
+ .End(BlockType.Paragraph)
+ .Begin(BlockType.Paragraph)
+ .Text("If the app is currently active as a dependency, it is marked as activated anyways.").LineBreak()
+ .Text("If the app is required by Bench, it is not marked as activated.").LineBreak()
+ .Text("If the app is marked as deactivated, this mark is removed.")
+ .End(BlockType.Paragraph);
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to activate.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var cfg = LoadConfiguration();
+
+ if (!cfg.ContainsGroup(appId))
+ {
+ WriteError("The app '{0}' was not found.", appId);
+ return false;
+ }
+
+ var activationFile = cfg.GetStringValue(PropertyKeys.AppActivationFile);
+ if (!File.Exists(activationFile))
+ {
+ WriteError("The activation file for apps was not found.");
+ WriteLine(" " + activationFile);
+ return false;
+ }
+ WriteDetail("Found activation file: " + activationFile);
+ var deactivationFile = cfg.GetStringValue(PropertyKeys.AppDeactivationFile);
+ if (!File.Exists(deactivationFile))
+ {
+ WriteError("The deactivation file for apps was not found.");
+ WriteLine(" " + deactivationFile);
+ return false;
+ }
+ WriteDetail("Found deactivation file: " + deactivationFile);
+ var activationList = new ActivationFile(activationFile);
+ var deactivationList = new ActivationFile(deactivationFile);
+
+ var app = cfg.Apps[appId];
+ WriteDetail("App ID: " + appId);
+ if (app.IsDeactivated)
+ {
+ WriteDetail("Removing the app from the deactivation file.");
+ deactivationList.SignOut(appId);
+ }
+ if (app.IsRequired)
+ {
+ WriteDetail("The app is required and does not need to be activated.");
+ return true;
+ }
+ if (app.IsActivated)
+ {
+ WriteDetail("The app is already activated.");
+ return true;
+ }
+ WriteDetail("Adding the app to the activation file.");
+ activationList.SignIn(appId);
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppCommand.cs b/BenchManager/BenchCLI/Commands/AppCommand.cs
new file mode 100644
index 00000000..26f5aa8c
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppCommand.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppCommand : BenchCommand
+ {
+ private readonly BenchCommand appInfoCommand = new AppInfoCommand();
+ private readonly BenchCommand appPropertyCommand = new AppPropertyCommand();
+ private readonly BenchCommand appListPropertiesCommand = new AppListPropertiesCommand();
+ private readonly BenchCommand appActivateCommand = new AppActivateCommand();
+ private readonly BenchCommand appDeactivateCommand = new AppDeactivateCommand();
+ private readonly BenchCommand appDownloadCommand = new AppDownloadCommand();
+ private readonly BenchCommand appInstallCommand = new AppInstallCommand();
+ private readonly BenchCommand appReinstallCommand = new AppReinstallCommand();
+ private readonly BenchCommand appUpgradeCommand = new AppUpgradeCommand();
+ private readonly BenchCommand appUninstallCommand = new AppUninstallCommand();
+ private readonly BenchCommand appExecuteCommand = new AppExecuteCommand();
+
+ public override string Name => "app";
+
+ public AppCommand()
+ {
+ RegisterSubCommand(appInfoCommand);
+ RegisterSubCommand(appPropertyCommand);
+ RegisterSubCommand(appListPropertiesCommand);
+ RegisterSubCommand(appActivateCommand);
+ RegisterSubCommand(appDeactivateCommand);
+ RegisterSubCommand(appDownloadCommand);
+ RegisterSubCommand(appInstallCommand);
+ RegisterSubCommand(appReinstallCommand);
+ RegisterSubCommand(appUpgradeCommand);
+ RegisterSubCommand(appUninstallCommand);
+ RegisterSubCommand(appExecuteCommand);
+ }
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(Docs.BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command allows interacting with Bench apps.")
+ .End(Docs.BlockType.Paragraph)
+ .Paragraph("Use the sub-commands to select the kind of interaction.");
+
+ var commandProperty = new CommandArgument(appPropertyCommand.Name, 'p', "prop");
+ commandProperty.Description
+ .Text("Reads an app property value.");
+ commandProperty.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appPropertyCommand);
+
+ var commandInfo = new CommandArgument(appInfoCommand.Name, 'i');
+ commandInfo.Description
+ .Text("Shows a detailed, human readable info of an app.");
+ commandInfo.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appInfoCommand);
+
+ var commandListProperties = new CommandArgument(appListPropertiesCommand.Name, 'l', "list");
+ commandListProperties.Description
+ .Text("Lists the properties of an app.");
+ commandListProperties.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appListPropertiesCommand);
+
+ var commandActivate = new CommandArgument(appActivateCommand.Name, 'a', "enable");
+ commandActivate.Description
+ .Text("Activates an app.");
+ commandActivate.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appActivateCommand);
+
+ var commandDeactivate = new CommandArgument(appDeactivateCommand.Name, 'd', "disable");
+ commandDeactivate.Description
+ .Text("Deactivates an app.");
+ commandDeactivate.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appDeactivateCommand);
+
+ var commandDownload = new CommandArgument(appDownloadCommand.Name, 'c', "cache");
+ commandDownload.Description
+ .Text("Downloads an apps resource.");
+ commandDownload.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appDownloadCommand);
+
+ var commandInstall = new CommandArgument(appInstallCommand.Name, 's', "setup");
+ commandInstall.Description
+ .Text("Installs an app, regardless of its activation state.");
+ commandInstall.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appInstallCommand);
+
+ var commandReinstall = new CommandArgument(appReinstallCommand.Name, 'r');
+ commandReinstall.Description
+ .Text("Reinstalls an app.");
+ commandReinstall.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appReinstallCommand);
+
+ var commandUpgrade = new CommandArgument(appUpgradeCommand.Name, 'u');
+ commandUpgrade.Description
+ .Text("Upgrades an app.");
+ commandUpgrade.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appUpgradeCommand);
+
+ var commandUninstall = new CommandArgument(appUninstallCommand.Name, 'x', "remove");
+ commandUninstall.Description
+ .Text("Uninstalls an app, regardless of its activation state.");
+ commandUninstall.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appUninstallCommand);
+
+ var commandExecute = new CommandArgument(appExecuteCommand.Name, 'e', "exec", "launch", "run");
+ commandExecute.Description
+ .Text("Starts an apps main executable.");
+ commandExecute.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, appExecuteCommand);
+
+ parser.RegisterArguments(
+ commandProperty,
+ commandInfo,
+ commandListProperties,
+ commandActivate,
+ commandDeactivate,
+ commandDownload,
+ commandInstall,
+ commandReinstall,
+ commandUpgrade,
+ commandUninstall,
+ commandExecute);
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppDeactivateCommand.cs b/BenchManager/BenchCLI/Commands/AppDeactivateCommand.cs
new file mode 100644
index 00000000..1d02b1b4
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppDeactivateCommand.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppDeactivateCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "deactivate";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command removes an app from the activation list or marks it as deactivated.")
+ .End(BlockType.Paragraph)
+ .Begin(BlockType.Paragraph)
+ .Text("To actually uninstall the app, you have to run the ").Keyword("setup").Text(" command.")
+ .End(BlockType.Paragraph)
+ .Begin(BlockType.Paragraph)
+ .Text("If the app is currently on the activation list, it is removed from it.").LineBreak()
+ .Text("If the app is required by Bench, or as a dependency, it is marked as deactivated.")
+ .End(BlockType.Paragraph);
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to deactivate.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var cfg = LoadConfiguration();
+
+ if (!cfg.ContainsGroup(appId))
+ {
+ WriteError("The app '{0}' was not found.", appId);
+ return false;
+ }
+
+ var activationFile = cfg.GetStringValue(PropertyKeys.AppActivationFile);
+ if (!File.Exists(activationFile))
+ {
+ WriteError("The activation file for apps was not found.");
+ WriteLine(" " + activationFile);
+ return false;
+ }
+ WriteDetail("Found activation file: " + activationFile);
+ var deactivationFile = cfg.GetStringValue(PropertyKeys.AppDeactivationFile);
+ if (!File.Exists(deactivationFile))
+ {
+ WriteError("The deactivation file for apps was not found.");
+ WriteLine(" " + deactivationFile);
+ return false;
+ }
+ WriteDetail("Found deactivation file: " + deactivationFile);
+ var activationList = new ActivationFile(activationFile);
+ var deactivationList = new ActivationFile(deactivationFile);
+
+ var app = cfg.Apps[appId];
+ WriteDetail("App ID: " + appId);
+ if (app.IsDeactivated)
+ {
+ WriteDetail("The app is allready deactivated.");
+ return true;
+ }
+ if (app.IsActivated)
+ {
+ WriteDetail("Removing the app from the activation file.");
+ activationList.SignOut(appId);
+ }
+ if (app.IsRequired)
+ {
+ WriteDetail("The app is required by Bench.");
+ if (AskForAssurance("Are you sure you want to deactivate an app, which is required by Bench?"))
+ {
+ WriteDetail("Adding the app to the deactivation file.");
+ deactivationList.SignIn(appId);
+ return true;
+ }
+ else return false;
+ }
+ if (app.IsDependency)
+ {
+ WriteDetail("The app is required by the following apps: " + string.Join(", ", app.Responsibilities));
+ if (AskForAssurance("Are you sure you want to deactivate an app, which is required by another app?"))
+ {
+ WriteDetail("Adding the app to the deactivation file.");
+ deactivationList.SignIn(appId);
+ return true;
+ }
+ else return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppDownloadCommand.cs b/BenchManager/BenchCLI/Commands/AppDownloadCommand.cs
new file mode 100644
index 00000000..ba52c5e3
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppDownloadCommand.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppDownloadCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "download";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command downloads the app resources, in case it is not cached already.")
+ .End(BlockType.Paragraph);
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to download the resource for.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ return RunManagerTask(mgr => mgr.DownloadAppResource(
+ Arguments.GetPositionalValue(POSITIONAL_APP_ID)));
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppExecuteCommand.cs b/BenchManager/BenchCLI/Commands/AppExecuteCommand.cs
new file mode 100644
index 00000000..420f0ef9
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppExecuteCommand.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppExecuteCommand : BenchCommand
+ {
+ private const string FLAG_DETACHED = "detached";
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "execute";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command starts the main executable of the specified app.")
+ .End(BlockType.Paragraph);
+
+ var flagDetached = new FlagArgument(FLAG_DETACHED, 'd', "async");
+ flagDetached.Description
+ .Text("Do not wait for the end of the process.");
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to execute.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ flagDetached,
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var cfg = LoadConfiguration();
+
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+
+ if (!cfg.ContainsGroup(appId))
+ {
+ WriteError("The app '{0}' was not found.", appId);
+ return false;
+ }
+
+ var app = cfg.Apps[appId];
+ if (app.Exe == null)
+ {
+ WriteError("The app '{0}' has no main executable.", app.Label);
+ return false;
+ }
+ WriteDetail("Found apps executable: {0}", app.Exe);
+
+ var detached = Arguments.GetFlag(FLAG_DETACHED);
+ WriteDetail("Starting app '{0}' {1} ...", app.Label, detached ? "detached" : "synchronously");
+
+ using (var mgr = new DefaultBenchManager(cfg))
+ {
+ mgr.Verbose = Verbose;
+ if (detached)
+ {
+ mgr.ProcessExecutionHost.StartProcess(mgr.Env,
+ cfg.BenchRootDir, app.Exe, CommandLine.FormatArgumentList(args),
+ null, ProcessMonitoring.ExitCode);
+ return true;
+ }
+ else
+ {
+ var r = mgr.ProcessExecutionHost.RunProcess(mgr.Env,
+ cfg.BenchRootDir, app.Exe, CommandLine.FormatArgumentList(args),
+ ProcessMonitoring.ExitCodeAndOutput);
+ Console.Write(r.Output);
+ return r.ExitCode == 0;
+ }
+ }
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppInfoCommand.cs b/BenchManager/BenchCLI/Commands/AppInfoCommand.cs
new file mode 100644
index 00000000..ed08b22b
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppInfoCommand.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppInfoCommand : BenchCommand
+ {
+ private const string OPTION_FORMAT = "format";
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ private const DocumentOutputFormat DEF_FORMAT = DocumentOutputFormat.Plain;
+
+ public override string Name => "info";
+
+ private DocumentOutputFormat Format = DEF_FORMAT;
+
+ private BenchConfiguration config;
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command displayes a detailed description for an app in human readable form.")
+ .End(BlockType.Paragraph);
+
+ var optionFormat = new EnumOptionArgument(
+ OPTION_FORMAT, 'f', DEF_FORMAT, "fmt");
+ optionFormat.Description
+ .Text("Specify the output format.");
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to display the description for.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ optionFormat,
+ positionalAppId);
+ }
+
+ protected override bool ValidateArguments()
+ {
+ Format = (DocumentOutputFormat)Enum.Parse(typeof(DocumentOutputFormat),
+ Arguments.GetOptionValue(OPTION_FORMAT, DEF_FORMAT.ToString()), true);
+
+ return true;
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+
+ config = LoadConfiguration();
+ if (!config.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+
+ var app = config.Apps[appId];
+ using (var w = DocumentWriterFactory.Create(Format))
+ {
+ WriteAppInfo(app, w);
+ }
+
+ return true;
+ }
+
+ private void WriteAppInfo(AppFacade app, DocumentWriter writer)
+ {
+ writer.Begin(BlockType.Document);
+ writer.Title(app.Label);
+ writer.Headline2("app_" + app.ID + "_description", "Description");
+ writer.Begin(BlockType.List);
+ WriteProperty(writer, "ID", app.ID, InlineType.Keyword);
+ WriteProperty(writer, "Label", app.Label);
+ if (app.IsInstalled && app.Launcher != null)
+ {
+ WriteProperty(writer, "Launcher", app.Launcher);
+ }
+ WriteProperty(writer, "App Type", app.Typ, InlineType.Keyword);
+ WriteProperty(writer, "Version", app.Version ?? "latest", InlineType.Keyword);
+ writer.End(BlockType.List);
+ writer.Headline2("app_" + app.ID + "_state", "State");
+ writer.Paragraph(app.LongStatus);
+ var dependencies = app.Dependencies;
+ var responsibilities = app.Responsibilities;
+ if (dependencies.Length > 0 || responsibilities.Length > 0)
+ {
+ writer.Headline2("app_" + app.ID + "_relations", "Relationships");
+ writer.Begin(BlockType.List);
+ if (dependencies.Length > 0)
+ {
+ Array.Sort(dependencies);
+ writer.Begin(BlockType.ListItem)
+ .Text("Dependencies:")
+ .Begin(BlockType.List);
+ foreach (var d in dependencies)
+ {
+ writer.Begin(BlockType.ListItem)
+ .Keyword(d)
+ .End(BlockType.ListItem);
+ }
+ writer.End(BlockType.List);
+ writer.End(BlockType.ListItem);
+ }
+ if (responsibilities.Length > 0)
+ {
+ Array.Sort(responsibilities);
+ writer.Begin(BlockType.ListItem)
+ .Text("Responsibilities:")
+ .Begin(BlockType.List);
+ foreach (var r in app.Responsibilities)
+ {
+ writer.Begin(BlockType.ListItem)
+ .Keyword(r)
+ .End(BlockType.ListItem);
+ }
+ writer.End(BlockType.List);
+ writer.End(BlockType.ListItem);
+ }
+ writer.End(BlockType.List);
+ }
+ writer.Headline2("app_" + app.ID + "_paths", "Paths and Resources");
+ writer.Begin(BlockType.List);
+ if (app.IsInstalled)
+ {
+ WriteProperty(writer, "Installation Dir", app.Dir, InlineType.Keyword);
+ WriteProperty(writer, "Main Executable", app.Exe, InlineType.Keyword);
+ }
+ if (app.IsResourceCached)
+ {
+ WriteProperty(writer, "Cached Resource",
+ Path.Combine(config.GetStringValue(PropertyKeys.DownloadDir),
+ app.ResourceArchiveName ?? app.ResourceFileName),
+ InlineType.Keyword);
+ }
+ if (app.Url != null)
+ {
+ WriteProperty(writer, "Resource URL", app.Url);
+ }
+ writer.End(BlockType.List);
+
+ writer.End(BlockType.Document);
+ }
+
+ private void WriteProperty(DocumentWriter writer, string key, string value)
+ {
+ writer
+ .Begin(BlockType.ListItem)
+ .Text(key).Text(": ").Text(value)
+ .End(BlockType.ListItem);
+ }
+ private void WriteProperty(DocumentWriter writer, string key, string value, InlineType type)
+ {
+ writer.Begin(BlockType.ListItem);
+ writer.Text(key).Text(": ");
+ writer.Inline(type, value);
+ writer.End(BlockType.ListItem);
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppInstallCommand.cs b/BenchManager/BenchCLI/Commands/AppInstallCommand.cs
new file mode 100644
index 00000000..f0092b8f
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppInstallCommand.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppInstallCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "install";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command installes the specified app, regardless of its activation state.")
+ .End(BlockType.Paragraph)
+ .Paragraph("Missing app resources are downloaded automatically.");
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to install.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var cfg = LoadConfiguration();
+ if (!cfg.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+ return RunManagerTask(mgr => mgr.InstallApp(appId));
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppListPropertiesCommand.cs b/BenchManager/BenchCLI/Commands/AppListPropertiesCommand.cs
new file mode 100644
index 00000000..8d44631f
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppListPropertiesCommand.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppListPropertiesCommand : BenchCommand
+ {
+ private const string FLAG_RAW = "raw";
+ private const string OPTION_FORMAT = "format";
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ private const DataOutputFormat DEF_FORMAT = DataOutputFormat.Plain;
+
+ public override string Name => "list-properties";
+
+ private bool ShowRaw => Arguments.GetFlag(FLAG_RAW);
+
+ private DataOutputFormat Format = DataOutputFormat.Plain;
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command displayes the properties of an app.")
+ .End(BlockType.Paragraph)
+ .Paragraph("This command supports different output formats. "
+ + "And you can choose between the expanded or the raw properties.");
+
+ var flagRaw = new FlagArgument(FLAG_RAW, 'r');
+ flagRaw.Description
+ .Text("Shows the raw properties without expansion and default values.");
+
+ var optionFormat = new EnumOptionArgument(
+ OPTION_FORMAT, 'f', DEF_FORMAT, "fmt");
+ optionFormat.Description
+ .Text("Specify the output format.");
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app of which the properties are to be listed.");
+ positionalAppId.PossibleValueInfo
+ .Text("The apps ID, an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ flagRaw,
+ optionFormat,
+ positionalAppId);
+ }
+
+ protected override bool ValidateArguments()
+ {
+ Format = (DataOutputFormat)Enum.Parse(typeof(DataOutputFormat),
+ Arguments.GetOptionValue(OPTION_FORMAT, DEF_FORMAT.ToString()), true);
+
+ return true;
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+
+ var cfg = LoadConfiguration();
+ if (!cfg.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+
+ var app = cfg.Apps[appId];
+ if (ShowRaw)
+ PrintRawProperties(cfg, appId);
+ else
+ PrintProperties(app);
+ return true;
+ }
+
+ private void PrintProperties(AppFacade app)
+ {
+ var knownProperties = app.KnownProperties;
+ var unknownProperties = app.UnknownProperties;
+ var lookup = new Dictionary();
+ var names = new List();
+ foreach (var kvp in knownProperties)
+ {
+ lookup[kvp.Key] = kvp.Value;
+ if (!names.Contains(kvp.Key)) names.Add(kvp.Key);
+ }
+ foreach (var kvp in unknownProperties)
+ {
+ lookup[kvp.Key] = kvp.Value;
+ if (!names.Contains(kvp.Key)) names.Add(kvp.Key);
+ }
+ names.Sort();
+
+ using (var w = MapWriterFactory.Create(Format))
+ {
+ w.Write("ID", app.ID);
+ foreach (var p in names)
+ {
+ w.Write(p, lookup[p]);
+ }
+ }
+ }
+
+ private void PrintRawProperties(BenchConfiguration cfg, string appId)
+ {
+ using (var w = MapWriterFactory.Create(Format))
+ {
+ w.Write("ID", appId);
+ foreach (var name in cfg.PropertyNames(appId))
+ {
+ w.Write(name, cfg.GetRawGroupValue(appId, name));
+ }
+ }
+ }
+
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppPropertyCommand.cs b/BenchManager/BenchCLI/Commands/AppPropertyCommand.cs
new file mode 100644
index 00000000..89ee39ee
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppPropertyCommand.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppPropertyCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+ private const string POSITIONAL_PROPERTY_NAME = "Property Name";
+
+ public override string Name => "property";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command reads the value of an app property.")
+ .End(BlockType.Paragraph);
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to get the property from.");
+ positionalAppId.PossibleValueInfo
+ .Text("The apps ID, an alphanumeric string without whitespace.");
+
+ var positionalPropertyName = new PositionalArgument(POSITIONAL_PROPERTY_NAME,
+ ArgumentValidation.IsIdString,
+ 2);
+ positionalPropertyName.Description
+ .Text("Specifies the property to read.");
+ positionalPropertyName.PossibleValueInfo
+ .Text("The property name, an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId,
+ positionalPropertyName);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var propertyName = Arguments.GetPositionalValue(POSITIONAL_PROPERTY_NAME);
+
+ var cfg = LoadConfiguration();
+ if (!cfg.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+ WriteDetail("App ID: " + appId);
+ WriteDetail("Property: " + propertyName);
+ PropertyWriter.WritePropertyValue(cfg.GetGroupValue(appId, propertyName));
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppReinstallCommand.cs b/BenchManager/BenchCLI/Commands/AppReinstallCommand.cs
new file mode 100644
index 00000000..1a15b897
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppReinstallCommand.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppReinstallCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "reinstall";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command reinstalles the specified app.")
+ .End(BlockType.Paragraph);
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to reinstall.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var cfg = LoadConfiguration();
+ if (!cfg.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+ return RunManagerTask(mgr => mgr.ReinstallApp(appId));
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppUninstallCommand.cs b/BenchManager/BenchCLI/Commands/AppUninstallCommand.cs
new file mode 100644
index 00000000..35be2687
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppUninstallCommand.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppUninstallCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "uninstall";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command uninstalles the specified app, regardless of its activation state.")
+ .End(BlockType.Paragraph);
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to install.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var cfg = LoadConfiguration();
+ if (!cfg.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+ return RunManagerTask(mgr => mgr.UninstallApp(appId));
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AppUpgradeCommand.cs b/BenchManager/BenchCLI/Commands/AppUpgradeCommand.cs
new file mode 100644
index 00000000..50f110d7
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AppUpgradeCommand.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AppUpgradeCommand : BenchCommand
+ {
+ private const string POSITIONAL_APP_ID = "App ID";
+
+ public override string Name => "upgrade";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command upgrades the specified app to the most current release.")
+ .End(BlockType.Paragraph)
+ .Paragraph("Updates app resources are downloaded automatically.");
+
+ var positionalAppId = new PositionalArgument(POSITIONAL_APP_ID,
+ ArgumentValidation.IsIdString,
+ 1);
+ positionalAppId.Description
+ .Text("Specifies the app to upgrade.");
+ positionalAppId.PossibleValueInfo
+ .Text("An app ID is an alphanumeric string without whitespace.");
+
+ parser.RegisterArguments(
+ positionalAppId);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var appId = Arguments.GetPositionalValue(POSITIONAL_APP_ID);
+ var cfg = LoadConfiguration();
+ if (!cfg.Apps.Exists(appId))
+ {
+ WriteError("Unknown app ID: " + appId);
+ return false;
+ }
+ return RunManagerTask(mgr => mgr.UpgradeApp(appId));
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/AutoSetupCommand.cs b/BenchManager/BenchCLI/Commands/AutoSetupCommand.cs
new file mode 100644
index 00000000..ac517821
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/AutoSetupCommand.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class AutoSetupCommand : BenchCommand
+ {
+ public override string Name => "setup";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command runs the auto-setup for the active Bench apps.")
+ .End(BlockType.Paragraph);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ => RunManagerTask(mgr => mgr.AutoSetup());
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/BenchCommand.cs b/BenchManager/BenchCLI/Commands/BenchCommand.cs
new file mode 100644
index 00000000..b07b71ad
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/BenchCommand.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ abstract class BenchCommand : CommandBase
+ {
+ #region Bubbeling Properties
+
+ private string rootPath;
+
+ public string RootPath
+ {
+ get { return (Parent as BenchCommand)?.RootPath ?? rootPath; }
+ set { rootPath = value; }
+ }
+
+ private string logFile;
+
+ public string LogFile
+ {
+ get { return (Parent as BenchCommand)?.LogFile ?? logFile; }
+ set { logFile = value; }
+ }
+
+ #endregion
+
+ protected static string BenchBinDirPath()
+ {
+ return Path.GetDirectoryName(Program.CliExecutable());
+ }
+
+ protected static string DefaultRootPath()
+ {
+ var rootPath = Path.GetFullPath(Path.Combine(Path.Combine(BenchBinDirPath(), ".."), ".."));
+ return BenchConfiguration.IsValidBenchRoot(rootPath) ? rootPath : null;
+ }
+
+ protected static string DashboardExecutable(string rootDir = null)
+ {
+ if (!BenchTasks.IsDashboardSupported) return null;
+ var path = rootDir != null
+ ? Path.Combine(Path.Combine(Path.Combine(rootDir, "auto"), "bin"), "BenchDashboard.exe")
+ : Path.Combine(BenchBinDirPath(), "BenchDashboard.exe");
+ return File.Exists(path) ? path : null;
+ }
+
+ #region Task Helper
+
+ protected BenchConfiguration LoadConfiguration(bool withApps = true)
+ {
+ var cfg = new BenchConfiguration(RootPath, withApps, true, true);
+ if (LogFile != null) cfg.SetValue(PropertyKeys.LogFile, LogFile);
+ return cfg;
+ }
+
+ protected DefaultBenchManager CreateManager()
+ {
+ return new DefaultBenchManager(LoadConfiguration())
+ {
+ Verbose = Verbose
+ };
+ }
+
+ protected bool RunManagerTask(ManagerTask task)
+ {
+ using (var mgr = CreateManager())
+ {
+ return task(mgr);
+ }
+ }
+
+ #endregion
+ }
+
+ delegate bool ManagerTask(DefaultBenchManager mgr);
+}
diff --git a/BenchManager/BenchCLI/Commands/ConfigCommand.cs b/BenchManager/BenchCLI/Commands/ConfigCommand.cs
new file mode 100644
index 00000000..381567b1
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/ConfigCommand.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class ConfigCommand : BenchCommand
+ {
+ public override string Name => "config";
+
+ private readonly BenchCommand getCommand = new ConfigGetCommand();
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command gives access to the Bench user configuration.")
+ .End(BlockType.Paragraph);
+
+ var commandGet = new CommandArgument(getCommand.Name, 'g', "read");
+ commandGet.Description
+ .Text("Reads a configuration value.");
+ commandGet.SyntaxInfo
+ .Append(HelpFormatter.CommandSyntax, getCommand);
+
+ parser.RegisterArguments(
+ commandGet);
+ }
+
+ public ConfigCommand()
+ {
+ RegisterSubCommand(getCommand);
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/ConfigGetCommand.cs b/BenchManager/BenchCLI/Commands/ConfigGetCommand.cs
new file mode 100644
index 00000000..873d1214
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/ConfigGetCommand.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class ConfigGetCommand : BenchCommand
+ {
+ private static string POSITIONAL_PROPERTY_NAME = "property-name";
+
+ public override string Name => "get";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command reads a configuration value.")
+ .End(BlockType.Paragraph);
+
+ var positionalPropertyName = new PositionalArgument(POSITIONAL_PROPERTY_NAME,
+ null, 1);
+ positionalPropertyName.Description
+ .Text("The name of the configuration property to read.");
+
+ parser.RegisterArguments(
+ positionalPropertyName);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var propertyName = Arguments.GetPositionalValue(POSITIONAL_PROPERTY_NAME);
+
+ var cfg = LoadConfiguration(false);
+ if (!cfg.ContainsValue(propertyName))
+ {
+ WriteError("Unknown property name: " + propertyName);
+ return false;
+ }
+ WriteDetail("Property: " + propertyName);
+ PropertyWriter.WritePropertyValue(cfg.GetValue(propertyName));
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/DashboardCommand.cs b/BenchManager/BenchCLI/Commands/DashboardCommand.cs
new file mode 100644
index 00000000..005bb9f6
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/DashboardCommand.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class DashboardCommand : BenchCommand
+ {
+ public override string Name => "dashboard";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command starts the graphical user interface ")
+ .Emph("Bench Dashboard").Text(".")
+ .End(BlockType.Paragraph);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var path = DashboardExecutable(RootPath);
+ if (path == null || !File.Exists(path))
+ {
+ WriteError("Could not find the executable of the Bench Dashboard.");
+ return false;
+ }
+
+ System.Diagnostics.Process.Start(path);
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/DownloadsCommand.cs b/BenchManager/BenchCLI/Commands/DownloadsCommand.cs
new file mode 100644
index 00000000..762a1365
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/DownloadsCommand.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class DownloadsCommand : BenchCommand
+ {
+ private const string COMMAND_CLEAN = "clean";
+ private const string COMMAND_PURGE = "purge";
+ private const string COMMAND_DOWNLOAD = "download";
+
+ public override string Name => "downloads";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command manages the cached app resources.")
+ .End(BlockType.Paragraph);
+
+ var commandClean = new CommandArgument(COMMAND_CLEAN, 'c', "cl");
+ commandClean.Description
+ .Text("Deletes obsolete app resources.");
+
+ var commandPurge = new CommandArgument(COMMAND_PURGE, 'x');
+ commandPurge.Description
+ .Text("Deletes all cached app resources.");
+
+ var commandDownload = new CommandArgument(COMMAND_DOWNLOAD, 'd', "dl");
+ commandDownload.Description
+ .Text("Downloads the app resources for all active apps.");
+
+ parser.RegisterArguments(
+ commandClean,
+ commandPurge,
+ commandDownload);
+ }
+
+ protected override bool ExecuteUnknownSubCommand(string command, string[] args)
+ {
+ switch (command)
+ {
+ case COMMAND_CLEAN:
+ return TaskClean(args);
+ case COMMAND_PURGE:
+ return TaskPurge(args);
+ case COMMAND_DOWNLOAD:
+ return TaskDownload(args);
+
+ default:
+ WriteError("Unsupported command: " + command + ".");
+ return false;
+ }
+ }
+
+ private bool TaskClean(string[] args)
+ => RunManagerTask(mgr => mgr.CleanUpAppResources());
+
+ private bool TaskPurge(string[] args)
+ {
+ if (!AskForAssurance("Are you sure, you want to delete all downloaded app resources?"))
+ {
+ return false;
+ }
+ return RunManagerTask(mgr => mgr.DeleteAppResources());
+ }
+
+ private bool TaskDownload(string[] args)
+ => RunManagerTask(mgr => mgr.DownloadAppResources());
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/HelpCommand.cs b/BenchManager/BenchCLI/Commands/HelpCommand.cs
new file mode 100644
index 00000000..c4762124
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/HelpCommand.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class HelpCommand : BenchCommand
+ {
+ private const string FLAG_NO_TITLE = "no-title";
+ private const string FLAG_NO_VERSION = "no-version";
+ private const string FLAG_NO_INDEX = "no-index";
+ private const string FLAG_APPEND = "append";
+ private const string OPTION_TARGET_FILE = "target-file";
+
+ public override string Name => "help";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command displays the full help for all commands.")
+ .End(BlockType.Paragraph);
+
+ var flagNoTitle = new FlagArgument(FLAG_NO_TITLE, 't');
+ flagNoTitle.Description
+ .Text("Suppress the output of the tool name as the document title.");
+
+ var flagNoVersion = new FlagArgument(FLAG_NO_VERSION, 'v');
+ flagNoVersion.Description
+ .Text("Suppress the output of the tool version number.");
+
+ var flagNoIndex = new FlagArgument(FLAG_NO_INDEX, 'i');
+ flagNoIndex.Description
+ .Text("Suppress the index of the commands.");
+
+ var flagAppend = new FlagArgument(FLAG_APPEND, 'a');
+ flagAppend.Description
+ .Text("Append to an existing file, in case a target file is specified.");
+
+ var optionTargetFile = new OptionArgument(OPTION_TARGET_FILE, 'o',
+ ArgumentValidation.IsValidPath,
+ "out");
+ optionTargetFile.Description
+ .Text("Specifies a target file to write the help content to.");
+ optionTargetFile.PossibleValueInfo
+ .Text("A path to a writable file. The target file will be created or overridden.");
+ optionTargetFile.DefaultValueInfo
+ .Text("None");
+
+ parser.RegisterArguments(
+ flagNoTitle,
+ flagNoVersion,
+ flagNoIndex,
+ flagAppend,
+ optionTargetFile);
+ }
+
+ private bool NoTitle => Arguments.GetFlag(FLAG_NO_TITLE);
+ private bool NoVersion => Arguments.GetFlag(FLAG_NO_VERSION);
+ private bool NoIndex => Arguments.GetFlag(FLAG_NO_INDEX);
+ private bool Append => Arguments.GetFlag(FLAG_APPEND);
+ private string TargetFile => Arguments.GetOptionValue(OPTION_TARGET_FILE);
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var targetFile = TargetFile;
+ Stream s = null;
+ if (targetFile != null)
+ {
+ try
+ {
+ s = File.Open(targetFile, Append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read);
+ }
+ catch (IOException exc)
+ {
+ WriteError("Failed to open the target file: " + exc.Message);
+ return false;
+ }
+ }
+ using (var w = DocumentWriterFactory.Create(HelpFormat, s))
+ {
+ RootCommand.PrintFullHelp(w, !NoTitle, !NoVersion, !NoIndex);
+ }
+ if (s != null) s.Close();
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/InitializeCommand.cs b/BenchManager/BenchCLI/Commands/InitializeCommand.cs
new file mode 100644
index 00000000..c1c38c7a
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/InitializeCommand.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class InitializeCommand : BenchCommand
+ {
+ public override string Name => "initialize";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command initializes the Bench configuration and starts the setup process.")
+ .End(BlockType.Paragraph);
+ }
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ BenchConfiguration cfgWithSite, cfgWithCoreApps, cfgWithCustom;
+
+ // 1. Initialize the site configuration, possibly with HTTP(S) proxy
+ cfgWithSite = BenchTasks.InitializeSiteConfiguration(RootPath);
+ if (cfgWithSite == null)
+ {
+ WriteInfo("Initialization canceled.");
+ return false;
+ }
+
+ // Create a manager object to get a download manager
+ using (var mgrWithSite = new DefaultBenchManager(cfgWithSite))
+ {
+ mgrWithSite.Verbose = Verbose;
+ // 2. Download the app libraries, listed in the Bench system and site configuration
+ if (!mgrWithSite.LoadAppLibraries())
+ {
+ WriteError("Loading the core app libraries failed.");
+ return false;
+ }
+ } // dispose the manager object
+
+ // Reload the configuration with the core app libraries
+ cfgWithCoreApps = new BenchConfiguration(RootPath, true, false, true);
+ cfgWithSite.InjectBenchInitializationProperties(cfgWithCoreApps);
+ cfgWithSite = null;
+
+ // Create a manager object to get an execution host
+ using (var mgrWithCoreApps = new DefaultBenchManager(cfgWithCoreApps))
+ {
+ cfgWithCoreApps = null;
+ mgrWithCoreApps.Verbose = Verbose;
+ // 3. Download and install required apps from the core app library
+ if (!mgrWithCoreApps.SetupRequiredApps())
+ {
+ WriteError("Initial app setup failed.");
+ return false;
+ }
+
+ // 4. Initialize the user configuration and reload the Bench configuration
+ cfgWithCustom = BenchTasks.InitializeCustomConfiguration(mgrWithCoreApps);
+ if (cfgWithCustom == null)
+ {
+ WriteInfo("Initialization canceled.");
+ return false;
+ }
+ } // dispose the manager object
+
+ // Create a manager object to get a download manager
+ using (var mgrWithCustom = new DefaultBenchManager(cfgWithCustom))
+ {
+ mgrWithCustom.Verbose = Verbose;
+ // 5. Download the app libraries, listed in the custom configuration
+ if (!mgrWithCustom.LoadAppLibraries())
+ {
+ WriteError("Loading the app libraries failed.");
+ return false;
+ }
+ }
+
+ // Check if the auto setup should be started right now
+ var autoSetup = cfgWithCustom.GetBooleanValue(PropertyKeys.WizzardStartAutoSetup, true);
+
+ var dashboardPath = DashboardExecutable();
+ if (dashboardPath != null)
+ {
+ // Kick-off the auto setup with the GUI
+ var arguments = string.Format("-root \"{0}\"", RootPath);
+ if (autoSetup)
+ {
+ arguments += " -setup";
+ }
+ var pi = new ProcessStartInfo()
+ {
+ FileName = dashboardPath,
+ Arguments = arguments,
+ UseShellExecute = false
+ };
+ System.Diagnostics.Process.Start(pi);
+ return true;
+ }
+ else if (autoSetup)
+ {
+ // Kick-off the auto setup with the CLI
+ return RunManagerTask(m => m.AutoSetup());
+ }
+ else
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/ListAppLibrariesCommand.cs b/BenchManager/BenchCLI/Commands/ListAppLibrariesCommand.cs
new file mode 100644
index 00000000..163256f9
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/ListAppLibrariesCommand.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class ListAppLibrariesCommand : BenchCommand
+ {
+ public override string Name => "applibs";
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command lists all loaded app libraries.")
+ .End(BlockType.Paragraph);
+ }
+
+ private DataOutputFormat Format => ((ListCommand)Parent).Format;
+
+ private bool OutputAsTable => ((ListCommand)Parent).OutputAsTable;
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var cfg = LoadConfiguration(withApps: true);
+ var appLibs = cfg.AppLibraries;
+ if (OutputAsTable)
+ {
+ using (var w = TableWriterFactory.Create(Format))
+ {
+ w.Initialize(new[] { "Order", "ID", "Path", "URL" });
+ for (int i = 0; i < appLibs.Length; i++)
+ {
+ var l = appLibs[i];
+ w.Write((i + 1).ToString().PadLeft(5), l.ID, l.BaseDir, l.Url.OriginalString);
+ }
+ }
+ }
+ else
+ {
+ foreach (var l in appLibs)
+ {
+ Console.WriteLine("{0}={1}", l.ID, l.Url);
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/BenchManager/BenchCLI/Commands/ListAppsCommand.cs b/BenchManager/BenchCLI/Commands/ListAppsCommand.cs
new file mode 100644
index 00000000..5525b99f
--- /dev/null
+++ b/BenchManager/BenchCLI/Commands/ListAppsCommand.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+using Mastersign.CliTools;
+using Mastersign.Docs;
+
+namespace Mastersign.Bench.Cli.Commands
+{
+ class ListAppsCommand : BenchCommand
+ {
+ public override string Name => "apps";
+
+ public enum AppSet
+ {
+ All,
+ Active,
+ NotActive,
+ Activated,
+ Deactivated,
+ Installed,
+ NotInstalled,
+ Cached,
+ NotCached,
+ DefaultApps,
+ MetaApps,
+ ManagedPackages,
+ }
+
+ private const string OPTION_SET = "set";
+ private const string OPTION_PROPERTIES = "properties";
+ private const string OPTION_FILTER = "filter";
+ private const string OPTION_SORT_BY = "sort-by";
+
+ private static readonly AppSet DEF_SET = AppSet.All;
+ private static readonly string DEF_PROPERTIES = string.Join(",",
+ new[] { "ID", PropertyKeys.AppLabel, PropertyKeys.AppVersion, PropertyKeys.AppIsActive });
+ private static readonly string DEF_FILTER = string.Empty;
+ private static readonly string DEF_SORT_BY = "ID";
+
+ public AppSet Set
+ => (AppSet)Enum.Parse(
+ typeof(AppSet),
+ Arguments.GetOptionValue(OPTION_SET, DEF_SET.ToString()),
+ true);
+
+ public string[] TableProperties
+ => Arguments.GetOptionValue(OPTION_PROPERTIES, DEF_PROPERTIES)
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+
+ public string[] Filter
+ => Arguments.GetOptionValue(OPTION_FILTER, DEF_FILTER)
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+
+ public string SortBy
+ => Arguments.GetOptionValue(OPTION_SORT_BY, DEF_SORT_BY);
+
+ protected override void InitializeArgumentParser(ArgumentParser parser)
+ {
+ parser.Description
+ .Begin(BlockType.Paragraph)
+ .Text("The ").Keyword(Name).Text(" command lists apps from the app library.")
+ .End(BlockType.Paragraph)
+ .Paragraph("You can specify the base set of apps and filter the apps to list.");
+
+ var optionSet = new EnumOptionArgument(OPTION_SET, 's', DEF_SET);
+ optionSet.Description
+ .Text("Specifies the set of apps to list.");
+
+ var optionProperties = new OptionArgument(OPTION_PROPERTIES, 'p', null);
+ optionProperties.Description
+ .Text("Specifies the properties to display in the listed output.")
+ .Text(" This option only has an effect, if the flag ")
+ .Keyword(Parent.Name).Text(" ").Keyword("--table").Text(" is set.");
+ optionProperties.PossibleValueInfo
+ .Text("A comma separated list of property names.");
+
+ var optionFilter = new OptionArgument(OPTION_FILTER, 'f', null);
+ optionFilter.Description
+ .Text("Specifies a filter to reduce the number of listed apps.");
+ optionFilter.PossibleValueInfo
+ .Text("A comma separated list of criteria.")
+ .Text(" E.g. ").Code("ID=JDK*,!IsInstalled,IsCached").Text(".");
+ optionFilter.DefaultValueInfo
+ .Text("no filter");
+
+ var optionSortBy = new OptionArgument(OPTION_SORT_BY, 'o', null);
+ optionSortBy.Description
+ .Text("Specifies a property to sort the apps by.");
+ optionSortBy.PossibleValueInfo
+ .Text("The name of an app property.");
+ optionSortBy.DefaultValueInfo
+ .Text("ID");
+
+ parser.RegisterArguments(
+ optionSet,
+ optionProperties,
+ optionFilter,
+ optionSortBy);
+ }
+
+ private DataOutputFormat Format => ((ListCommand)Parent).Format;
+
+ private bool OutputAsTable => ((ListCommand)Parent).OutputAsTable;
+
+ protected override bool ExecuteCommand(string[] args)
+ {
+ var cfg = LoadConfiguration();
+ var apps = new List>();
+ var set = Set;
+ var filter = Filter;
+ var sortBy = SortBy;
+ foreach (var app in cfg.Apps)
+ {
+ if (!IsIncludedInSet(app, set)) continue;
+ var props = GetProperties(app);
+ var match = true;
+ foreach (var f in filter)
+ {
+ if (!MatchesFilter(f, props))
+ {
+ match = false;
+ break;
+ }
+ }
+ if (match) apps.Add(props);
+ }
+ apps.Sort((o1, o2) =>
+ {
+ object v1 = null;
+ object v2 = null;
+ o1.TryGetValue(sortBy, out v1);
+ o2.TryGetValue(sortBy, out v2);
+ if (v1 == null && v2 == null) return 0;
+ if (v1 == null) return -1;
+ if (v2 == null) return 1;
+ if (v1 is bool) return ((bool)v1).CompareTo((bool)v2);
+ if (v1 is string) return ((string)v1).CompareTo((string)v2);
+ return 0;
+ });
+ if (OutputAsTable)
+ {
+ using (var w = TableWriterFactory.Create(Format))
+ {
+ w.Initialize(TableProperties);
+ foreach (var app in apps)
+ {
+ var values = new List