Hermod
A cross-platform, modular and fully GDPR-compliant email archival solution!
Loading...
Searching...
No Matches
Hermod.cs
Go to the documentation of this file.
1using System;
2
3namespace Hermod {
4
5 using Config;
6 using Core;
7 using Core.Commands.Results;
8 using PluginFramework;
9
10 using Serilog;
11 using System.Diagnostics;
12 using System.Text;
13
19 public partial class Hermod {
20
21 public bool InteractiveMode { get; internal set; }
22
23 private Stack<string> m_previousCommands = new Stack<string>();
24
30 public Hermod(ConfigManager configManager, ILogger logger) {
31 m_consoleLock = new object();
32 m_configManager = configManager;
33 m_appLogger = logger;
34 m_keepAlive = true;
35 InteractiveMode = configManager.GetConfig<bool>("Terminal.EnableInteractive");
36 m_inputCancellationToken = new CancellationTokenSource();
37 }
38
39 internal void StartUp() {
40 SetTerminalTitle();
41 m_appLogger.Information("Setting up OS event handlers...");
42 Console.CancelKeyPress += Console_CancelKeyPress;
43
44 m_appLogger.Debug("Setting up PluginRegistry...");
45 PluginRegistry.Instance.AppLogger = m_appLogger;
46 PluginRegistry.Instance.BuiltInCommands = Commands;
47
48 m_appLogger.Information("Loading plugins...");
49 m_appLogger.Information($"Plugin dir: { m_configManager.GetPluginInstallDir() }");
50 foreach (var plugin in m_configManager.GetPluginInstallDir().EnumerateFiles("*.dll")) {
51 m_appLogger.Information($"Attempting to load { plugin.FullName }...");
52 try {
54 } catch (Exception ex) {
55 m_appLogger.Error($"Failed to load assembly { plugin.FullName }!");
56 m_appLogger.Error($"Error: { ex.Message }");
57
58 m_appLogger.Debug(ex.StackTrace);
59 }
60 }
61 }
62
67 internal async Task<int> Execute() {
68
69 while (m_keepAlive) {
70
71 if (InteractiveMode) {
72 var promptInput = ShowPrompt();
73 if (string.IsNullOrEmpty(promptInput)) { continue; }
74
75 var splitString = promptInput.Split(' ', '\t');
76 if (TryGetCommand(splitString.First(), out var command)) {
77 if (command is null) { continue; }
78
79 var argArray = new string[splitString.Length - 1];
80 if (argArray.Length > 0) {
81 Array.Copy(splitString[1..], argArray, argArray.Length);
82 }
83
84 var result = await command.ExecuteAsync(argArray);
85
86 if (result is CommandErrorResult errResult && !string.IsNullOrEmpty(result?.Message)) {
87 ConsoleErrorWrite(result.Message);
88
89 m_appLogger.Error(errResult.Result as Exception, $"Command execution failed! Command: { command.Name } { string.Join(' ', argArray) }");
90 } else if (!string.IsNullOrEmpty(result?.Message)) {
91 ConsoleWrite(result.Message);
92 }
93 } else {
94 m_appLogger.Error($"Command \"{splitString[0] }\" not found!");
95 }
96 } else {
97 Thread.Sleep(50);
98 }
99
100 }
101
102 ShutDown();
103
104 return 0; // for the moment; this will also be the exit code for the application.
105 }
106
111 private string? ShowPrompt() {
112 const string PROMPT_STR = "hermod > ";
113
114 void WritePrompt(bool newLine = true) {
115 if (newLine) { Console.WriteLine(); }
116 Console.Write(PROMPT_STR);
117 }
118
119 WritePrompt();
120 StringBuilder lineCache = new StringBuilder();
121
122 ConsoleKeyInfo keyCode;
123 var historyStartIndex = m_previousCommands.Count;
124
125 while ((keyCode = Console.ReadKey()).Key != ConsoleKey.Enter) {
126 switch (keyCode.Key) {
127 case ConsoleKey.Tab: {
128 var autocompletedString = GetAutocompletion(lineCache.ToString());
129 if (autocompletedString is null) {
130 Console.Beep();
131 continue;
132 }
133
134 Console.Write(autocompletedString);
135 lineCache.Append(autocompletedString);
136 break;
137 }
138 case ConsoleKey.Backspace:
139 if (lineCache.Length == 0) { Console.Beep(); continue; }
140 lineCache.Remove(lineCache.Length - 1, 1);
141
142 // This surely isn't the best way to handle this, but apparently the terminal doesn't response correctly to \b
143 Console.Write('\b');
144 Console.Write(' ');
145 Console.Write('\b');
146 break;
147 case ConsoleKey.UpArrow:
148 if (m_previousCommands.Count == 0 || historyStartIndex == m_previousCommands.Count) {
149 Console.Beep();
150 continue;
151 }
152
153 lineCache.Clear();
154 lineCache.Append(m_previousCommands.ElementAt(m_previousCommands.Count - historyStartIndex++));
155 Console.CursorLeft = 0;
156 WritePrompt();
157 Console.Write(lineCache.ToString());
158 break;
159 }
160
161 lineCache.Append(keyCode.KeyChar);
162 }
163
164 Console.WriteLine();
165 var cmdString = lineCache.ToString().Trim();
166 m_previousCommands.Push(cmdString);
167 return cmdString;
168 }
169
176 private string? GetAutocompletion(string input, int maxDistance = 2) {
177 var matches =
178 from command in PluginRegistry.Instance.GetAllCommands()
179 let distance = LevenshteinDistance(command.Name, input)
180 where distance <= maxDistance
181 select command.Name;
182
183 return matches.FirstOrDefault();
184 }
185
186 private int LevenshteinDistance(string haystack, string needle) {
187 // Special cases
188 if (haystack == needle) { return 0; }
189 if (haystack.Length == 0) { return needle.Length; }
190 if (needle.Length == 0) { return haystack.Length; }
191
192 // Initialize the distance matrix
193 int[,] distance = new int[haystack.Length + 1, needle.Length + 1];
194 for (int i = 0; i <= haystack.Length; i++) {
195 distance[i, 0] = i;
196 }
197 for (int j = 0; j <= needle.Length; j++) {
198 distance[0, j] = j;
199 }
200
201 // Calculate the distance
202 for (int i = 1; i <= haystack.Length; i++) {
203 for (int j = 1; j <= needle.Length; j++) {
204 int cost = (haystack[i - 1] == needle[j - 1]) ? 0 : 1;
205 distance[i, j] = Math.Min(Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1), distance[i - 1, j - 1] + cost);
206 }
207 }
208 // Return the distance
209 return distance[haystack.Length, needle.Length];
210 }
211
215 internal void ShutDown() {
216 m_appLogger.Warning("Shutting down plugins...");
217
218 m_appLogger.Warning("Preparing for graceful exit.");
219 m_keepAlive = false;
220 }
221
225 internal void SetTerminalTitle() {
226 var version = GetType().Assembly.GetName().Version;
227 var appTitle = new StringBuilder().Append("Hermod ");
228
229 if (InteractiveMode) { appTitle.Append("[interactive] "); }
230
231 Console.Title = $"{ appTitle.ToString() } - v{ version?.Major }.{ version?.MajorRevision }.{ version?.Minor }.{ version?.MinorRevision }";
232 }
233
239 private void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
240 Console.WriteLine(); // make sure the output isn't on the same line as the prompt or output
241 m_appLogger.Warning("Received signal SIGNIT (CTRL+C)!");
242 e.Cancel = true;
243
244 if (InteractiveMode) {
245 m_appLogger.Warning("Hermod is running in interactive mode! Please use \"quit\" command!");
246 ShowPrompt();
247 return;
248 }
249
250 m_inputCancellationToken.Cancel();
251 m_keepAlive = false;
252 }
253
254 private void ConsoleWrite(string message) {
255 lock (m_consoleLock) {
256 Console.WriteLine(message);
257 }
258 }
259
260 private void ConsoleErrorWrite(string message) {
261 lock (m_consoleLock) {
262 var prevBackground = Console.BackgroundColor;
263 var prevForegound = Console.ForegroundColor;
264 Console.ForegroundColor = ConsoleColor.Red;
265 Console.Error.WriteLine(message);
266 Console.BackgroundColor = prevBackground;
267 Console.ForegroundColor = prevForegound;
268 }
269 }
270 }
271}
272
Provides an application-wide, thread safe way of getting and setting configurations relevant to the m...
A ICommandResult which implicates the command encountered an error.
void ShutDown()
Shuts Hermod down.
Definition: Hermod.cs:215
void ConsoleErrorWrite(string message)
Definition: Hermod.cs:260
async Task< int > Execute()
Executes the main business logic of the application.
Definition: Hermod.cs:67
void SetTerminalTitle()
Sets the terminal's title.
Definition: Hermod.cs:225
void StartUp()
Definition: Hermod.cs:39
int LevenshteinDistance(string haystack, string needle)
Definition: Hermod.cs:186
void ConsoleWrite(string message)
Definition: Hermod.cs:254
string? GetAutocompletion(string input, int maxDistance=2)
Attempts to get an auto completed string for the user's input.
Definition: Hermod.cs:176
void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
Handles SIGINT (CTRL+C)
Definition: Hermod.cs:239
Hermod(ConfigManager configManager, ILogger logger)
Main constructor; initialises the object.
Definition: Hermod.cs:30
string? ShowPrompt()
Displays the input prompt.
Definition: Hermod.cs:111
Handles the loading, unloading, and general management of plugins.
void LoadPlugin(FileInfo pluginFile)
Loads one or plugins from an Assembly on disk.
static PluginRegistry Instance
Gets the current instance of this object.
List< ICommand > GetAllCommands()
Gets a list containing all ICommand instances known to the application at the current time.