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