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