1 //
2 // Copyright (c) 2010-2025 Antmicro
3 // Copyright (c) 2011-2015 Realtime Embedded
4 //
5 // This file is licensed under the MIT License.
6 // Full license text is available in 'licenses/MIT.txt'.
7 //
8 using System;
9 using Antmicro.Renode.Core;
10 using Antmicro.Renode.Exceptions;
11 using Antmicro.Renode.Logging;
12 using Antmicro.Renode.Sockets;
13 using Antmicro.Renode.Utilities;
14 using System.IO;
15 using System.Text;
16 using System.Collections.Generic;
17 using System.Linq;
18 using System.Text.RegularExpressions;
19 using System.Reflection;
20 using System.Security.Cryptography;
21 using AntShell;
22 using AntShell.Commands;
23 using Antmicro.Renode.UserInterface.Tokenizer;
24 using Antmicro.Renode.UserInterface.Commands;
25 
26 namespace Antmicro.Renode.UserInterface
27 {
28     public partial class Monitor : ICommandHandler
29     {
HandleCommand(string cmd, ICommandInteraction ci)30         public ICommandInteraction HandleCommand(string cmd, ICommandInteraction ci)
31         {
32             Parse(cmd, ci);
33             return ci;
34         }
35 
SuggestionNeeded(string cmd)36         public string[] SuggestionNeeded(string cmd)
37         {
38             return SuggestCommands(cmd).ToArray();
39         }
40 
41         public Func<IEnumerable<ICommandDescription>> GetInternalCommands { get; set; }
42 
43         public IMachine Machine
44         {
45             get
46             {
47                 return currentMachine;
48             }
49             set
50             {
51                 currentMachine = value;
52             }
53         }
54 
55         private Emulation Emulation
56         {
57             get
58             {
59                 return emulationManager.CurrentEmulation;
60             }
61         }
62 
63         public IEnumerable<string> CurrentPathPrefixes
64         {
65             get
66             {
67                 return monitorPath.PathElements;
68             }
69         }
70 
71         private readonly EmulationManager emulationManager;
72         private MonitorPath monitorPath = new MonitorPath(Environment.CurrentDirectory);
73         public const string StartupCommandEnv = "STARTUP_COMMAND";
74         private bool swallowExceptions;
75         private bool breakOnException;
76 
Monitor()77         public Monitor()
78         {
79             swallowExceptions = ConfigurationManager.Instance.Get(ConfigurationSection, "consume-exceptions-from-command", true);
80             breakOnException = ConfigurationManager.Instance.Get(ConfigurationSection, "break-script-on-exception", true);
81 
82             CurrentBindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static;
83             Commands = new HashSet<Command>(new CommandComparer());
84             TypeManager.Instance.AutoLoadedType += InitializeAutoCommand;
85 
86             this.emulationManager = EmulationManager.Instance;
87 
88             pythonRunner = new MonitorPythonEngine(this);
89             Quitted += pythonRunner.Dispose;
90             var startingCurrentDirectory = Environment.CurrentDirectory;
91             variableCollections = new Dictionary<VariableType, Dictionary<string, Token>>
92             {
93                 { VariableType.Variable, variables },
94                 { VariableType.Macro, macros },
95                 { VariableType.Alias, aliases },
96             };
97             SetBasePath();
98             InitCommands();
99             emulationManager.CurrentEmulation.MachineAdded += RegisterResetCommand;
100             emulationManager.CurrentEmulation.MachineRemoved += UpdateMonitorPrompt;
101             emulationManager.EmulationChanged += () =>
102             {
103                 Token oldOrigin;
104                 variables.TryGetValue(OriginVariable, out oldOrigin);
105 
106                 variables.Clear();
107                 SetVariable(CurrentDirectoryVariable, new PathToken("@" + startingCurrentDirectory), variables);
108                 if(oldOrigin != null)
109                 {
110                     SetVariable(OriginVariable, oldOrigin, variables);
111                 }
112                 macros.Clear();
113                 aliases.Clear();
114                 Machine = null;
115                 emulationManager.CurrentEmulation.MachineAdded += RegisterResetCommand;
116                 monitorPath.Reset();
117             };
118 
119             SetVariable(CurrentDirectoryVariable, new PathToken("@" + startingCurrentDirectory), variables);
120             CurrentNumberFormat = ConfigurationManager.Instance.Get<NumberModes>(ConfigurationSection, "number-format", NumberModes.Hexadecimal);
121 
122             JoinEmulation();
123         }
124 
RegisterResetCommand(IMachine machine)125         private void RegisterResetCommand(IMachine machine)
126         {
127             machine.MachineReset += ResetMachine;
128         }
129 
UpdateMonitorPrompt(IMachine machine)130         private void UpdateMonitorPrompt(IMachine machine)
131         {
132             if(currentMachine == machine)
133             {
134                 currentMachine = null;
135             }
136         }
137 
InitializeAutoCommand(Type type)138         private void InitializeAutoCommand(Type type)
139         {
140             if(type.IsSubclassOf(typeof(AutoLoadCommand)))
141             {
142                 var constructor = type.GetConstructor(new[] { typeof(Monitor) })
143                                   ?? type.GetConstructors().FirstOrDefault(x =>
144                 {
145                     var constructorParams = x.GetParameters();
146                     if(constructorParams.Length == 0)
147                     {
148                         return false;
149                     }
150                     return constructorParams[0].ParameterType == typeof(Monitor) && constructorParams.Skip(1).All(y => y.IsOptional);
151                 });
152                 if(constructor == null)
153                 {
154                     Logger.LogAs(this, LogLevel.Error, "Could not initialize command {0}.", type.Name);
155                     return;
156                 }
157                 var parameters = new List<object> { this };
158                 parameters.AddRange(constructor.GetParameters().Skip(1).Select(x => x.DefaultValue));
159                 var commandInstance = (AutoLoadCommand)constructor.Invoke(parameters.ToArray());
160                 RegisterCommand(commandInstance);
161             }
162         }
163 
JoinEmulation()164         private void JoinEmulation()
165         {
166             Emulation.MachineExchanged += (oldMachine, newMachine) =>
167             {
168                 if(currentMachine == oldMachine)
169                 {
170                     currentMachine = newMachine;
171                 }
172             };
173         }
174 
SetBasePath()175         private static void SetBasePath()
176         {
177             if(!Misc.TryGetRootDirectory(out var baseDirectory))
178             {
179                 Logger.Log(LogLevel.Warning, "Monitor: could not find root base path, using current instead.");
180                 return;
181             }
182             Directory.SetCurrentDirectory(baseDirectory);
183         }
184 
RegisterCommand(Command command)185         public void RegisterCommand(Command command)
186         {
187             if(Commands.Contains(command))
188             {
189                 Logger.LogAs(this, LogLevel.Warning, "Command {0} already registered.", command.Name);
190                 return;
191             }
192             Commands.Add(command);
193         }
194 
UnregisterCommand(Command command)195         public void UnregisterCommand(Command command)
196         {
197             if(!Commands.Contains(command))
198             {
199                 Logger.LogAs(this, LogLevel.Warning, "Command {0} not registered.", command.Name);
200                 return;
201             }
202             Commands.Remove(command);
203         }
204 
InitCommands()205         private void InitCommands()
206         {
207             Bind(Core.Machine.MachineKeyword, () => Machine);
208             BindStatic("connector", () => emulationManager.CurrentEmulation.Connector);
209             BindStatic(EmulationToken, () => Emulation);
210             BindStatic("plugins", () => TypeManager.Instance.PluginManager);
211             BindStatic("EmulationManager", () => emulationManager);
212             BindStatic("sockets", () => SocketsManager.Instance);
213 
214             var includeCommand = new IncludeFileCommand(this, (x, y) => pythonRunner.TryExecutePythonScript(x, y), x => TryExecuteScript(x), (x, y) => TryCompilePlugin(x, y), (x,y) => TryLoadPlatform(x,y));
215             Commands.Add(new HelpCommand(this, () =>
216             {
217                 var gic = GetInternalCommands;
218                 var result = Commands.Cast<ICommandDescription>();
219                 if(gic != null)
220                 {
221                     result = result.Concat(gic());
222                 }
223                 return result;
224             }));
225             Commands.Add(includeCommand);
226             Commands.Add(new CreatePlatformCommand(this, x => currentMachine = x));
227             Commands.Add(new UsingCommand(this, () => usings));
228             Commands.Add(new QuitCommand(this, x => currentMachine = x, () => Quitted));
229             Commands.Add(new PeripheralsCommand(this, () => currentMachine));
230             Commands.Add(new MonitorPathCommand(this, monitorPath));
231             Commands.Add(new StartCommand(this, includeCommand));
232             Commands.Add(new SetCommand(this, "set", "VARIABLE", (x, y) => SetVariable(x, y, variables), (x, y) => EnableStringEater(x, y, VariableType.Variable),
233                 DisableStringEater, () => stringEaterMode, GetVariableName));
234             Commands.Add(new SetCommand(this, "macro", "MACRO", (x, y) => SetVariable(x, y, macros), (x, y) => EnableStringEater(x, y, VariableType.Macro),
235                 DisableStringEater, () => stringEaterMode, GetVariableName));
236             Commands.Add(new SetCommand(this, "alias", "ALIAS", (x, y) => SetVariable(x, y, aliases), (x, y) => EnableStringEater(x, y, VariableType.Alias),
237 	        DisableStringEater, () => stringEaterMode, GetVariableName));
238             Commands.Add(new PythonExecuteCommand(this, x => ExpandVariable(x, variables), (x, y) => pythonRunner.ExecutePythonCommand(x, y)));
239             Commands.Add(new ExecuteCommand(this, "execute", "VARIABLE", x => ExpandVariable(x, variables), () => variables.Keys));
240             Commands.Add(new ExecuteCommand(this, "runMacro", "MACRO", x => ExpandVariable(x, macros), () => macros.Keys));
241             Commands.Add(new MachCommand(this, () => currentMachine, x => currentMachine = x));
242             Commands.Add(new ResdCommand(this));
243             Commands.Add(new VerboseCommand(this, x => verboseMode = x));
244         }
245 
DisableStringEater()246         private void DisableStringEater()
247         {
248             stringEaterMode = 0;
249             stringEaterValue = null;
250             stringEaterVariableName = null;
251             recordingType = null;
252         }
253 
EnableStringEater(string variable, int mode, VariableType type)254         private void EnableStringEater(string variable, int mode, VariableType type)
255         {
256             recordingType = type;
257             stringEaterMode = mode;
258             stringEaterVariableName = variable;
259         }
260 
ResetMachine(IMachine machine)261         private void ResetMachine(IMachine machine)
262         {
263             string machineName;
264             if(EmulationManager.Instance.CurrentEmulation.TryGetMachineName(machine, out machineName))
265             {
266                 var activeMachine = _currentMachine;
267                 _currentMachine = machine;
268                 var macroName = GetVariableName("reset");
269                 Token resetMacro;
270                 if(macros.TryGetValue(macroName, out resetMacro))
271                 {
272                     var macroLines = resetMacro.GetObjectValue().ToString().Split('\n');
273                     foreach(var line in macroLines)
274                     {
275                         Parse(line, Interaction);
276                     }
277                 }
278                 else
279                 {
280                     Logger.LogAs(this, LogLevel.Warning, "No action for reset - macro {0} is not registered.", macroName);
281                 }
282                 _currentMachine = activeMachine;
283             }
284         }
285 
Tokenize(string cmd, ICommandInteraction writer)286         private TokenizationResult Tokenize(string cmd, ICommandInteraction writer)
287         {
288             var result = tokenizer.Tokenize(cmd);
289             if(result.UnmatchedCharactersLeft != 0)
290             {
291                 //Reevaluate the expression if the tokenization failed, but expanding the variables may help.
292                 //E.g. i $ORIGIN/dir/script. This happens only if the variable is the last successful token.
293                 if(result.Tokens.Any() && result.Tokens.Last() is VariableToken lastVariableToken)
294                 {
295                     if(!TryExpandVariable(lastVariableToken, variables, out var lastExpandedToken))
296                     {
297                         writer.WriteError($"No such variable: ${lastVariableToken.Value}");
298                         return null;
299                     }
300                     // replace the last token with the expanded version
301                     var newString = result.Tokens.Take(result.Tokens.Count() - 1).Select(x => x.OriginalValue).Stringify() + lastExpandedToken.OriginalValue + cmd.Substring(cmd.Length - result.UnmatchedCharactersLeft);
302                     return Tokenize(newString, writer);
303                 }
304                 var messages = new StringBuilder();
305 
306                 var message = "Could not tokenize here:";
307                 writer.WriteError(message);
308                 messages.AppendFormat("Monitor: {0}\n", message);
309 
310                 writer.WriteError(cmd);
311                 messages.AppendLine(cmd);
312 
313                 var matchedLength = cmd.Length - result.UnmatchedCharactersLeft;
314                 var padded = "^".PadLeft(matchedLength + 1);
315                 writer.WriteError(padded);
316                 messages.AppendLine(padded);
317                 if(result.Exception != null)
318                 {
319                     messages.AppendFormat("Encountered exception: {0}\n", result.Exception.Message);
320                     writer.WriteError(result.Exception.Message);
321                 }
322                 Logger.Log(LogLevel.Warning, messages.ToString());
323                 return null;
324             }
325             return result;
326         }
327 
SetVariable(string var, Token val, Dictionary<string, Token> collection)328         public void SetVariable(string var, Token val, Dictionary<string, Token> collection)
329         {
330             collection[var] = val;
331         }
332 
ExecuteWithResult(String value, ICommandInteraction writer)333         private Token ExecuteWithResult(String value, ICommandInteraction writer)
334         {
335             var eater = new CommandInteractionEater();
336             if(Parse(value, eater))
337             {
338                 return new StringToken(eater.GetContents());
339             }
340             else
341             {
342                 writer.WriteError(eater.GetError());
343                 return null;
344             }
345         }
346 
ParseTokens(IEnumerable<Token> tokensToParse, ICommandInteraction writer)347         public bool ParseTokens(IEnumerable<Token> tokensToParse, ICommandInteraction writer)
348         {
349             var reParse = false;
350             var result = new List<Token>();
351             var tokens = tokensToParse.ToList();
352             foreach(var token in tokens)
353             {
354                 Token resultToken = token;
355                 if(token is CommentToken)
356                 {
357                     continue;
358                 }
359                 if(token is ExecutionToken)
360                 {
361                     resultToken = ExecuteWithResult((string)token.GetObjectValue(), writer);
362                     if(resultToken == null)
363                     {
364                         return false; //something went wrong with the inner command
365                     }
366                     reParse = true;
367                 }
368 
369                 var pathToken = token as PathToken;
370                 if(pathToken != null)
371                 {
372                     string fileName;
373                     if(TryGetFilenameFromAvailablePaths(pathToken.Value, out fileName))
374                     {
375                         resultToken = new PathToken("@" + fileName);
376                     }
377                     else
378                     {
379                         Uri uri;
380                         string filename;
381                         string fname = pathToken.Value;
382                         try
383                         {
384                             uri = new Uri(fname);
385                             if(uri.IsFile)
386                             {
387                                 throw new UriFormatException();
388                             }
389                             var success = Emulation.FileFetcher.TryFetchFromUri(uri, out filename);
390                             if(!success)
391                             {
392                                 writer.WriteError("Failed to download {0}, see log for details.".FormatWith(fname));
393                                 filename = null;
394                                 if(breakOnException)
395                                 {
396                                     return false;
397                                 }
398                             }
399                             resultToken = new PathToken("@" + filename);
400                         }
401                         catch(UriFormatException)
402                         {
403                             //Not a proper uri, so probably a nonexisting local path
404                         }
405                     }
406                 }
407 
408                 result.Add(resultToken);
409             }
410             if(!result.Any())
411             {
412                 return true;
413             }
414             if(reParse)
415             {
416                 return Parse(String.Join(" ", result.Select(x => x.OriginalValue)), writer);
417             }
418             try
419             {
420                 if(!ExecuteCommand(result.ToArray(), writer) && breakOnException)
421                 {
422                     return false;
423                 }
424             }
425             catch(Exception e)
426             {
427                 var ex = e as AggregateException;
428                 if(ex != null)
429                 {
430                     if(ex.InnerExceptions.Any(x => !(x is RecoverableException)))
431                     {
432                         throw;
433                     }
434                 }
435                 else if(!(e is RecoverableException))
436                 {
437                     throw;
438                 }
439                 if(swallowExceptions)
440                 {
441                     if(ex != null)
442                     {
443                         foreach(var inner in ex.InnerExceptions)
444                         {
445                             PrintException(String.Join(" ", result.Select(x => x.OriginalValue)), inner, writer);
446                         }
447                     }
448                     else
449                     {
450                         PrintException(String.Join(" ", result.Select(x => x.OriginalValue)), e, writer);
451                     }
452                     if(breakOnException)
453                     {
454                         return false;
455                     }
456                 }
457                 else
458                 {
459                     throw;
460                 }
461             }
462             return true;
463         }
464 
Parse(string cmd, ICommandInteraction writer = null)465         public bool Parse(string cmd, ICommandInteraction writer = null)
466         {
467             if(writer == null)
468             {
469                 writer = Interaction;
470             }
471 
472             if(stringEaterMode > 0)
473             {
474                 //For multiline scripts in variables
475                 if(cmd.Contains(MultiLineTerminator))
476                 {
477                     stringEaterMode += 1;
478                     if(stringEaterMode > 2)
479                     {
480                         SetVariable(stringEaterVariableName, new StringToken(stringEaterValue), variableCollections[recordingType.Value]);
481                         stringEaterValue = "";
482                         stringEaterMode = 0;
483                     }
484                     return true;
485                 }
486                 if(stringEaterMode > 1)
487                 {
488                     if(stringEaterValue != "")
489                     {
490                         stringEaterValue = stringEaterValue + "\n";
491                     }
492                     stringEaterValue = stringEaterValue + cmd;
493                     return true;
494                 }
495                 SetVariable(stringEaterVariableName, null, variableCollections[recordingType.Value]);
496                 stringEaterValue = "";
497                 stringEaterMode = 0;
498             }
499 
500             if(string.IsNullOrWhiteSpace(cmd))
501             {
502                 return true;
503             }
504             var tokens = Tokenize(cmd, writer);
505             if(tokens == null)
506             {
507                 return false;
508             }
509             int groupNumber = 0;
510             foreach(var singleCommand in tokens.Tokens
511                     .GroupBy(x => { if(x is CommandSplit) groupNumber++; return groupNumber; })
512                     .Select(x => x.Where(y => !(y is CommandSplit)))
513                     .Where(x => x.Any()))
514             {
515                 if(!ParseTokens(singleCommand, writer))
516                     return false;
517             }
518             return true;
519         }
520 
GetVariableName(string variableName)521         private string GetVariableName(string variableName)
522         {
523             var elements = variableName.Split(new[] { '.' }, 2);
524 
525             if(elements.Length == 1 || (!elements[0].Equals("global") && !EmulationManager.Instance.CurrentEmulation.Names.Select(x => x.Replace("-", "_")).Any(x => x == elements[0])))
526             {
527                 if(currentMachine != null)
528                 {
529                     variableName = String.Format("{0}.{1}", EmulationManager.Instance.CurrentEmulation[currentMachine].Replace("-", "_"), variableName);
530                 }
531                 else
532                 {
533                     variableName = String.Format("global.{0}", variableName);
534                 }
535             }
536             return variableName;
537         }
538 
TryCompilePlugin(string filename, ICommandInteraction writer = null)539         public bool TryCompilePlugin(string filename, ICommandInteraction writer = null)
540         {
541             if(writer == null)
542             {
543                 writer = Interaction;
544             }
545             string sha;
546             using(var shaComputer = SHA256.Create())
547             {
548                 using(var f = File.OpenRead(filename))
549                 {
550                     var bytesSha = shaComputer.ComputeHash(f);
551 
552                     var strBldr = new StringBuilder(32 * 2);
553                     foreach(var b in bytesSha)
554                     {
555                         strBldr.AppendFormat("{0:X2}", b);
556                     }
557                     sha = strBldr.ToString();
558 
559                     if(scannedFilesCache.Contains(sha))
560                     {
561                         writer.WriteLine($"Code from file {filename} has already been compiled. Ignoring...");
562                         return true;
563                     }
564                 }
565             }
566 
567             try
568             {
569                 if(!EmulationManager.Instance.CompiledFilesCache.TryGetEntryWithSha(sha, out var compiledCode))
570                 {
571                     var compiler = new AdHocCompiler();
572                     compiledCode = compiler.Compile(filename);
573                     // Load dynamically compiled assembly to memory. It presents an advantage that next
574                     // ad-hoc compiled assembly can reference types from this one without any extra steps.
575                     // Therefore "EnsureTypeIsLoaded" call is no necessary as dependencies are already loaded.
576                     // Assembly.LoadFrom is used for a compatibility with Mono/.NET Framework,
577                     // but once we move fully to .NET, consider AssemblyLoadContext.LoadFromAssemblyPath.
578                     Assembly.LoadFrom(compiledCode);
579                     EmulationManager.Instance.CompiledFilesCache.StoreEntryWithSha(sha, compiledCode);
580                 }
581 
582                 cache.ClearCache();
583                 var result = TypeManager.Instance.ScanFile(compiledCode);
584                 if(result)
585                 {
586                     scannedFilesCache.Add(sha);
587                 }
588                 return result;
589             }
590             catch(Exception e)
591                 when(e is RecoverableException
592                   || e is InvalidOperationException)
593             {
594                 writer.WriteError("Errors during compilation or loading:\r\n" + e.Message.Replace(Environment.NewLine, "\r\n"));
595                 return false;
596             }
597         }
598 
TryLoadPlatform(string filename, ICommandInteraction writer = null)599         public bool TryLoadPlatform(string filename, ICommandInteraction writer = null)
600         {
601             if(writer == null)
602             {
603                 writer = Interaction;
604             }
605             if(currentMachine == null)
606             {
607                 var machine = new Machine();
608                 EmulationManager.Instance.CurrentEmulation.AddMachine(machine);
609                 currentMachine = machine;
610             }
611             var path = new PathToken(filename);
612             var command = new LiteralToken("LoadPlatformDescription");
613             ExecuteDeviceAction("machine", Machine, new Token[]{ command, path });
614             return true;
615         }
616 
617         private List<string> scannedFilesCache = new List<string>();
618 
TryExecuteScript(string filename, ICommandInteraction writer = null)619         public bool TryExecuteScript(string filename, ICommandInteraction writer = null)
620         {
621             if(writer == null)
622             {
623                 writer = Interaction;
624             }
625 
626             Token oldOrigin;
627             var originalFilename = filename;
628             if(!TryGetFilenameFromAvailablePaths(filename, out filename))
629             {
630                 writer.WriteError($"Could not find file '{originalFilename}'");
631                 return false;
632             }
633             variables.TryGetValue(OriginVariable, out oldOrigin);
634             SetVariable(OriginVariable, new PathToken("@" + Path.GetDirectoryName(filename).Replace(" ", @"\ ")), variables);
635             var lines = File.ReadAllLines(filename);
636             Array.ForEach(lines, x => x.Replace("\r", "\n"));
637             var processedLines = new List<string>(lines.Length);
638             var builder = new StringBuilder();
639             var currentlyEating = false;
640 
641             foreach(var line in lines)
642             {
643                 var hasTerminator = line.Contains(MultiLineTerminator);
644                 if(!currentlyEating && !hasTerminator)
645                 {
646                     processedLines.Add(line);
647                 }
648                 if(hasTerminator)
649                 {
650                     //concatenate with the previous line
651                     if(!currentlyEating && line.StartsWith(MultiLineTerminator, StringComparison.Ordinal))
652                     {
653                         builder.AppendLine(processedLines.Last());
654                         processedLines.RemoveAt(processedLines.Count - 1);
655                     }
656                     builder.AppendLine(line);
657                     if(currentlyEating)
658                     {
659                         processedLines.Add(builder.ToString());
660                         builder.Clear();
661                     }
662                     currentlyEating = !currentlyEating;
663                 }
664                 else if(currentlyEating)
665                 {
666                     builder.AppendLine(line);
667                 }
668             }
669 
670             var success = true;
671             foreach(var ln in processedLines)
672             {
673                 if(!Parse(ln))
674                 {
675                     success = false;
676                     break;
677                 }
678             }
679             if(oldOrigin != null)
680             {
681                 SetVariable(OriginVariable, oldOrigin, variables);
682             }
683             return success;
684         }
685 
ExecutePythonCommand(string command)686         public object ExecutePythonCommand(string command)
687         {
688             return pythonRunner.ExecutePythonCommand(command, Interaction);
689         }
690 
GetVariable(string name)691         public object GetVariable(string name)
692         {
693             return variables.GetOrDefault(GetVariableName(name))?.GetObjectValue();
694         }
695 
TryGetFilenameFromAvailablePaths(string fileName, out string fullPath)696         private bool TryGetFilenameFromAvailablePaths(string fileName, out string fullPath)
697         {
698             fullPath = String.Empty;
699             //Try to find the given file, then the file with path prefix
700             foreach(var pathElement in monitorPath.PathElements.Prepend(String.Empty))
701             {
702                 var currentPath = Path.Combine(pathElement, fileName);
703                 if(File.Exists(currentPath) || Directory.Exists(currentPath))
704                 {
705                     fullPath = Path.GetFullPath(currentPath);
706                     return true;
707                 }
708             }
709             return false;
710         }
711 
PrintExceptionDetails(Exception e, ICommandInteraction writer, int tab = 0)712         private void PrintExceptionDetails(Exception e, ICommandInteraction writer, int tab = 0)
713         {
714             if(!(e is TargetInvocationException) && !String.IsNullOrWhiteSpace(e.Message))
715             {
716                 writer.WriteError(e.Message.Replace("\n", "\r\n").Indent(tab, '\t'));
717             }
718             else
719             {
720                 tab--; //if no message is printed out, we do not need an indentation.
721             }
722             var aggregateException = e as AggregateException;
723             if(aggregateException != null)
724             {
725                 foreach(var exception in aggregateException.InnerExceptions)
726                 {
727                     PrintExceptionDetails(exception, writer, tab + 1);
728                 }
729             }
730             if(e.InnerException != null)
731             {
732                 PrintExceptionDetails(e.InnerException, writer, tab + 1);
733             }
734         }
735 
PrintException(string commandName, Exception e, ICommandInteraction writer)736         private void PrintException(string commandName, Exception e, ICommandInteraction writer)
737         {
738             writer.WriteError(string.Format("There was an error executing command '{0}'", commandName));
739             PrintExceptionDetails(e, writer);
740         }
741 
ExpandVariables(IEnumerable<Token> tokens)742         private IList<Token> ExpandVariables(IEnumerable<Token> tokens)
743         {
744             return tokens.Select(x => x is VariableToken ? ExpandVariable(x as VariableToken, variables) ?? x : x).ToList(); // ?? to prevent null tokens
745         }
746 
ExpandVariable(VariableToken token, Dictionary<string, Token> collection)747         private Token ExpandVariable(VariableToken token, Dictionary<string, Token> collection)
748         {
749             Token result;
750             if(!TryExpandVariable(token, collection, out result))
751             {
752                 throw new RecoverableException(string.Format("No such variable: ${0}", token.Value));
753             }
754             return result;
755         }
756 
TryExpandVariable(VariableToken token, Dictionary<string, Token> collection, out Token expandedVariable)757         private bool TryExpandVariable(VariableToken token, Dictionary<string, Token> collection, out Token expandedVariable)
758         {
759             expandedVariable = null;
760             var varName = token.Value;
761             string newName;
762             if(collection.TryGetValue(varName, out expandedVariable))
763             {
764                 return true;
765             }
766             if(currentMachine != null)
767             {
768                 newName = String.Format("{0}.{1}", Emulation[currentMachine].Replace("-", "_"), varName);
769                 if(collection.TryGetValue(newName, out expandedVariable))
770                 {
771                     return true;
772                 }
773             }
774             newName = String.Format("{0}{1}", globalVariablePrefix, varName);
775             if(collection.TryGetValue(newName, out expandedVariable))
776             {
777                 return true;
778             }
779             return false;
780         }
781 
ExecuteCommand(Token[] com, ICommandInteraction writer)782         private bool ExecuteCommand(Token[] com, ICommandInteraction writer)
783         {
784             if(verboseMode)
785             {
786                 writer.WriteLine("Executing: " + com.Select(x => x.OriginalValue).Aggregate((x, y) => x + " " + y));
787             }
788             if(!com.Any())
789             {
790                 return true;
791             }
792 
793             //variable definition
794             if(com.Length == 3 && com[0] is VariableToken && com[1] is EqualityToken)
795             {
796                 Token dummy;
797                 var variableToExpand = com[0] as VariableToken;
798                 if(com[1] is ConditionalEqualityToken && TryExpandVariable(variableToExpand, variables, out dummy))
799                 {
800                     //variable exists, so we ignore this command
801                     return true;
802                 }
803                 (Commands.OfType<SetCommand>().First()).Run(writer, variableToExpand, com[2]);
804                 return true;
805             }
806             var command = com[0] as LiteralToken;
807 
808             if(command == null)
809             {
810                 writer.WriteError(string.Format("No such command or device: {0}", com[0].OriginalValue));
811                 return false;
812             }
813 
814             var commandHandler = Commands.FirstOrDefault(x => x.Name == command.Value);
815             if(commandHandler != null)
816             {
817                 return RunCommand(writer, commandHandler, com.Skip(1).ToList());
818             }
819             else if(IsNameAvailable(command.Value))
820             {
821                 ProcessDeviceActionByName(command.Value, ExpandVariables(com.Skip(1)), writer);
822             }
823             else if(IsNameAvailableInEmulationManager(command.Value))
824             {
825                 ProcessDeviceAction(typeof(EmulationManager), typeof(EmulationManager).Name, com, writer);
826             }
827             else
828             {
829                 foreach(var item in Commands)
830                 {
831                     if(item.AlternativeNames != null && item.AlternativeNames.Contains(command.Value))
832                     {
833                         return RunCommand(writer, item, com.Skip(1).ToList());
834                     }
835                 }
836 
837                 if (TryExpandVariable(new VariableToken(string.Format("${0}", com[0].OriginalValue)), aliases, out var cmd)) {
838                         var aliasedCommand = Tokenize(cmd.GetObjectValue().ToString(), writer).Tokens;
839                         return ParseTokens(aliasedCommand.Concat(com.Skip(1)), writer);
840                 }
841 
842                 if(!pythonRunner.ExecuteBuiltinCommand(ExpandVariables(com).ToArray(), writer))
843                 {
844                     writer.WriteError(string.Format("No such command or device: {0}", com[0].GetObjectValue()));
845                     return false;
846                 }
847             }
848             return true;
849         }
850 
FindLastCommandInString(string origin)851         private static string FindLastCommandInString(string origin)
852         {
853             bool inApostrophes = false;
854             int position = 0;
855             for(int i = 0; i < origin.Length; ++i)
856             {
857                 switch(origin[i])
858                 {
859                 case '"':
860                     inApostrophes = !inApostrophes;
861                     break;
862                 case ';':
863                     if(!inApostrophes)
864                     {
865                         position = i + 1;
866                     }
867                     break;
868                 }
869             }
870             return origin.Substring(position).TrimStart();
871         }
872 
SuggestFiles(String allButLast, String prefix, String directory, String lastElement)873         private static IEnumerable<String> SuggestFiles(String allButLast, String prefix, String directory, String lastElement)
874         {
875             //the sanitization of the first "./" is required to preserve the original input provided by the user
876             var directoryPath = Path.Combine(prefix, directory);
877             try
878             {
879                 if(!Directory.Exists(directoryPath))
880                 {
881                     return Enumerable.Empty<string>();
882                 }
883                 var files = Directory.GetFiles(directoryPath, lastElement + '*', SearchOption.TopDirectoryOnly)
884                                      .Select(x => allButLast + "@" + StripPrefix(x, prefix).Replace(" ", @"\ "));
885                 var dirs = Directory.GetDirectories(directoryPath, lastElement + '*', SearchOption.TopDirectoryOnly)
886                                     .Select(x => allButLast + "@" + (StripPrefix(x, prefix) + '/').Replace(" ", @"\ "));
887 
888                 var result = new List<string>();
889                 //We change "\" characters to "/", unless they were followed by the space character, in which case they treated as escape char.
890                 foreach(var file in files.Concat(dirs))
891                 {
892                     var sanitizedFile = SanitizePathSeparator(file);
893                     result.Add(sanitizedFile);
894                 }
895                 return result;
896             }
897             catch(UnauthorizedAccessException)
898             {
899                 return new[] { "{0}@{1}/".FormatWith(allButLast, Path.Combine(StripPrefix(directoryPath, prefix), lastElement)) };
900             }
901         }
902 
StripPrefix(string path, string prefix)903         private static string StripPrefix(string path, string prefix)
904         {
905             if(String.IsNullOrEmpty(prefix))
906             {
907                 return path;
908             }
909             return path.StartsWith(prefix, StringComparison.Ordinal) ? path.Substring(prefix.Length + (prefix.EndsWith(Path.DirectorySeparatorChar) ? 0 : 1)) : path;
910         }
911 
SuggestCommands(String prefix)912         private IEnumerable<String> SuggestCommands(String prefix)
913         {
914             var currentCommand = FindLastCommandInString(prefix);
915             var suggestions = new List<String>();
916             var prefixSplit = Regex.Matches(prefix, @"(((\\ )|\S))+").Cast<Match>().Select(x => x.Value).ToArray();
917             var prefixToAdd = prefix.EndsWith(currentCommand, StringComparison.Ordinal) ? prefix.Substring(0, prefix.Length - currentCommand.Length) : String.Empty;
918             var lastElement = String.Empty;
919 
920             if(prefixSplit.Length > 0)
921             {
922                 lastElement = prefixSplit.Last();
923             }
924             var allButLastOptional = AllButLastAndAggregate(prefixSplit, prefix.EndsWith(' '));
925             if(!string.IsNullOrEmpty(allButLastOptional))
926             {
927                 allButLastOptional += ' ';
928             }
929             var allButLast = AllButLastAndAggregate(prefixSplit);
930             if(!string.IsNullOrEmpty(allButLast))
931             {
932                 allButLast += ' ';
933             }
934             //paths
935             if(lastElement.StartsWith('@'))
936             {
937                 lastElement = Regex.Replace(lastElement.Substring(1), @"\\([^\\])", "$1");
938                 var directory = String.Empty;
939                 var file = String.Empty;
940                 if(!String.IsNullOrWhiteSpace(lastElement))
941                 {
942                     //these functions will fail on empty input
943                     directory = Path.GetDirectoryName(lastElement) ?? lastElement;
944                     file = Path.GetFileName(lastElement);
945                 }
946 #if PLATFORM_WINDOWS
947                 var rootIndicator = "^[a-zA-Z]:/";
948 #else
949                 var rootIndicator = "^/";
950 #endif
951                 if(Regex.Match(lastElement, rootIndicator).Success)
952                 {
953                     try
954                     {
955                         suggestions.AddRange(SuggestFiles(allButLast, String.Empty, directory, file)); //we need to filter out "/", because Path.GetDirectory returns null for "/"
956                     }
957                     catch(DirectoryNotFoundException) { }
958                 }
959                 else
960                 {
961                     foreach(var pathEntry in monitorPath.PathElements.Select(x => Path.GetFullPath(x)))
962                     {
963                         if(!Directory.Exists(pathEntry))
964                         {
965                             continue;
966                         }
967                         try
968                         {
969                             suggestions.AddRange(SuggestFiles(allButLast, pathEntry, directory, file));
970                         }
971                         catch(Exception)
972                         {
973                             Logger.LogAs(this, LogLevel.Debug, "Bug in mono on Directory.GetFiles!");
974                         }
975                     }
976                 }
977             }
978             //variables
979             else if(lastElement.StartsWith('$'))
980             {
981                 var varName = lastElement.Substring(1);
982                 var options = variables.Keys.Concat(macros.Keys).Where(x => x.StartsWith(varName, StringComparison.Ordinal)).ToList();
983                 var machinePrefix = currentMachine == null ? globalVariablePrefix : Emulation[currentMachine] + ".";
984                 options.AddRange(variables.Keys.Concat(macros.Keys).Where(x => x.StartsWith(String.Format("{0}{1}", machinePrefix, varName), StringComparison.Ordinal)).Select(x => x.Substring(machinePrefix.Length)));
985 
986                 if(options.Any())
987                 {
988                     suggestions.AddRange(options.Select(x => allButLast + '$' + x));
989                 }
990             }
991             var currentCommandSplit = currentCommand.Split(' ');
992 
993             if(currentCommand.Contains(' '))
994             {
995                 var cmd = Commands.SingleOrDefault(c => c.Name == currentCommandSplit[0] || c.AlternativeNames.Contains(currentCommandSplit[0])) as ISuggestionProvider;
996                 if(cmd != null)
997                 {
998                     var sugs = cmd.ProvideSuggestions(currentCommandSplit.Length > 1 ? currentCommandSplit[1] : string.Empty);
999                     suggestions.AddRange(sugs.Select(s => string.Format("{0}{1}", allButLastOptional, s)));
1000                 }
1001                 else if(currentCommandSplit.Length > 1 && GetAllAvailableNames().Contains(currentCommandSplit[0]))
1002                 {
1003                     var currentObject = GetDevice(currentCommandSplit[0]);
1004                     //Take whole command split without first and last element
1005                     var commandsChain = currentCommandSplit.Skip(1).Take(currentCommandSplit.Length - 2);
1006                     foreach(var command in commandsChain)
1007                     {
1008                         //It is assumed that commands chain can contain only properties or fields
1009                         var newObject = FindFieldOrProperty(currentObject, command);
1010                         if(newObject == null)
1011                         {
1012                             currentObject = null;
1013                             break;
1014                         }
1015                         currentObject = newObject;
1016                     }
1017 
1018                     if(currentObject != null)
1019                     {
1020                         var devInfo = GetObjectSuggestions(currentObject).Distinct();
1021                         suggestions.AddRange(devInfo.Where(x => x.StartsWith(currentCommandSplit[currentCommandSplit.Length - 1], StringComparison.OrdinalIgnoreCase))
1022                             .Select(x => allButLastOptional + x));
1023                     }
1024                 }
1025             }
1026             else
1027             {
1028                 var sugg = Commands.Select(x => x.Name).ToList();
1029 
1030                 sugg.AddRange(GetAllAvailableNames());
1031                 sugg.AddRange(pythonRunner.GetPythonCommands());
1032                 sugg.AddRange(aliases.Keys);
1033                 sugg.AddRange(aliases.Keys.Select(x => x.Substring(x.IndexOf('.') + 1))); // remove the "global." or "{machine-name}." prefix
1034                 suggestions.AddRange(sugg.Where(x => x.StartsWith(currentCommandSplit[0])).Select(x => prefixToAdd + x));
1035 
1036                 if(suggestions.Count == 0) //EmulationManager
1037                 {
1038                     var dev = GetDevice(typeof(EmulationManager).Name);
1039                     var devInfo = GetObjectSuggestions(dev).Distinct();
1040                     if(devInfo != null)
1041                     {
1042                         suggestions.AddRange(devInfo.Where(x => x.StartsWith(currentCommandSplit[0], StringComparison.OrdinalIgnoreCase)));
1043                     }
1044                 }
1045             }
1046             return suggestions.OrderBy(x => x).Distinct();
1047         }
1048 
GetAvailableNames()1049         private IEnumerable<string> GetAvailableNames()
1050         {
1051             if(currentMachine != null)
1052             {
1053                 return currentMachine.GetAllNames().Union(Emulation.ExternalsManager.GetNames().Union(staticObjectDelegateMappings.Keys.Union(objectDelegateMappings.Keys)));
1054             }
1055             return Emulation.ExternalsManager.GetNames().Union(staticObjectDelegateMappings.Keys);
1056         }
1057 
GetAllAvailableNames()1058         private IEnumerable<string> GetAllAvailableNames()
1059         {
1060             var baseNames = GetAvailableNames().ToList();
1061             var result = new List<string>(baseNames);
1062             foreach(var use in usings)
1063             {
1064                 var localUse = use;
1065                 result.AddRange(baseNames.Where(x => x.StartsWith(localUse, StringComparison.Ordinal) && x.Length > localUse.Length).Select(x => x.Substring(localUse.Length)));
1066             }
1067             return result;
1068         }
1069 
IsNameAvailable(string name)1070         private bool IsNameAvailable(string name)
1071         {
1072             var names = GetAvailableNames();
1073             var ret = names.Contains(name);
1074             if(!ret)
1075             {
1076                 foreach(var use in usings)
1077                 {
1078                     ret = names.Contains(use + name);
1079                     if(ret)
1080                     {
1081                         break;
1082                     }
1083                 }
1084             }
1085             return ret;
1086         }
1087 
IsNameAvailableInEmulationManager(string name)1088         private bool IsNameAvailableInEmulationManager(string name)
1089         {
1090             var info = GetMonitorInfo(typeof(EmulationManager));
1091             return info.AllNames.Contains(name);
1092         }
1093 
1094         private static IEnumerable<T> AllButLast<T>(IEnumerable<T> value) where T : class
1095         {
1096             var list = value.ToList();
1097             if(list.Any())
1098             {
1099                 var last = list.Last();
1100                 return list.Where(x => x != last);
1101             }
1102             return value;
1103         }
1104 
AllButLastAndAggregate(IEnumerable<String> value, bool dontDropLast = false)1105         private static String AllButLastAndAggregate(IEnumerable<String> value, bool dontDropLast = false)
1106         {
1107             if(dontDropLast)
1108             {
1109                 return value.Any() ? value.Aggregate((x, y) => x + ' ' + y) : string.Empty;
1110             }
1111             var list = value.ToList();
1112             if(list.Count < 2)
1113             {
1114                 return String.Empty;
1115             }
1116             var output = AllButLast(value);
1117             return output.Aggregate((x, y) => x + ' ' + y);
1118         }
1119 
Bind(string name, Func<object> objectServer)1120         public void Bind(string name, Func<object> objectServer)
1121         {
1122             objectDelegateMappings[name] = objectServer;
1123         }
1124 
BindStatic(string name, Func<object> objectServer)1125         public void BindStatic(string name, Func<object> objectServer)
1126         {
1127             staticObjectDelegateMappings[name] = objectServer;
1128         }
1129 
FromStaticMapping(string name)1130         private object FromStaticMapping(string name)
1131         {
1132             Func<object> value;
1133             if(staticObjectDelegateMappings.TryGetValue(name, out value))
1134             {
1135                 return value();
1136             }
1137             return null;
1138         }
1139 
FromMapping(string name)1140         private object FromMapping(string name)
1141         {
1142             Func<object> value;
1143             if(objectDelegateMappings.TryGetValue(name, out value))
1144             {
1145                 return value();
1146             }
1147             return null;
1148         }
1149 
1150         public IEnumerable<Command> RegisteredCommands
1151         {
1152             get
1153             {
1154                 return Commands;
1155             }
1156         }
1157 
1158         private readonly Dictionary<string, Func<object>> staticObjectDelegateMappings = new Dictionary<string, Func<object>>();
1159         private readonly Dictionary<string, Func<object>> objectDelegateMappings = new Dictionary<string, Func<object>>();
1160         private readonly Dictionary<string, Token> variables = new Dictionary<string, Token>();
1161         private readonly Dictionary<string, Token> macros = new Dictionary<string, Token>();
1162         private readonly Dictionary<string, Token> aliases = new Dictionary<string, Token>();
1163         private readonly Dictionary<VariableType, Dictionary<string, Token>> variableCollections;
1164         private int stringEaterMode;
1165         private string stringEaterValue = "";
1166         private string stringEaterVariableName = "";
1167         private VariableType? recordingType;
1168         private bool verboseMode;
1169 
1170         public ICommandInteraction Interaction { get; set; }
1171 
OnMachineRemoved(Machine m)1172         public void OnMachineRemoved(Machine m)
1173         {
1174             if(m == currentMachine)
1175             {
1176                 currentMachine = null;
1177             }
1178         }
1179 
1180         private IMachine _currentMachine;
1181 
1182         private IMachine currentMachine
1183         {
1184             get
1185             {
1186                 return _currentMachine;
1187             }
1188             set
1189             {
1190                 _currentMachine = value;
1191 
1192                 var mc = MachineChanged;
1193                 if(mc != null)
1194                 {
1195                     mc(_currentMachine != null ? Emulation[_currentMachine] : null);
1196                 }
1197             }
1198         }
1199 
1200         public event Action<string> MachineChanged;
1201 
1202         private readonly Tokenizer.Tokenizer tokenizer = Tokenizer.Tokenizer.CreateTokenizer();
1203 
CommandHandler(IEnumerable<Token> p, ICommandInteraction w)1204         internal delegate void CommandHandler(IEnumerable<Token> p, ICommandInteraction w);
1205 
1206         private const string globalVariablePrefix = "global.";
1207 
1208         private const string ConfigurationSection = "monitor";
1209 
1210         private const string EmulationToken = "emulation";
1211 
1212         private const string MultiLineTerminator = @"""""""";
1213 
1214         private const string OriginVariable = globalVariablePrefix + "ORIGIN";
1215 
1216         private const string CurrentDirectoryVariable = globalVariablePrefix + "CWD";
1217 
1218         private HashSet<Command> Commands { get; set; }
1219 
1220         private readonly MonitorPythonEngine pythonRunner;
1221 
1222         private enum VariableType
1223         {
1224             Variable,
1225             Macro,
1226             Alias
1227         }
1228     }
1229 }
1230