1 // 2 // Copyright (c) 2010-2025 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 using System; 8 using System.IO; 9 using System.Linq; 10 using System.Collections.Generic; 11 using Antmicro.Renode.Core; 12 using Antmicro.Renode.UserInterface; 13 using Antmicro.Renode.Utilities; 14 using Antmicro.Renode.Logging; 15 using Antmicro.Renode.Exceptions; 16 using System.Runtime.ExceptionServices; 17 18 namespace Antmicro.Renode.RobotFramework 19 { 20 internal class RenodeKeywords : IRobotFrameworkKeywordProvider 21 { RenodeKeywords()22 public RenodeKeywords() 23 { 24 monitor = ObjectCreator.Instance.GetSurrogate<Monitor>(); 25 savepoints = new Dictionary<string, Savepoint>(); 26 if(!(monitor.Interaction is CommandInteractionWrapper)) 27 { 28 monitor.Interaction = new CommandInteractionWrapper(monitor.Interaction); 29 } 30 } 31 Dispose()32 public void Dispose() 33 { 34 var interaction = monitor.Interaction as CommandInteractionWrapper; 35 monitor.Interaction = interaction.UnderlyingCommandInteraction; 36 TemporaryFilesManager.Instance.Cleanup(); 37 } 38 39 [RobotFrameworkKeyword] ResetEmulation()40 public void ResetEmulation() 41 { 42 EmulationManager.Instance.Clear(); 43 Recorder.Instance.ClearEvents(); 44 } 45 46 [RobotFrameworkKeyword(replayMode: Replay.Always)] StartEmulation()47 public void StartEmulation() 48 { 49 EmulationManager.Instance.CurrentEmulation.StartAll(); 50 } 51 52 [RobotFrameworkKeyword] ExecuteCommand(string command, string machine = null)53 public string ExecuteCommand(string command, string machine = null) 54 { 55 var interaction = monitor.Interaction as CommandInteractionWrapper; 56 interaction.Clear(); 57 SetMonitorMachine(machine); 58 59 if(!monitor.Parse(command)) 60 { 61 throw new KeywordException("Could not execute command '{0}': {1}", command, interaction.GetError()); 62 } 63 64 var error = interaction.GetError(); 65 if(!string.IsNullOrEmpty(error)) 66 { 67 throw new KeywordException($"There was an error when executing command '{command}': {error}"); 68 } 69 70 return interaction.GetContents(); 71 } 72 73 [RobotFrameworkKeyword] ExecutePython(string command, string machine = null)74 public object ExecutePython(string command, string machine = null) 75 { 76 SetMonitorMachine(machine); 77 78 try 79 { 80 return monitor.ExecutePythonCommand(command); 81 } 82 catch(RecoverableException ex) 83 { 84 // Rethrow the inner exception preserving the stack trace, the return is unreachable 85 ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); 86 return null; 87 } 88 } 89 90 [RobotFrameworkKeyword] 91 // This method accepts array of strings that is later 92 // concatenated using single space and parsed by the monitor. 93 // 94 // Using array instead of a single string allows us to 95 // split long commands into several lines using (...) 96 // notation in robot script; otherwise it would be impossible 97 // as there is no option to split a single parameter. ExecuteCommand(string[] commandFragments, string machine = null)98 public string ExecuteCommand(string[] commandFragments, string machine = null) 99 { 100 var command = string.Join(" ", commandFragments); 101 return ExecuteCommand(command, machine); 102 } 103 104 [RobotFrameworkKeyword] ExecuteScript(string path)105 public string ExecuteScript(string path) 106 { 107 var interaction = monitor.Interaction as CommandInteractionWrapper; 108 interaction.Clear(); 109 110 if(!monitor.TryExecuteScript(path, interaction)) 111 { 112 throw new KeywordException("Could not execute script: {0}", interaction.GetError()); 113 } 114 115 return interaction.GetContents(); 116 } 117 118 [RobotFrameworkKeyword(replayMode: Replay.Always)] StopRemoteServer()119 public void StopRemoteServer() 120 { 121 var robotFrontendEngine = (RobotFrameworkEngine)ObjectCreator.Instance.GetSurrogate(typeof(RobotFrameworkEngine)); 122 robotFrontendEngine.Shutdown(); 123 } 124 125 [RobotFrameworkKeyword] HandleHotSpot(HotSpotAction action)126 public void HandleHotSpot(HotSpotAction action) 127 { 128 var isStarted = EmulationManager.Instance.CurrentEmulation.IsStarted; 129 switch(action) 130 { 131 case HotSpotAction.None: 132 // do nothing 133 break; 134 case HotSpotAction.Pause: 135 if(isStarted) 136 { 137 EmulationManager.Instance.CurrentEmulation.PauseAll(); 138 EmulationManager.Instance.CurrentEmulation.StartAll(); 139 } 140 break; 141 case HotSpotAction.Serialize: 142 var fileName = TemporaryFilesManager.Instance.GetTemporaryFile(); 143 var monitor = ObjectCreator.Instance.GetSurrogate<Monitor>(); 144 if(monitor.Machine != null) 145 { 146 EmulationManager.Instance.CurrentEmulation.AddOrUpdateInBag("monitor_machine", monitor.Machine); 147 } 148 EmulationManager.Instance.Save(fileName); 149 EmulationManager.Instance.Load(fileName); 150 if(EmulationManager.Instance.CurrentEmulation.TryGetFromBag<Machine>("monitor_machine", out var mac)) 151 { 152 monitor.Machine = mac; 153 } 154 if(isStarted) 155 { 156 EmulationManager.Instance.CurrentEmulation.StartAll(); 157 } 158 break; 159 default: 160 throw new KeywordException("Hot spot action {0} is not currently supported", action); 161 } 162 } 163 164 [RobotFrameworkKeyword(replayMode: Replay.Never)] Provides(string state, ProviderType type = ProviderType.Serialization)165 public void Provides(string state, ProviderType type = ProviderType.Serialization) 166 { 167 if(type == ProviderType.Serialization) 168 { 169 var tempfileName = AllocateTemporaryFile(); 170 EmulationManager.Instance.CurrentEmulation.TryGetEmulationElementName(monitor.Machine, out var currentMachine); 171 EmulationManager.Instance.Save(tempfileName); 172 savepoints[state] = new Savepoint(currentMachine, tempfileName); 173 } 174 Recorder.Instance.SaveCurrentState(state); 175 } 176 177 [RobotFrameworkKeyword(replayMode: Replay.Never)] Requires(string state)178 public void Requires(string state) 179 { 180 List<Recorder.Event> events; 181 if(!Recorder.Instance.TryGetState(state, out events)) 182 { 183 throw new KeywordException("Required state {0} not found.", state); 184 } 185 ResetEmulation(); 186 var robotFrontendEngine = (RobotFrameworkEngine)ObjectCreator.Instance.GetSurrogate(typeof(RobotFrameworkEngine)); 187 188 IEnumerable<Recorder.Event> eventsToExecute = events; 189 var isSerialized = savepoints.TryGetValue(state, out var savepoint); 190 191 if(isSerialized) 192 { 193 EmulationManager.Instance.Load(savepoint.Filename); 194 if(savepoint.SelectedMachine!= null) 195 { 196 ExecuteCommand($"mach set \"{savepoint.SelectedMachine}\""); 197 } 198 eventsToExecute = eventsToExecute.Where(x => (x.ReplayMode == Replay.Always || x.ReplayMode == Replay.InSerializationMode)); 199 } 200 201 foreach(var e in eventsToExecute) 202 { 203 robotFrontendEngine.ExecuteKeyword(e.Name, e.Arguments); 204 } 205 } 206 207 [RobotFrameworkKeyword] WaitForPause(float timeout)208 public void WaitForPause(float timeout) 209 { 210 var masterTimeSource = EmulationManager.Instance.CurrentEmulation.MasterTimeSource; 211 var mre = new System.Threading.ManualResetEvent(false); 212 var callback = (Action)(() => 213 { 214 // it is possible that the block hook is triggered before virtual time has passed 215 // - in such case it should not be interpreted as a machine pause 216 if(masterTimeSource.ElapsedVirtualTime.Ticks > 0) 217 { 218 mre.Set(); 219 } 220 }); 221 222 var timeoutEvent = masterTimeSource.EnqueueTimeoutEvent((uint)(timeout * 1000)); 223 224 try 225 { 226 masterTimeSource.BlockHook += callback; 227 System.Threading.WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, mre }); 228 229 if(timeoutEvent.IsTriggered) 230 { 231 throw new KeywordException($"Emulation did not pause in expected time of {timeout} seconds."); 232 } 233 } 234 finally 235 { 236 masterTimeSource.BlockHook -= callback; 237 } 238 } 239 240 [RobotFrameworkKeyword] WaitForGdbConnection(int port, string machine = null, bool pauseToWait = true, bool acceptRunningServer = true)241 public void WaitForGdbConnection(int port, string machine = null, bool pauseToWait = true, bool acceptRunningServer = true) 242 { 243 IMachine machineObject; 244 if(machine == null) 245 { 246 machineObject = TestersProvider<object, IEmulationElement>.TryGetDefaultMachineOrThrowKeywordException(); 247 } 248 else if(!EmulationManager.Instance.CurrentEmulation.TryGetMachineByName(machine, out machineObject)) 249 { 250 throw new KeywordException("Machine with name {0} not found. Available machines: [{1}]", machine, 251 string.Join(", ", EmulationManager.Instance.CurrentEmulation.Names)); 252 } 253 254 if(pauseToWait) 255 { 256 machineObject.PauseAndRequestEmulationPause(); 257 } 258 259 if(machineObject.IsGdbConnectedToServer(port) && acceptRunningServer) 260 { 261 // A server is already running, so no need to wait 262 return; 263 } 264 265 // Since this keyword is likely to be used to manually inspect running application or in issue reproduction cases 266 // make sure that the user is informed about the need to connect 267 machineObject.Log(LogLevel.Warning, "Awaiting GDB connection on port {0}", port); 268 269 var connectedEvent = new System.Threading.ManualResetEvent(false); 270 Action<Stream> listener = delegate 271 { 272 connectedEvent.Set(); 273 }; 274 275 if(!machineObject.AttachConnectionAcceptedListenerToGdbStub(port, listener)) 276 { 277 throw new KeywordException($"No GDB server running on port {port}. Cannot await GDB connection"); 278 } 279 connectedEvent.WaitOne(); 280 // If we fail here, we can't do anything - the stub might have disconnected 281 machineObject.DetachConnectionAcceptedListenerFromGdbStub(port, listener); 282 } 283 284 [RobotFrameworkKeyword(replayMode: Replay.Always)] AllocateTemporaryFile()285 public string AllocateTemporaryFile() 286 { 287 return TemporaryFilesManager.Instance.GetTemporaryFile(); 288 } 289 290 [RobotFrameworkKeyword] DownloadFile(string uri)291 public string DownloadFile(string uri) 292 { 293 if(!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri)) 294 { 295 throw new KeywordException($"Wrong URI format: {uri}"); 296 } 297 298 var fileFetcher = EmulationManager.Instance.CurrentEmulation.FileFetcher; 299 if(!fileFetcher.TryFetchFromUri(parsedUri, out var result)) 300 { 301 throw new KeywordException("Couldn't download file from: {uri}"); 302 } 303 304 return result; 305 } 306 307 [RobotFrameworkKeyword(replayMode: Replay.Always)] CreateLogTester(float timeout, bool? defaultPauseEmulation = null)308 public void CreateLogTester(float timeout, bool? defaultPauseEmulation = null) 309 { 310 this.defaultPauseEmulation = defaultPauseEmulation.GetValueOrDefault(); 311 logTester = new LogTester(timeout); 312 Logging.Logger.AddBackend(logTester, "Log Tester", true); 313 } 314 315 [RobotFrameworkKeyword] WaitForLogEntry(string pattern, float? timeout = null, bool keep = false, bool treatAsRegex = false, bool? pauseEmulation = null, LogLevel level = null)316 public string WaitForLogEntry(string pattern, float? timeout = null, bool keep = false, bool treatAsRegex = false, 317 bool? pauseEmulation = null, LogLevel level = null) 318 { 319 CheckLogTester(); 320 321 var result = logTester.WaitForEntry(pattern, out var bufferedMessages, timeout, keep, treatAsRegex, pauseEmulation ?? defaultPauseEmulation, level); 322 if(result == null) 323 { 324 // We must limit the length of the resulting string to Int32.MaxValue to avoid OutOfMemoryException. 325 // 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. 326 // We arbitrarily chose the number of messages to include here. In theory it could still throw during string.Join operation given very long messages, 327 // but it's unlikely to happen given the value of Int32.MaxValue = 2,147,483,647. 328 var logContextMessages = bufferedMessages.TakeLast(MaxLogContextPrintedOnException); 329 var logMessages = string.Join("\n ", logContextMessages); 330 throw new KeywordException($"Expected pattern \"{pattern}\" did not appear in the log\nLast {logContextMessages.Count()} buffered log messages are: \n {logMessages}"); 331 } 332 return result; 333 } 334 335 [RobotFrameworkKeyword] ShouldNotBeInLog(String pattern, float? timeout = null, bool treatAsRegex = false, bool? pauseEmulation = null, LogLevel level = null)336 public void ShouldNotBeInLog(String pattern, float? timeout = null, bool treatAsRegex = false, bool? pauseEmulation = null, LogLevel level = null) 337 { 338 CheckLogTester(); 339 340 // Passing `level` as a named argument causes a compiler crash in Mono 6.8.0.105+dfsg-3.4 341 // from Debian 342 var result = logTester.WaitForEntry(pattern, out var _, timeout, true, treatAsRegex, pauseEmulation ?? defaultPauseEmulation, level); 343 if(result != null) 344 { 345 throw new KeywordException($"Unexpected line detected in the log: {result}"); 346 } 347 } 348 349 [RobotFrameworkKeyword] ClearLogTesterHistory()350 public void ClearLogTesterHistory() 351 { 352 CheckLogTester(); 353 logTester.ClearHistory(); 354 } 355 356 [RobotFrameworkKeyword(replayMode: Replay.Never)] LogToFile(string filePath, bool flushAfterEveryWrite = false)357 public void LogToFile(string filePath, bool flushAfterEveryWrite = false) 358 { 359 Logger.AddBackend(new FileBackend(filePath, flushAfterEveryWrite), "file", true); 360 } 361 362 [RobotFrameworkKeyword] OpenGUI()363 public void OpenGUI() 364 { 365 Emulator.OpenGUI(); 366 } 367 368 [RobotFrameworkKeyword] CloseGUI()369 public void CloseGUI() 370 { 371 Emulator.CloseGUI(); 372 } 373 374 [RobotFrameworkKeyword] EnableLoggingToCache()375 public void EnableLoggingToCache() 376 { 377 if(cachedLogFilePath == null) 378 { 379 cachedLogFilePath = Path.Combine( 380 TemporaryFilesManager.Instance.EmulatorTemporaryPath, 381 "renode-robot.log"); 382 Logger.AddBackend(new FileBackend(cachedLogFilePath, false), CachedLogBackendName, true); 383 } 384 } 385 386 [RobotFrameworkKeyword] SaveCachedLog(string filePath)387 public void SaveCachedLog(string filePath) 388 { 389 if(cachedLogFilePath == null) 390 { 391 throw new KeywordException($"Cannot save cached log, cached logging has not been enabled."); 392 } 393 394 (Logger.GetBackends()[CachedLogBackendName] as FileBackend).Flush(); 395 System.IO.File.Copy(cachedLogFilePath, filePath, true); 396 } 397 398 [RobotFrameworkKeyword] ClearCachedLog()399 public void ClearCachedLog() 400 { 401 if(cachedLogFilePath != null) 402 { 403 Logger.RemoveBackend(Logger.GetBackends()[CachedLogBackendName]); 404 System.IO.File.Delete(cachedLogFilePath); 405 cachedLogFilePath = null; 406 EnableLoggingToCache(); 407 } 408 } 409 CheckLogTester()410 private void CheckLogTester() 411 { 412 if(logTester == null) 413 { 414 throw new KeywordException("Log tester is not available. Create it with the `CreateLogTester` keyword"); 415 } 416 } 417 SetMonitorMachine(string machine)418 private void SetMonitorMachine(string machine) 419 { 420 if(!string.IsNullOrWhiteSpace(machine)) 421 { 422 if(!EmulationManager.Instance.CurrentEmulation.TryGetMachineByName(machine, out var machobj)) 423 { 424 throw new KeywordException("Could not find machine named {0} in the emulation", machine); 425 } 426 monitor.Machine = machobj; 427 } 428 } 429 430 private readonly Dictionary<string, Savepoint> savepoints; 431 432 private LogTester logTester; 433 private string cachedLogFilePath; 434 private bool defaultPauseEmulation; 435 436 private readonly Monitor monitor; 437 438 private const string CachedLogBackendName = "cache"; 439 private const int MaxLogContextPrintedOnException = 1000; 440 441 private struct Savepoint 442 { SavepointAntmicro.Renode.RobotFramework.RenodeKeywords.Savepoint443 public Savepoint(string selectedMachine, string filename) 444 { 445 SelectedMachine = selectedMachine; 446 Filename = filename; 447 } 448 449 public string SelectedMachine { get; } 450 public string Filename { get; } 451 } 452 453 public enum ProviderType 454 { 455 Serialization = 0, 456 Reexecution = 1, 457 } 458 } 459 } 460 461