Engine.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Reflection;
  5. using Quadarax.Foundation.Core.Console;
  6. using Quadarax.Foundation.Core.QConsole.Attributes;
  7. using Quadarax.Foundation.Core.QConsole.Command.Base;
  8. using Quadarax.Foundation.Core.QConsole.Configuration;
  9. using Quadarax.Foundation.Core.QConsole.Context;
  10. namespace Quadarax.Foundation.Core.QConsole
  11. {
  12. public class Engine : IDisposable
  13. {
  14. #region *** Private Fields ***
  15. private bool _isDispoising;
  16. private bool _isExitSignal;
  17. private IList<AbstractCommand> _commands = new List<AbstractCommand>();
  18. private StartupConfiguration _configuration;
  19. private IList<ISelectionEntry> _selections = new List<ISelectionEntry>();
  20. #endregion
  21. #region *** Properties ***
  22. public string[] RawArguments { get; }
  23. public StartupConfiguration Configuration => _configuration;
  24. public AbstractCommand[] Commands => _commands.ToArray();
  25. public ISelectionEntry[] Selections => _selections.ToArray();
  26. public IEngineContext Context { get; }
  27. #endregion
  28. #region *** Constructor ***
  29. public Engine(StartupConfiguration configuration, IEngineContext context, string[] args)
  30. {
  31. _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
  32. Context = context ?? throw new ArgumentNullException(nameof(context));
  33. if (args == null)
  34. args = new string[0];
  35. // correction of arguments (fix " values)
  36. for (var i = 0; i < args.Length; i++)
  37. {
  38. if (args[i].Contains(" ") && args[i].Contains(configuration.CharacterNamedArgumentValueSeparator) ||
  39. args[i].Contains(configuration.CharacterNamedArgumentSeparator) && args[i].Contains(configuration.CharacterNamedArgumentValueSeparator))
  40. {
  41. var parts = args[i].Split(new[] {configuration.CharacterNamedArgumentValueSeparator},
  42. StringSplitOptions.RemoveEmptyEntries);
  43. if (parts.Length == 2)
  44. args[i] = parts[0] + configuration.CharacterNamedArgumentValueSeparator + configuration.CharacterTextValueBraceSeparator + parts[1] +
  45. configuration.CharacterTextValueBraceSeparator;
  46. else
  47. args[i] = parts[0] + configuration.CharacterNamedArgumentValueSeparator + configuration.CharacterTextValueBraceSeparator + string.Join(":",parts.Skip(1).Take(parts.Length - 1)) +
  48. configuration.CharacterTextValueBraceSeparator;
  49. }
  50. }
  51. // Ensure if debug mode -> Enable debug mode and remove explicit argument
  52. if (args.Any(x => x.Trim().ToLower() == Constants.Console.DebugModeExplicitArg))
  53. {
  54. configuration.EnableDebugMode();
  55. var newArgs = new List<string>(args);
  56. newArgs.Remove(Constants.Console.DebugModeExplicitArg);
  57. args = newArgs.ToArray();
  58. }
  59. WriteDebugInfo($"Console '{_configuration.ConsoleName}' initialization started.");
  60. WriteDebugInfo($"Engine assembly version '{Assembly.GetExecutingAssembly().GetName().Version}'.");
  61. WriteDebugInfo($"IsInitFromXml is set to '{_configuration.IsInitFromXml}'.");
  62. ShowHeader();
  63. RawArguments = args;
  64. WriteDebugInfo($"Input {args.Length} arguments appended.");
  65. WriteDebugInfo($"Context set: {Context}");
  66. var commandClasses = GetTypesWithHelpAttribute(new CommandDefinitionAttribute());
  67. WriteDebugInfo($"Console found {commandClasses.Count()} command definitions.");
  68. var paramConstrTypes = new Type[] { typeof(Engine) };
  69. var paramConstrValues = new object[] { this };
  70. foreach (var commandClass in commandClasses)
  71. {
  72. var command = Construct<AbstractCommand>(commandClass, paramConstrTypes, paramConstrValues);
  73. _commands.Add(command);
  74. WriteDebugInfo($"Command definition '{command}' added - class '{command.GetType().FullName}', assembly '{command.GetType().Assembly.GetName().Name}'.");
  75. }
  76. WriteDebugInfo("Console initialized.");
  77. }
  78. public void Start()
  79. {
  80. WriteDebugInfo("Console starting.");
  81. var commandLineArguments = string.Join(" ", RawArguments);
  82. using (var writer = new ConsoleWriter(ConsoleColor.White))
  83. {
  84. while (!_isExitSignal)
  85. {
  86. var input = string.Empty;
  87. if (commandLineArguments.Length == 0 && Configuration.AllowInteractive)
  88. {
  89. // interactive command entry
  90. writer.Write(_configuration.CharacterLineIntroduce);
  91. input = System.Console.ReadLine();
  92. }
  93. else
  94. {
  95. // inline command entry
  96. input = commandLineArguments;
  97. _isExitSignal = true;
  98. }
  99. var args = input.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
  100. if (args.Length == 0)
  101. {
  102. if (_isExitSignal == true && !Configuration.AllowInteractive &&
  103. Configuration.HandlerNothingToDo != null)
  104. {
  105. Configuration.HandlerNothingToDo.Invoke(this);
  106. }
  107. continue;
  108. }
  109. var command = GetCommand(args[0]);
  110. if (command == null)
  111. {
  112. using (var writerError = new ConsoleWriter(ConsoleColor.Red))
  113. {
  114. writerError.WriteLine($"Unknown command '{args[0]}'.");
  115. }
  116. continue;
  117. }
  118. var result = command.Execute(input);
  119. if (result.IsSuccess)
  120. {
  121. using (var writerSucc = new ConsoleWriter(ConsoleColor.Green))
  122. {
  123. writerSucc.WriteLine("Done.");
  124. }
  125. }
  126. else
  127. {
  128. using (var writerError = new ConsoleWriter(ConsoleColor.Red))
  129. {
  130. writerError.WriteLine("Error:");
  131. var e = result.ThrownException;
  132. if (e is AggregateException aggr)
  133. {
  134. foreach (var ex in aggr.InnerExceptions)
  135. DumpException(writerError, ex);
  136. }
  137. else
  138. while (e != null)
  139. {
  140. writerError.WriteLine(e.Message);
  141. e = e.InnerException;
  142. }
  143. }
  144. }
  145. }
  146. if (!Configuration.AllowInteractive && Configuration.WaitOnKeyInNonInteractiveMode)
  147. {
  148. WriteDebugInfo("Waiting for the key by configuration settings...");
  149. using (var writerGreen = new ConsoleWriter(ConsoleColor.Magenta))
  150. {
  151. writerGreen.WriteLine("Press any key to close...");
  152. System.Console.ReadKey();
  153. }
  154. }
  155. }
  156. }
  157. public void Stop()
  158. {
  159. _isExitSignal = true;
  160. WriteDebugInfo("Console stopped.");
  161. }
  162. public AbstractCommand GetCommand(string commandName)
  163. {
  164. return _commands.FirstOrDefault(x => x.Name.ToUpper() == commandName.ToUpper());
  165. }
  166. public void AppendSelection(params ISelectionEntry[] selectionEntries)
  167. {
  168. if (selectionEntries == null)
  169. throw new ArgumentNullException(nameof(selectionEntries));
  170. foreach (var entry in selectionEntries)
  171. {
  172. _selections.Add(entry);
  173. }
  174. WriteDebugInfo($"{selectionEntries.Length} was added to global selections. Total count {_selections.Count} entries.");
  175. }
  176. public void ClearSelection()
  177. {
  178. _selections.Clear();
  179. WriteDebugInfo("Global selections was cleared.");
  180. }
  181. public ISelectionEntry GetSelection(int ordinal)
  182. {
  183. if (ordinal==0)
  184. throw new ArgumentOutOfRangeException(nameof(ordinal), "Ordinal value must be greater than 0.");
  185. if (_selections.Count < ordinal)
  186. throw new ArgumentOutOfRangeException(nameof(ordinal), $"Ordinal value must be less than {_selections.Count}.");
  187. return _selections[ordinal - 1];
  188. }
  189. #endregion
  190. #region *** Public Operations ***
  191. public void Dispose()
  192. {
  193. if (_isDispoising)
  194. return;
  195. _isDispoising = true;
  196. WriteDebugInfo("Dispoising called.");
  197. OnDispoising();
  198. WriteDebugInfo("Dispoising done.");
  199. }
  200. #endregion
  201. #region *** Protected Operations ***
  202. protected virtual void OnDispoising()
  203. {
  204. }
  205. protected virtual void OnShowHeader()
  206. {
  207. var headerText = string.IsNullOrEmpty(_configuration.ConsoleCopyright) ? @"{0} * {1}" : @"{0} * {1} * {2}";
  208. using (var writer = new ConsoleWriter(ConsoleColor.Green))
  209. {
  210. writer.WriteLine(!string.IsNullOrEmpty(_configuration.ConsoleCopyright)
  211. ? string.Format(headerText, _configuration.ConsoleName, _configuration.ConsoleVersion,
  212. _configuration.ConsoleCopyright)
  213. : string.Format(headerText, _configuration.ConsoleName, _configuration.ConsoleVersion));
  214. }
  215. }
  216. #endregion
  217. #region *** Private Operations ***
  218. private static T Construct<T>(Type instance, Type[] paramTypes, object[] paramValues)
  219. {
  220. var ci = instance.GetConstructor(
  221. BindingFlags.Instance | BindingFlags.Public,
  222. null, paramTypes, null);
  223. if (ci == null)
  224. throw new Exception($"Command class '{instance.FullName}' has not public constuctor defined or has bad constructor's format.");
  225. return (T)ci.Invoke(paramValues);
  226. }
  227. private IEnumerable<Type> GetTypesWithHelpAttribute(Attribute attribute)
  228. {
  229. WriteDebugInfo($"Loading command assemblies ...");
  230. var defaultTypes = Assembly.GetExecutingAssembly().GetTypes().Where(type => type.GetCustomAttributes(attribute.GetType(), true).Length > 0).Distinct().ToList();
  231. var customTypes = new List<Type>();
  232. if (_configuration.CommandDefinitionAssebmly != null)
  233. {
  234. foreach (var assembly in _configuration.CommandDefinitionAssebmly)
  235. {
  236. customTypes.AddRange(assembly.GetTypes().Where(type => type.GetCustomAttributes(attribute.GetType(), true).Length > 0).Distinct().ToArray());
  237. WriteDebugInfo(assembly.GetName().Name + " was scanned for commands.");
  238. }
  239. }
  240. else
  241. {
  242. WriteDebugInfo("Custom CommandDefinitionAssembly is not defined. Skip.");
  243. }
  244. WriteDebugInfo($"Command definition obtaining process found {defaultTypes.Count} default definitions and {customTypes.Count} custom definitions.");
  245. defaultTypes.AddRange(customTypes);
  246. defaultTypes = defaultTypes.Distinct().ToList();
  247. foreach (var disabledCommand in _configuration.DisabledCommands)
  248. {
  249. var disabled = defaultTypes.FirstOrDefault(x => x.Name == disabledCommand);
  250. if (disabled==null)
  251. continue;
  252. defaultTypes.Remove(disabled);
  253. WriteDebugInfo($"Commnad class '{disabled.Name}', fullname '{disabled.FullName}', assembly '{disabled.Assembly.GetName().Name}' was skipped by configuration.");
  254. }
  255. return defaultTypes.ToArray();
  256. }
  257. private void WriteDebugInfo(string text)
  258. {
  259. using (var debug = new DebugConsoleWriter())
  260. {
  261. debug.WriteDebugLine(text);
  262. }
  263. }
  264. private void DumpException(ConsoleWriter writer, Exception e)
  265. {
  266. while (e != null)
  267. {
  268. writer.WriteLine("- " + e.Message);
  269. e = e.InnerException;
  270. }
  271. }
  272. private void ShowHeader()
  273. {
  274. OnShowHeader();
  275. }
  276. #endregion
  277. }
  278. }