blob: 20aedb3f000e7910576d1172a8cc6eb4a53610a9 [file] [log] [blame]
//
// 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.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using Antmicro.Renode.Core;
using Antmicro.Renode.Exceptions;
using Antmicro.Renode.Logging;
using Antmicro.Renode.Network.ExternalControl;
using Antmicro.Renode.Utilities;
using Antmicro.Renode.Utilities.Packets;
namespace Antmicro.Renode.Network
{
public static class ExternalControlServerExtensions
{
public static void CreateExternalControlServer(this Emulation emulation, string name, int port)
{
emulation.ExternalsManager.AddExternal(new ExternalControlServer(port), name);
}
}
public class ExternalControlServer : IDisposable, IExternal, IEmulationElement
{
public ExternalControlServer(int port)
{
socketServerProvider.BufferSize = 0x10;
commandHandlers = new CommandHandlerCollection();
commandHandlers.Register(new RunFor(this));
socketServerProvider.ConnectionAccepted += delegate
{
state = State.Handshake;
this.Log(LogLevel.Debug, "Connection established");
};
socketServerProvider.ConnectionClosed += delegate
{
commandHandlers.ClearActivation();
state = State.NotConnected;
this.Log(LogLevel.Debug, "Connection closed");
};
socketServerProvider.DataBlockReceived += OnBytesWritten;
socketServerProvider.Start(port);
this.Log(LogLevel.Info, "{0}: Listening on port {1}", nameof(ExternalControlServer), port);
}
public void Dispose()
{
socketServerProvider.Stop();
state = State.Disposed;
}
private bool IsHeaderValid()
{
if(!header.HasValue)
{
return false;
}
if(header.Value.Magic != Magic)
{
return false;
}
if(header.Value.dataSize > (uint)Int32.MaxValue)
{
return false;
}
return true;
}
private bool TryActivateCommands(List<byte> data)
{
foreach(var pair in data.Split(2))
{
var command = (Command)pair[0];
var version = pair[1];
if(commandHandlers.TryActivate(command, version))
{
this.Log(LogLevel.Noisy, "{0} (version 0x{1:X}) activated", command, version);
continue;
}
var message = commandHandlers.TryGetVersion(command, out var expectedVersion)
? $"Encountered invalid version (0x{version:X}) for {command}, expected 0x{expectedVersion:X}"
: $"Encountered unknown command 0x{command:X}";
this.Log(LogLevel.Error, message);
SendResponse(Response.FatalError(message));
return false;
}
return true;
}
private void OnBytesWritten(byte[] data)
{
buffer.AddRange(data);
this.Log(LogLevel.Noisy, "Received new data: {0}", Misc.PrettyPrintCollectionHex(data));
this.Log(LogLevel.Debug, "Current buffer: {0}", Misc.PrettyPrintCollectionHex(buffer));
while(state != State.Disposed)
{
switch(state)
{
case State.Handshake:
if(buffer.Count < HandshakeHeaderSize)
{
return;
}
commandsToActivate = BitConverter.ToUInt16(buffer.GetRange(0, HandshakeHeaderSize).ToArray(), 0);
buffer.RemoveRange(0, HandshakeHeaderSize);
this.Log(LogLevel.Noisy, "{0} commands to activate", commandsToActivate);
SetState(State.WaitingForHandshakeData);
continue;
case State.WaitingForHandshakeData:
if(commandsToActivate > 0 && buffer.Count >= 2)
{
var toActivate = (int)Math.Min(commandsToActivate, buffer.Count / 2);
if(!TryActivateCommands(buffer.GetRange(0, toActivate * 2)))
{
socketServerProvider.Stop();
return;
}
buffer.RemoveRange(0, toActivate * 2);
commandsToActivate -= toActivate;
}
if(commandsToActivate > 0)
{
return;
}
SendResponse(Response.SuccessfulHandshake());
this.Log(LogLevel.Noisy, "Handshake finished");
SetState(State.WaitingForHeader);
continue;
case State.WaitingForHeader:
if(buffer.Count < HeaderSize)
{
return;
}
header = Packet.Decode<ExternalControlProtocolHeader>(buffer);
if(!IsHeaderValid())
{
var message = $"Encountered invalid header: {header}";
this.Log(LogLevel.Error, message);
SendResponse(Response.FatalError(message));
socketServerProvider.Stop();
return;
}
this.Log(LogLevel.Noisy, "Received header: {0}", header);
buffer.RemoveRange(0, HeaderSize);
SetState(State.WaitingForData);
continue;
case State.WaitingForData:
if(buffer.Count < header.Value.dataSize)
{
return;
}
TryHandleCommand(out var response, header.Value.command, buffer.GetRange(0, (int)header.Value.dataSize));
buffer.RemoveRange(0, (int)header.Value.dataSize);
header = null;
SendResponse(response);
SetState(State.WaitingForHeader);
continue;
case State.NotConnected:
default:
throw new Exception("Unreachable");
}
}
}
private bool TryHandleCommand(out Response response, Command command, List<byte> data)
{
try
{
response = commandHandlers.Invoke(command, data);
return true;
}
catch(RecoverableException e)
{
this.Log(LogLevel.Error, "{0} command error: {1}", command, e.Message);
response = Response.CommandFailed(command, e.Message);
return false;
}
}
private void SendResponse(Response response)
{
var bytes = response.GetBytes();
socketServerProvider.Send(bytes);
this.Log(LogLevel.Debug, "Response sent: {0}", response);
this.Log(LogLevel.Noisy, "Bytes sent: {0}", Misc.PrettyPrintCollectionHex(bytes));
}
private void SetState(State newState)
{
if(state == State.Disposed)
{
return;
}
state = newState;
}
private State state = State.NotConnected;
private int commandsToActivate = 0;
private ExternalControlProtocolHeader? header;
private readonly List<byte> buffer = new List<byte>();
private readonly CommandHandlerCollection commandHandlers;
private readonly SocketServerProvider socketServerProvider = new SocketServerProvider(emitConfigBytes: false);
private const int HeaderSize = 7;
private const int HandshakeHeaderSize = 2;
private const string Magic = "RE";
[LeastSignificantByteFirst]
private struct ExternalControlProtocolHeader
{
public override string ToString()
{
return $"{{ magic: {Misc.PrettyPrintCollectionHex(magic)} ({Magic}), command: 0x{(byte)command} ({command}), dataSize: {dataSize} }}";
}
public string Magic
{
get
{
try
{
return Encoding.ASCII.GetString(magic);
}
catch
{
return "<invalid>";
}
}
}
#pragma warning disable 649
[PacketField, Width(2)]
public byte[] magic;
[PacketField, Width(8)]
public Command command;
[PacketField, Width(32)]
public uint dataSize;
#pragma warning restore 649
}
private class CommandHandlerCollection
{
public CommandHandlerCollection()
{
commandHandlers = new Dictionary<Command, ICommand>();
activeCommandHandlers = new Dictionary<Command, ICommand>();
}
public void Register(ICommand command)
{
commandHandlers.Add(command.Identifier, command);
}
public void ClearActivation()
{
activeCommandHandlers.Clear();
}
public bool TryActivate(Command id, byte version)
{
if(activeCommandHandlers.ContainsKey(id))
{
return false;
}
if(!commandHandlers.TryGetValue(id, out var command))
{
return false;
}
if(command.Version != version)
{
return false;
}
activeCommandHandlers.Add(command.Identifier, command);
return true;
}
public Response Invoke(Command id, List<byte> data)
{
if(!activeCommandHandlers.TryGetValue(id, out var command))
{
return Response.InvalidCommand(id);
}
return command.Invoke(data);
}
public bool TryGetVersion(Command id, out byte version)
{
if(!commandHandlers.TryGetValue(id, out var command))
{
version = default(byte);
return false;
}
version = command.Version;
return true;
}
private readonly Dictionary<Command, ICommand> commandHandlers;
private readonly Dictionary<Command, ICommand> activeCommandHandlers;
}
private enum State
{
NotConnected,
Handshake,
WaitingForHandshakeData,
WaitingForHeader,
WaitingForData,
Disposed,
}
}
}