Engine.cs 13 KB

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