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