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.Net;
10 using System.Net.Sockets;
11 using System.Text;
12 using System.Diagnostics;
13 using System.Threading;
14 using System.Threading.Tasks;
15 using System.Runtime.InteropServices;
16 using System.ComponentModel;
17 using Antmicro.Renode.Core;
18 using Antmicro.Renode.Logging;
19 using Antmicro.Renode.Exceptions;
20 using Antmicro.Renode.Peripherals;
21 using Antmicro.Renode.Peripherals.CPU;
22 using Antmicro.Renode.Plugins.CoSimulationPlugin.Connection.Protocols;
23 #if !PLATFORM_WINDOWS
24 using Mono.Unix.Native;
25 #endif
26 
27 namespace Antmicro.Renode.Plugins.CoSimulationPlugin.Connection
28 {
29     public class SocketConnection : ICoSimulationConnection, IDisposable
30     {
SocketConnection(IEmulationElement parentElement, int timeoutInMilliseconds, Action<ProtocolMessage> receiveAction, string address = null)31         public SocketConnection(IEmulationElement parentElement, int timeoutInMilliseconds, Action<ProtocolMessage> receiveAction, string address = null)
32         {
33             this.parentElement = parentElement;
34             this.address = address ?? DefaultAddress;
35             timeout = timeoutInMilliseconds;
36             receivedHandler = receiveAction;
37             mainSocketCommunicator = new SocketCommunicator(parentElement, timeout, this.address);
38             asyncSocketCommunicator = new SocketCommunicator(parentElement, Timeout.Infinite, this.address);
39 
40             pauseMRES = new ManualResetEventSlim(initialState: true);
41             receiveThread = new Thread(ReceiveLoop)
42             {
43                 IsBackground = true,
44                 Name = "CoSimulated.Receiver"
45             };
46         }
47 
Dispose()48         public void Dispose()
49         {
50             Abort();
51             pauseMRES.Dispose();
52         }
53 
Connect()54         public void Connect()
55         {
56             var success = true;
57             if(!mainSocketCommunicator.AcceptConnection(timeout))
58             {
59                 parentElement.Log(LogLevel.Error, $"Main socket failed to accept connection after timeout of {timeout}ms.");
60                 success = false;
61             }
62 
63             if(success && !asyncSocketCommunicator.AcceptConnection(timeout))
64             {
65                 parentElement.Log(LogLevel.Error, $"Async socket failed to accept connection after timeout of {timeout}ms.");
66                 success = false;
67             }
68 
69             if(success && !TryHandshake())
70             {
71                 parentElement.Log(LogLevel.Error, "Handshake with co-simulation failed.");
72                 success = false;
73             }
74 
75             if(!success)
76             {
77                 mainSocketCommunicator.ResetConnections();
78                 asyncSocketCommunicator.ResetConnections();
79                 KillCoSimulatedProcess();
80 
81                 LogAndThrowRE($"Connection to the cosimulated peripheral failed!");
82             }
83             else
84             {
85                 // If connected succesfully, listening sockets can be closed
86                 mainSocketCommunicator.CloseListener();
87                 asyncSocketCommunicator.CloseListener();
88 
89                 parentElement.Log(LogLevel.Debug, "Connected to the cosimulated peripheral!");
90             }
91 
92             lock(receiveThreadLock)
93             {
94                 if(!receiveThread.IsAlive && disposeInitiated == 0)
95                 {
96                     receiveThread.Start();
97                 }
98             }
99         }
100 
TrySendMessage(ProtocolMessage message)101         public bool TrySendMessage(ProtocolMessage message)
102         {
103             if(!IsConnected)
104             {
105                 parentElement.Log(LogLevel.Debug, "Didn't send message {0} - not connected to co-simulation", message);
106                 return false;
107             }
108             return mainSocketCommunicator.TrySendMessage(message);
109         }
110 
TryRespond(ProtocolMessage message)111         public bool TryRespond(ProtocolMessage message)
112         {
113             if(!IsConnected)
114             {
115                 return false;
116             }
117             return TrySendMessage(message);
118         }
119 
TryReceiveMessage(out ProtocolMessage message)120         public bool TryReceiveMessage(out ProtocolMessage message)
121         {
122             if(!IsConnected)
123             {
124                 message = default(ProtocolMessage);
125                 return false;
126             }
127             return mainSocketCommunicator.TryReceiveMessage(out message);
128         }
129 
HandleMessage()130         public void HandleMessage()
131         {
132         }
133 
Abort()134         public void Abort()
135         {
136             // This method is thread-safe and can be called many times.
137             if(Interlocked.CompareExchange(ref disposeInitiated, 1, 0) != 0)
138             {
139                 return;
140             }
141 
142             asyncSocketCommunicator.CancelCommunication();
143             lock(receiveThreadLock)
144             {
145                 if(receiveThread.IsAlive)
146                 {
147                     receiveThread.Join(timeout);
148                 }
149             }
150 
151             if(IsConnected)
152             {
153                 parentElement.DebugLog("Sending 'Disconnect' message to close peripheral gracefully...");
154                 TrySendMessage(new ProtocolMessage(ActionType.Disconnect, 0, 0, ProtocolMessage.NoPeripheralIndex));
155                 mainSocketCommunicator.CancelCommunication();
156             }
157 
158             if(cosimulatedProcess != null)
159             {
160                 // Ask cosimulatedProcess to close, kill if it doesn't
161                 if(!cosimulatedProcess.HasExited)
162                 {
163                     parentElement.DebugLog($"Co-simulated process '{simulationFilePath}' is still working...");
164                     if(cosimulatedProcess.WaitForExit(500))
165                     {
166                         parentElement.DebugLog("Co-simulated process exited gracefully.");
167                     }
168                     else
169                     {
170                         KillCoSimulatedProcess();
171                         parentElement.Log(LogLevel.Warning, "Co-simulated process had to be killed.");
172                     }
173                 }
174                 cosimulatedProcess.Dispose();
175             }
176 
177             mainSocketCommunicator.Dispose();
178             asyncSocketCommunicator.Dispose();
179         }
180 
181         public bool IsConnected => mainSocketCommunicator.Connected;
182 
183         public string Context
184         {
185             get
186             {
187                 return this.context;
188             }
189             set
190             {
191                 if(IsConnected)
192                 {
193                     throw new RecoverableException("Context cannot be modified while connected");
194                 }
195                 this.context = (value == "" || value == null) ? "{0} {1} {2}" : value;
196             }
197         }
198 
199         public string SimulationFilePath
200         {
201             set
202             {
203                 simulationFilePath = value;
204                 if(!File.Exists(simulationFilePath))
205                 {
206                     parentElement.Log(LogLevel.Error, $"Simulation file \"{value}\" doesn't exist.");
207                 }
208                 parentElement.Log(LogLevel.Debug,
209                     "Trying to run and connect to the cosimulated peripheral '{0}' through ports {1} and {2}...",
210                     value, mainSocketCommunicator.ListenerPort, asyncSocketCommunicator.ListenerPort);
211 #if !PLATFORM_WINDOWS
212                 Mono.Unix.Native.Syscall.chmod(value, FilePermissions.S_IRWXU); //setting permissions to 0x700
213 #endif
214                 InitCoSimulatedProcess(value);
215             }
216         }
217 
218         public string ConnectionParameters
219         {
220             get
221             {
222                 try
223                 {
224                     return String.Format(this.context,
225                         mainSocketCommunicator.ListenerPort, asyncSocketCommunicator.ListenerPort, address);
226                 }
227                 catch (FormatException e)
228                 {
229                     throw new RecoverableException(e.Message);
230                 }
231             }
232         }
233 
ReceiveLoop()234         private void ReceiveLoop()
235         {
236             while(asyncSocketCommunicator.Connected)
237             {
238                 pauseMRES.Wait();
239                 if(disposeInitiated != 0)
240                 {
241                     break;
242                 }
243                 else if(asyncSocketCommunicator.TryReceiveMessage(out var message))
244                 {
245                     HandleReceived(message);
246                 }
247                 else
248                 {
249                     AbortAndLogError("Connection error!");
250                 }
251             }
252         }
253 
InitCoSimulatedProcess(string filePath)254         private void InitCoSimulatedProcess(string filePath)
255         {
256             try
257             {
258                 cosimulatedProcess = new Process
259                 {
260                     StartInfo = new ProcessStartInfo(filePath)
261                     {
262                         UseShellExecute = false,
263                         Arguments = ConnectionParameters
264                     }
265                 };
266 
267                 cosimulatedProcess.Start();
268             }
269             catch(Exception e)
270             {
271                 cosimulatedProcess = null;
272                 LogAndThrowRE($"Error starting cosimulated peripheral!\n{e.Message}");
273             }
274         }
275 
LogAndThrowRE(string info)276         private void LogAndThrowRE(string info)
277         {
278             parentElement.Log(LogLevel.Error, info);
279             throw new RecoverableException(info);
280         }
281 
AbortAndLogError(string message)282         private void AbortAndLogError(string message)
283         {
284             if(disposeInitiated != 0)
285             {
286                 return;
287             }
288             parentElement.Log(LogLevel.Error, message);
289             Abort();
290 
291             // Due to deadlock, we need to abort CPU instead of pausing emulation.
292             throw new CpuAbortException();
293         }
294 
KillCoSimulatedProcess()295         private void KillCoSimulatedProcess()
296         {
297             try
298             {
299                 cosimulatedProcess?.Kill();
300             }
301             catch
302             {
303                 return;
304             }
305         }
306 
TryHandshake()307         private bool TryHandshake()
308         {
309             if(!TrySendMessage(new ProtocolMessage(ActionType.Handshake, 0, 0, ProtocolMessage.NoPeripheralIndex)))
310             {
311                 parentElement.Log(LogLevel.Error, "Failed to send handshake message to co-simulation.");
312                 return false;
313             }
314             if(!TryReceiveMessage(out var result))
315             {
316                 parentElement.Log(LogLevel.Error, "Failed to receive handshake response from co-simulation.");
317                 return false;
318             }
319             if(result.ActionId != ActionType.Handshake)
320             {
321                 parentElement.Log(LogLevel.Error, "Invalid handshake response received from co-simulation.");
322                 return false;
323             }
324 
325             return true;
326         }
327 
HandleReceived(ProtocolMessage message)328         private void HandleReceived(ProtocolMessage message)
329         {
330             switch(message.ActionId)
331             {
332                 case ActionType.LogMessage:
333                     // message.Address is used to transfer log length
334                     if(asyncSocketCommunicator.TryReceiveString(out var log, (int)message.Address))
335                     {
336                         parentElement.Log((LogLevel)(int)message.Data, $"Co-simulation: {log}");
337                     }
338                     else
339                     {
340                         parentElement.Log(LogLevel.Warning, "Failed to receive log message!");
341                     }
342                     break;
343                 default:
344                     receivedHandler(message);
345                     break;
346             }
347         }
348 
349         private volatile int disposeInitiated;
350         private string simulationFilePath;
351         private string context = "{0} {1} {2}";
352         private Process cosimulatedProcess;
353         private SocketCommunicator mainSocketCommunicator;
354         private SocketCommunicator asyncSocketCommunicator;
355         private Action<ProtocolMessage> receivedHandler;
356 
357         private readonly IEmulationElement parentElement;
358         private readonly int timeout;
359         private readonly string address;
360         private readonly Thread receiveThread;
361         private readonly object receiveThreadLock = new object();
362         private readonly ManualResetEventSlim pauseMRES;
363 
364         private const string DefaultAddress = "127.0.0.1";
365         private const int MaxPendingConnections = 1;
366 
367         private class SocketCommunicator
368         {
SocketCommunicator(IEmulationElement logger, int timeoutInMilliseconds, string address)369             public SocketCommunicator(IEmulationElement logger, int timeoutInMilliseconds, string address)
370             {
371                 disposalCTS = new CancellationTokenSource();
372                 channelTaskFactory = new TaskFactory<int>(disposalCTS.Token);
373                 this.logger = logger;
374                 this.address = address;
375                 timeout = timeoutInMilliseconds;
376                 ListenerPort = CreateListenerAndStartListening();
377             }
378 
Dispose()379             public void Dispose()
380             {
381                 listener?.Close(timeout);
382                 socket?.Close(timeout);
383                 disposalCTS.Dispose();
384             }
385 
AcceptConnection(int timeoutInMilliseconds)386             public bool AcceptConnection(int timeoutInMilliseconds)
387             {
388                 // Check if there's any connection waiting to be accepted (with timeout in MICROseconds)
389                 var acceptAttempt = listener.Poll(timeoutInMilliseconds * 1000, SelectMode.SelectRead);
390                 if(acceptAttempt)
391                 {
392                     socket = listener.Accept();
393                 }
394                 return acceptAttempt;
395             }
396 
CloseListener()397             public void CloseListener()
398             {
399                 listener.Close();
400                 listener = null;
401             }
402 
ResetConnections()403             public void ResetConnections()
404             {
405                 socket?.Close();
406 
407                 if(listener.Poll(0, SelectMode.SelectRead))
408                 {
409                     logger.DebugLog($"Clients are pending on the listening {ListenerPort} port. Connection queue will be reset.");
410 
411                     // There's no other way to reset listener's connection queue
412                     CloseListener();
413                     ListenerPort = CreateListenerAndStartListening();
414                 }
415             }
416 
CancelCommunication()417             public void CancelCommunication()
418             {
419                 disposalCTS.Cancel();
420             }
421 
TrySendMessage(ProtocolMessage message)422             public bool TrySendMessage(ProtocolMessage message)
423             {
424 #if DEBUG_LOG_COSIM_MESSAGES
425                 Logger.Log(LogLevel.Noisy, "Sending message to co-sim: {0}", message);
426 #endif
427                 var serializedMessage = message.Serialize();
428                 var size = serializedMessage.Length;
429                 var task = channelTaskFactory.FromAsync(
430                     (callback, state) => socket.BeginSend(serializedMessage, 0, size, SocketFlags.None, callback, state),
431                     socket.EndSend, state: null);
432 
433                 return WaitSendOrReceiveTask(task, size);
434             }
435 
TryReceiveMessage(out ProtocolMessage message)436             public bool TryReceiveMessage(out ProtocolMessage message)
437             {
438 #if DEBUG_LOG_COSIM_MESSAGES
439                 Logger.Log(LogLevel.Noisy, "Trying to receive message from co-sim");
440 #endif
441                 message = default(ProtocolMessage);
442 
443                 var result = TryReceive(out var buffer, Marshal.SizeOf(message));
444                 if(result)
445                 {
446                     message.Deserialize(buffer);
447                 }
448 #if DEBUG_LOG_COSIM_MESSAGES
449                 Logger.Log(LogLevel.Noisy, "Received message from co-sim: {0}", message);
450 #endif
451                 return result;
452             }
453 
TryReceiveString(out string message, int size)454             public bool TryReceiveString(out string message, int size)
455             {
456                 message = String.Empty;
457                 var result = TryReceive(out var buffer, size);
458                 if(result)
459                 {
460                     message = Encoding.ASCII.GetString(buffer);
461                 }
462                 return result;
463             }
464 
TryReceive(out byte[] buffer, int size)465             public bool TryReceive(out byte[] buffer, int size)
466             {
467                 buffer = null;
468                 var taskBuffer = new byte[size];
469                 var task = channelTaskFactory.FromAsync(
470                     (callback, state) => socket.BeginReceive(taskBuffer, 0, size, SocketFlags.None, callback, state),
471                     socket.EndReceive, state: null);
472 
473                 var isSuccess = WaitSendOrReceiveTask(task, size);
474                 if(isSuccess)
475                 {
476                     buffer = taskBuffer;
477                 }
478                 return isSuccess;
479             }
480 
481             public int ListenerPort { get; private set; }
482             public bool Connected => socket?.Connected ?? false;
483 
CreateListenerAndStartListening()484             private int CreateListenerAndStartListening()
485             {
486                 listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
487                 listener.Bind(new IPEndPoint(IPAddress.Parse(address), 0));
488 
489                 listener.Listen(MaxPendingConnections);
490                 return (listener.LocalEndPoint as IPEndPoint).Port;
491             }
492 
WaitSendOrReceiveTask(Task<int> task, int size)493             private bool WaitSendOrReceiveTask(Task<int> task, int size)
494             {
495                 try
496                 {
497                     task.Wait(timeout, channelTaskFactory.CancellationToken);
498                 }
499                 // Exceptions thrown from the task are always packed in AggregateException
500                 catch(AggregateException aggregateException)
501                 {
502                     foreach(var innerException in aggregateException.InnerExceptions)
503                     {
504                         logger.DebugLog("Send/Receive task exception: {0}", innerException.Message);
505                     }
506                 }
507                 catch(OperationCanceledException)
508                 {
509                     logger.DebugLog("Send/Receive task was canceled.");
510                 }
511 
512                 if(task.Status != TaskStatus.RanToCompletion)
513                 {
514                     if(task.Status == TaskStatus.Canceled)
515                     {
516                         logger.DebugLog("Send/Receive task canceled (e.g. due to removing the peripheral).");
517                     }
518                     else
519                     {
520                         logger.DebugLog("Error while trying to Send/Receive. Task status: {0}", task.Status);
521                     }
522                     return false;
523                 }
524 
525                 if(task.Result != size)
526                 {
527                     logger.DebugLog("Error while trying to Send/Receive. Unexpected number of sent/received bytes: {0} (expected {1})", task.Result, size);
528                     return false;
529                 }
530 #if DEBUG_LOG_COSIM_MESSAGES
531                 logger.NoisyLog("Message sent/received succesfully", task.Status);
532 #endif
533                 return true;
534             }
535 
536             private Socket listener;
537             private Socket socket;
538 
539             private readonly int timeout;
540             private readonly string address;
541             private readonly CancellationTokenSource disposalCTS;
542             private readonly TaskFactory<int> channelTaskFactory;
543             private readonly IEmulationElement logger;
544         }
545     }
546 }
547