| // | |
| // Copyright (c) 2010-2024 Antmicro | |
| // | |
| // This file is licensed under the MIT License. | |
| // Full license text is available in 'licenses/MIT.txt'. | |
| // | |
| using System; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Collections.Generic; | |
| using Antmicro.Renode.Core; | |
| using Antmicro.Renode.UserInterface; | |
| using Antmicro.Renode.Utilities; | |
| using Antmicro.Renode.Logging; | |
| using Antmicro.Renode.Exceptions; | |
| using System.Runtime.ExceptionServices; | |
| namespace Antmicro.Renode.RobotFramework | |
| { | |
| internal class RenodeKeywords : IRobotFrameworkKeywordProvider | |
| { | |
| public RenodeKeywords() | |
| { | |
| monitor = ObjectCreator.Instance.GetSurrogate<Monitor>(); | |
| savepoints = new Dictionary<string, Savepoint>(); | |
| if(!(monitor.Interaction is CommandInteractionWrapper)) | |
| { | |
| monitor.Interaction = new CommandInteractionWrapper(monitor.Interaction); | |
| } | |
| } | |
| public void Dispose() | |
| { | |
| var interaction = monitor.Interaction as CommandInteractionWrapper; | |
| monitor.Interaction = interaction.UnderlyingCommandInteraction; | |
| TemporaryFilesManager.Instance.Cleanup(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public void ResetEmulation() | |
| { | |
| EmulationManager.Instance.Clear(); | |
| Recorder.Instance.ClearEvents(); | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] | |
| public void StartEmulation() | |
| { | |
| EmulationManager.Instance.CurrentEmulation.StartAll(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public string ExecuteCommand(string command, string machine = null) | |
| { | |
| var interaction = monitor.Interaction as CommandInteractionWrapper; | |
| interaction.Clear(); | |
| SetMonitorMachine(machine); | |
| if(!monitor.Parse(command)) | |
| { | |
| throw new KeywordException("Could not execute command '{0}': {1}", command, interaction.GetError()); | |
| } | |
| var error = interaction.GetError(); | |
| if(!string.IsNullOrEmpty(error)) | |
| { | |
| throw new KeywordException($"There was an error when executing command '{command}': {error}"); | |
| } | |
| return interaction.GetContents(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public object ExecutePython(string command, string machine = null) | |
| { | |
| SetMonitorMachine(machine); | |
| try | |
| { | |
| return monitor.ExecutePythonCommand(command); | |
| } | |
| catch(RecoverableException ex) | |
| { | |
| // Rethrow the inner exception preserving the stack trace, the return is unreachable | |
| ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); | |
| return null; | |
| } | |
| } | |
| [RobotFrameworkKeyword] | |
| // This method accepts array of strings that is later | |
| // concatenated using single space and parsed by the monitor. | |
| // | |
| // Using array instead of a single string allows us to | |
| // split long commands into several lines using (...) | |
| // notation in robot script; otherwise it would be impossible | |
| // as there is no option to split a single parameter. | |
| public string ExecuteCommand(string[] commandFragments, string machine = null) | |
| { | |
| var command = string.Join(" ", commandFragments); | |
| return ExecuteCommand(command, machine); | |
| } | |
| [RobotFrameworkKeyword] | |
| public string ExecuteScript(string path) | |
| { | |
| var interaction = monitor.Interaction as CommandInteractionWrapper; | |
| interaction.Clear(); | |
| if(!monitor.TryExecuteScript(path, interaction)) | |
| { | |
| throw new KeywordException("Could not execute script: {0}", interaction.GetError()); | |
| } | |
| return interaction.GetContents(); | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] | |
| public void StopRemoteServer() | |
| { | |
| var robotFrontendEngine = (RobotFrameworkEngine)ObjectCreator.Instance.GetSurrogate(typeof(RobotFrameworkEngine)); | |
| robotFrontendEngine.Shutdown(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public void HandleHotSpot(HotSpotAction action) | |
| { | |
| var isStarted = EmulationManager.Instance.CurrentEmulation.IsStarted; | |
| switch(action) | |
| { | |
| case HotSpotAction.None: | |
| // do nothing | |
| break; | |
| case HotSpotAction.Pause: | |
| if(isStarted) | |
| { | |
| EmulationManager.Instance.CurrentEmulation.PauseAll(); | |
| EmulationManager.Instance.CurrentEmulation.StartAll(); | |
| } | |
| break; | |
| case HotSpotAction.Serialize: | |
| var fileName = TemporaryFilesManager.Instance.GetTemporaryFile(); | |
| var monitor = ObjectCreator.Instance.GetSurrogate<Monitor>(); | |
| if(monitor.Machine != null) | |
| { | |
| EmulationManager.Instance.CurrentEmulation.AddOrUpdateInBag("monitor_machine", monitor.Machine); | |
| } | |
| EmulationManager.Instance.Save(fileName); | |
| EmulationManager.Instance.Load(fileName); | |
| if(EmulationManager.Instance.CurrentEmulation.TryGetFromBag<Machine>("monitor_machine", out var mac)) | |
| { | |
| monitor.Machine = mac; | |
| } | |
| if(isStarted) | |
| { | |
| EmulationManager.Instance.CurrentEmulation.StartAll(); | |
| } | |
| break; | |
| default: | |
| throw new KeywordException("Hot spot action {0} is not currently supported", action); | |
| } | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Never)] | |
| public void Provides(string state, ProviderType type = ProviderType.Serialization) | |
| { | |
| if(type == ProviderType.Serialization) | |
| { | |
| var tempfileName = AllocateTemporaryFile(); | |
| EmulationManager.Instance.CurrentEmulation.TryGetEmulationElementName(monitor.Machine, out var currentMachine); | |
| EmulationManager.Instance.Save(tempfileName); | |
| savepoints[state] = new Savepoint(currentMachine, tempfileName); | |
| } | |
| Recorder.Instance.SaveCurrentState(state); | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Never)] | |
| public void Requires(string state) | |
| { | |
| List<Recorder.Event> events; | |
| if(!Recorder.Instance.TryGetState(state, out events)) | |
| { | |
| throw new KeywordException("Required state {0} not found.", state); | |
| } | |
| ResetEmulation(); | |
| var robotFrontendEngine = (RobotFrameworkEngine)ObjectCreator.Instance.GetSurrogate(typeof(RobotFrameworkEngine)); | |
| IEnumerable<Recorder.Event> eventsToExecute = events; | |
| var isSerialized = savepoints.TryGetValue(state, out var savepoint); | |
| if(isSerialized) | |
| { | |
| EmulationManager.Instance.Load(savepoint.Filename); | |
| if(savepoint.SelectedMachine!= null) | |
| { | |
| ExecuteCommand($"mach set \"{savepoint.SelectedMachine}\""); | |
| } | |
| eventsToExecute = eventsToExecute.Where(x => (x.ReplayMode == Replay.Always || x.ReplayMode == Replay.InSerializationMode)); | |
| } | |
| foreach(var e in eventsToExecute) | |
| { | |
| robotFrontendEngine.ExecuteKeyword(e.Name, e.Arguments); | |
| } | |
| } | |
| [RobotFrameworkKeyword] | |
| public void WaitForPause(float timeout) | |
| { | |
| var masterTimeSource = EmulationManager.Instance.CurrentEmulation.MasterTimeSource; | |
| var mre = new System.Threading.ManualResetEvent(false); | |
| var callback = (Action)(() => | |
| { | |
| // it is possible that the block hook is triggered before virtual time has passed | |
| // - in such case it should not be interpreted as a machine pause | |
| if(masterTimeSource.ElapsedVirtualTime.Ticks > 0) | |
| { | |
| mre.Set(); | |
| } | |
| }); | |
| var timeoutEvent = masterTimeSource.EnqueueTimeoutEvent((uint)(timeout * 1000)); | |
| try | |
| { | |
| masterTimeSource.BlockHook += callback; | |
| System.Threading.WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, mre }); | |
| if(timeoutEvent.IsTriggered) | |
| { | |
| throw new KeywordException($"Emulation did not pause in expected time of {timeout} seconds."); | |
| } | |
| } | |
| finally | |
| { | |
| masterTimeSource.BlockHook -= callback; | |
| } | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] | |
| public string AllocateTemporaryFile() | |
| { | |
| return TemporaryFilesManager.Instance.GetTemporaryFile(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public string DownloadFile(string uri) | |
| { | |
| if(!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri)) | |
| { | |
| throw new KeywordException($"Wrong URI format: {uri}"); | |
| } | |
| var fileFetcher = EmulationManager.Instance.CurrentEmulation.FileFetcher; | |
| if(!fileFetcher.TryFetchFromUri(parsedUri, out var result)) | |
| { | |
| throw new KeywordException("Couldn't download file from: {uri}"); | |
| } | |
| return result; | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] | |
| public void CreateLogTester(float timeout, bool? defaultPauseEmulation = null) | |
| { | |
| this.defaultPauseEmulation = defaultPauseEmulation.GetValueOrDefault(); | |
| logTester = new LogTester(timeout); | |
| Logging.Logger.AddBackend(logTester, "Log Tester", true); | |
| } | |
| [RobotFrameworkKeyword] | |
| public string WaitForLogEntry(string pattern, float? timeout = null, bool keep = false, bool treatAsRegex = false, | |
| bool? pauseEmulation = null, LogLevel level = null) | |
| { | |
| CheckLogTester(); | |
| var result = logTester.WaitForEntry(pattern, out var bufferedMessages, timeout, keep, treatAsRegex, pauseEmulation ?? defaultPauseEmulation, level); | |
| if(result == null) | |
| { | |
| // We must limit the length of the resulting string to Int32.MaxValue to avoid OutOfMemoryException. | |
| // We could do it accurately, but it doesn't seem worth here, because the goal is just to provide some extra context to the exception message. | |
| // We arbitrarily chose the number of messages to include here. In theory it could still throw during string.Join operation given very long messages, | |
| // but it's unlikely to happen given the value of Int32.MaxValue = 2,147,483,647. | |
| var logContextMessages = bufferedMessages.TakeLast(MaxLogContextPrintedOnException); | |
| var logMessages = string.Join("\n ", logContextMessages); | |
| throw new KeywordException($"Expected pattern \"{pattern}\" did not appear in the log\nLast {logContextMessages.Count()} buffered log messages are: \n {logMessages}"); | |
| } | |
| return result; | |
| } | |
| [RobotFrameworkKeyword] | |
| public void ShouldNotBeInLog(String pattern, float? timeout = null, bool treatAsRegex = false, bool? pauseEmulation = null, LogLevel level = null) | |
| { | |
| CheckLogTester(); | |
| // Passing `level` as a named argument causes a compiler crash in Mono 6.8.0.105+dfsg-3.4 | |
| // from Debian | |
| var result = logTester.WaitForEntry(pattern, out var _, timeout, true, treatAsRegex, pauseEmulation ?? defaultPauseEmulation, level); | |
| if(result != null) | |
| { | |
| throw new KeywordException($"Unexpected line detected in the log: {result}"); | |
| } | |
| } | |
| [RobotFrameworkKeyword] | |
| public void ClearLogTesterHistory() | |
| { | |
| CheckLogTester(); | |
| logTester.ClearHistory(); | |
| } | |
| [RobotFrameworkKeyword(replayMode: Replay.Never)] | |
| public void LogToFile(string filePath, bool flushAfterEveryWrite = false) | |
| { | |
| Logger.AddBackend(new FileBackend(filePath, flushAfterEveryWrite), "file", true); | |
| } | |
| [RobotFrameworkKeyword] | |
| public void OpenGUI() | |
| { | |
| Emulator.OpenGUI(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public void CloseGUI() | |
| { | |
| Emulator.CloseGUI(); | |
| } | |
| [RobotFrameworkKeyword] | |
| public void EnableLoggingToCache() | |
| { | |
| if(cachedLogFilePath == null) | |
| { | |
| cachedLogFilePath = Path.Combine( | |
| TemporaryFilesManager.Instance.EmulatorTemporaryPath, | |
| "renode-robot.log"); | |
| Logger.AddBackend(new FileBackend(cachedLogFilePath, false), CachedLogBackendName, true); | |
| } | |
| } | |
| [RobotFrameworkKeyword] | |
| public void SaveCachedLog(string filePath) | |
| { | |
| if(cachedLogFilePath == null) | |
| { | |
| throw new KeywordException($"Cannot save cached log, cached logging has not been enabled."); | |
| } | |
| (Logger.GetBackends()[CachedLogBackendName] as FileBackend).Flush(); | |
| System.IO.File.Copy(cachedLogFilePath, filePath, true); | |
| } | |
| [RobotFrameworkKeyword] | |
| public void ClearCachedLog() | |
| { | |
| if(cachedLogFilePath != null) | |
| { | |
| Logger.RemoveBackend(Logger.GetBackends()[CachedLogBackendName]); | |
| System.IO.File.Delete(cachedLogFilePath); | |
| cachedLogFilePath = null; | |
| EnableLoggingToCache(); | |
| } | |
| } | |
| private void CheckLogTester() | |
| { | |
| if(logTester == null) | |
| { | |
| throw new KeywordException("Log tester is not available. Create it with the `CreateLogTester` keyword"); | |
| } | |
| } | |
| private void SetMonitorMachine(string machine) | |
| { | |
| if(!string.IsNullOrWhiteSpace(machine)) | |
| { | |
| if(!EmulationManager.Instance.CurrentEmulation.TryGetMachineByName(machine, out var machobj)) | |
| { | |
| throw new KeywordException("Could not find machine named {0} in the emulation", machine); | |
| } | |
| monitor.Machine = machobj; | |
| } | |
| } | |
| private readonly Dictionary<string, Savepoint> savepoints; | |
| private LogTester logTester; | |
| private string cachedLogFilePath; | |
| private bool defaultPauseEmulation; | |
| private readonly Monitor monitor; | |
| private const string CachedLogBackendName = "cache"; | |
| private const int MaxLogContextPrintedOnException = 1000; | |
| private struct Savepoint | |
| { | |
| public Savepoint(string selectedMachine, string filename) | |
| { | |
| SelectedMachine = selectedMachine; | |
| Filename = filename; | |
| } | |
| public string SelectedMachine { get; } | |
| public string Filename { get; } | |
| } | |
| public enum ProviderType | |
| { | |
| Serialization = 0, | |
| Reexecution = 1, | |
| } | |
| } | |
| } | |