diff --git a/Tests/SimpleDebugClient/Program.cs b/Tests/SimpleDebugClient/Program.cs index b815ec1..fbdc00a 100644 --- a/Tests/SimpleDebugClient/Program.cs +++ b/Tests/SimpleDebugClient/Program.cs @@ -1,5 +1,6 @@ using Microsoft.Iris.Debug; using Microsoft.Iris.Debug.Data; +using Microsoft.Iris.Debug.SystemNet; using System; namespace SimpleDebugClient; @@ -10,12 +11,12 @@ internal class Program static int Main(string[] args) { - string connectionString = args.Length >= 2 - ? args[1] : "tcp://127.0.0.1:5556"; + var connectionString = args.Length >= 2 + ? new Uri(args[1]) : DebugRemoting.DEFAULT_TCP_URI; Console.CancelKeyPress += Console_CancelKeyPress; - Debugger = new ZmqDebuggerClient(connectionString); + Debugger = new NetDebuggerClient(connectionString); Debugger.DispatcherStep += Debugger_DispatcherStep; Debugger.InterpreterStep += Debugger_InterpreterStep; diff --git a/Tests/SimpleIrisApp/Program.cs b/Tests/SimpleIrisApp/Program.cs index 349263a..d14e869 100644 --- a/Tests/SimpleIrisApp/Program.cs +++ b/Tests/SimpleIrisApp/Program.cs @@ -1,4 +1,5 @@ using Microsoft.Iris; +using Microsoft.Iris.Debug; using System; namespace SimpleIrisApp; @@ -19,7 +20,7 @@ internal class Program }; Application.DebugSettings.DebugConnectionUri = args.Length >= 2 - ? args[1] : "tcp://127.0.0.1:5556"; + ? args[1] : DebugRemoting.DEFAULT_TCP_URI.OriginalString; Application.DebugSettings.Breakpoints.Add(new("clr-res://SimpleIrisApp!MainPage.uix", 3, 22)); #endif diff --git a/UIX/Microsoft/Iris/Debug/Data/DebuggerMessageFrame.cs b/UIX/Microsoft/Iris/Debug/Data/DebuggerMessageFrame.cs new file mode 100644 index 0000000..16ca654 --- /dev/null +++ b/UIX/Microsoft/Iris/Debug/Data/DebuggerMessageFrame.cs @@ -0,0 +1,57 @@ +using System; +using System.Text; + +namespace Microsoft.Iris.Debug.Data; + +public class DebuggerMessageFrame +{ + public DebuggerMessageFrame() { } + + public DebuggerMessageFrame(long transactionId, DebuggerMessageType type, byte[] data) + { + TransactionId = transactionId; + Type = type; + Data = data; + } + + public DebuggerMessageFrame(long transactionId, DebuggerMessageType type, string message, Encoding encoding = null) + : this(transactionId, type, (encoding ?? Encoding.UTF8).GetBytes(message)) + { + } + + public DebuggerMessageFrame(byte[] bytes) + { + TransactionId = BitConverter.ToInt64(bytes, 0); + Type = (DebuggerMessageType)BitConverter.ToInt32(bytes, sizeof(long)); + Data = bytes.AsSpan(sizeof(long) + sizeof(DebuggerMessageType)).ToArray(); + } + + public long TransactionId { get; set; } + + public DebuggerMessageType Type { get; set; } + + public byte[] Data { get; set; } + + public byte[] ToBytes() + { + byte[] bytes = new byte[Data.Length + sizeof(DebuggerMessageType) + sizeof(long)]; + +#if NET5_0_OR_GREATER + var span = bytes.AsSpan(); + BitConverter.TryWriteBytes(span, TransactionId); + BitConverter.TryWriteBytes(span[sizeof(long)..], (int)Type); +#else + var idBytes = BitConverter.GetBytes(TransactionId); + idBytes.CopyTo(bytes, 0); + + var typeBytes = BitConverter.GetBytes((int)Type); + typeBytes.CopyTo(bytes, sizeof(long)); +#endif + + Data.CopyTo(bytes, sizeof(long) + sizeof(DebuggerMessageType)); + + return bytes; + } + + public string GetDataAsString(Encoding encoding = null) => (encoding ?? Encoding.UTF8).GetString(Data); +} diff --git a/UIX/Microsoft/Iris/Debug/DebugRemoting.cs b/UIX/Microsoft/Iris/Debug/DebugRemoting.cs index a078cea..4a36bad 100644 --- a/UIX/Microsoft/Iris/Debug/DebugRemoting.cs +++ b/UIX/Microsoft/Iris/Debug/DebugRemoting.cs @@ -1,4 +1,8 @@ -using System.Runtime.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Runtime.Serialization; namespace Microsoft.Iris.Debug; @@ -7,9 +11,34 @@ namespace Microsoft.Iris.Debug; /// public static class DebugRemoting { - public const string DEFAULT_TCP_CLIENT_URI = ">tcp://127.0.0.1:5555,@tcp://127.0.0.1:55556"; + public const string DEFAULT_TCP_CLIENT_URI = ">tcp://127.0.0.1:5555,@tcp://127.0.0.1:5556"; + public const string DEFAULT_TCP_SERVER_URI = "@tcp://127.0.0.1:5555,>tcp://127.0.0.1:5556"; - public const string DEFAULT_TCP_SERVER_URI = "@tcp://127.0.0.1:5555,>tcp://127.0.0.1:55556"; + public static readonly Uri DEFAULT_TCP_URI = new("tcp://127.0.0.1:5555"); internal static IFormatter CreateBsonFormatter() => new BsonFormatter(new StreamingContext(StreamingContextStates.Remoting)); + + internal static Data.DebuggerMessageFrame ReceiveDebuggerMessage(Socket socket) + { + try + { + byte[] sizeBuffer = new byte[sizeof(int)]; + int bytesReceived = socket.Receive(sizeBuffer); + + // Reached end of stream, no bytes to recieve + if (bytesReceived == 0) + return null; + + int frameLength = BitConverter.ToInt32(sizeBuffer, 0); + + byte[] frameBytes = new byte[frameLength]; + socket.Receive(frameBytes, frameLength, SocketFlags.None); + + return new(frameBytes); + } + catch (SocketException) + { + return null; + } + } } diff --git a/UIX/Microsoft/Iris/Debug/IDebuggerClient.cs b/UIX/Microsoft/Iris/Debug/IDebuggerClient.cs index 5015f44..0d5a2ca 100644 --- a/UIX/Microsoft/Iris/Debug/IDebuggerClient.cs +++ b/UIX/Microsoft/Iris/Debug/IDebuggerClient.cs @@ -8,7 +8,7 @@ public interface IDebuggerClient /// /// The URI the client is connected to. /// - string ConnectionUri { get; } + Uri ConnectionUri { get; } /// /// Fired when the UIX interpreter steps forward. diff --git a/UIX/Microsoft/Iris/Debug/NetMQ/ZmqDebuggerClient.cs b/UIX/Microsoft/Iris/Debug/NetMQ/ZmqDebuggerClient.cs index 110baba..ef711a2 100644 --- a/UIX/Microsoft/Iris/Debug/NetMQ/ZmqDebuggerClient.cs +++ b/UIX/Microsoft/Iris/Debug/NetMQ/ZmqDebuggerClient.cs @@ -1,4 +1,3 @@ -using Microsoft.Iris.Debug; using Microsoft.Iris.Debug.Data; using NetMQ; using NetMQ.Sockets; @@ -16,14 +15,14 @@ public class ZmqDebuggerClient : IDebuggerClient, IDisposable private readonly PairSocket _socket; private readonly IFormatter _formatter; - public string ConnectionUri { get; } + public Uri ConnectionUri { get; } public event EventHandler InterpreterStep; public event Action DispatcherStep; public ZmqDebuggerClient(string connectionUri) { - ConnectionUri = connectionUri ?? DebugRemoting.DEFAULT_TCP_CLIENT_URI; + ConnectionUri = new(connectionUri ?? DebugRemoting.DEFAULT_TCP_CLIENT_URI); _socket = new(connectionUri); _formatter = DebugRemoting.CreateBsonFormatter(); diff --git a/UIX/Microsoft/Iris/Debug/SystemNet/NetDebuggerClient.cs b/UIX/Microsoft/Iris/Debug/SystemNet/NetDebuggerClient.cs new file mode 100644 index 0000000..4d4a442 --- /dev/null +++ b/UIX/Microsoft/Iris/Debug/SystemNet/NetDebuggerClient.cs @@ -0,0 +1,89 @@ +using Microsoft.Iris.Debug.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.Serialization; +using System.Text; + +namespace Microsoft.Iris.Debug.SystemNet; + +public class NetDebuggerClient : IDebuggerClient, IDisposable +{ + public Uri ConnectionUri { get; } + + private readonly Socket _socket; + private readonly Queue _queue; + private readonly IFormatter _formatter; + + public event EventHandler InterpreterStep; + public event Action DispatcherStep; + + public NetDebuggerClient(string connectionUri) : this(new Uri(connectionUri)) + { + } + + public NetDebuggerClient(Uri connectionUri) + { + ConnectionUri = connectionUri ?? DebugRemoting.DEFAULT_TCP_URI; + _socket = new(SocketType.Stream, ProtocolType.Tcp); + _formatter = DebugRemoting.CreateBsonFormatter(); + + System.Threading.Thread connectThread = new(ConnectLoop); + connectThread.Start(); + } + + public void Dispose() => _socket.Dispose(); + + private void QueueDebuggerMessage(DebuggerMessageFrame frame) + { + _queue.Enqueue(frame.ToBytes()); + } + + private void MessageRecieveLoop() + { + while (_socket.Connected) + { + DebuggerMessageFrame frame; + while ((frame = DebugRemoting.ReceiveDebuggerMessage(_socket)) == null) + if (!_socket.Connected) return; + + switch (frame.Type) + { + case DebuggerMessageType.InterpreterOpCode: + { + using MemoryStream stream = new(frame.Data); + var entry = (InterpreterEntry)_formatter.Deserialize(stream); + InterpreterStep?.Invoke(this, entry); + } + break; + + case DebuggerMessageType.DispatcherStep: + string message = frame.GetDataAsString(); + DispatcherStep?.Invoke(message); + break; + + default: + Trace.WriteLine(TraceCategory.MarkupDebug, "Recieved unknown debugger message of type '{0}'.", frame.Type); + break; + } + } + } + + private void ConnectLoop() + { + while (!_socket.Connected) + { + try + { + var endpoint = new IPEndPoint(IPAddress.Parse(ConnectionUri.Host), ConnectionUri.Port); + _socket.Connect(endpoint); + } + catch { } + } + + System.Threading.Thread receiveThread = new(MessageRecieveLoop); + receiveThread.Start(); + } +} \ No newline at end of file diff --git a/UIX/Microsoft/Iris/Debug/SystemNet/NetDebuggerServer.cs b/UIX/Microsoft/Iris/Debug/SystemNet/NetDebuggerServer.cs new file mode 100644 index 0000000..f0899cb --- /dev/null +++ b/UIX/Microsoft/Iris/Debug/SystemNet/NetDebuggerServer.cs @@ -0,0 +1,106 @@ +using Microsoft.Iris.Debug.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.Serialization; +using System.Text; + +namespace Microsoft.Iris.Debug.SystemNet; + +internal class NetDebuggerServer : IDebuggerServer, IDisposable +{ + public static IDebuggerServer Current { get; private set; } + + public Uri ConnectionUri { get; } + + private readonly Queue _outQueue = new(); + private readonly TcpListener _listener; + private readonly IFormatter _formatter; + private Socket _socket; + + public NetDebuggerServer(string connectionUri) : this(new Uri(connectionUri)) + { + } + + public NetDebuggerServer(Uri connectionUri) + { + ConnectionUri = connectionUri ?? DebugRemoting.DEFAULT_TCP_URI; + + _listener = TcpListener.Create(connectionUri.Port); + _listener.Start(); + + _formatter = DebugRemoting.CreateBsonFormatter(); + + System.Threading.Thread connectThread = new(ConnectLoop); + connectThread.Start(); + + Current = this; + } + + public void LogInterpreterOpCode(object context, InterpreterEntry entry) + { + using MemoryStream entryStream = new(); + _formatter.Serialize(entryStream, entry); + + QueueDebuggerMessage(new(0, DebuggerMessageType.InterpreterOpCode, entryStream.ToArray())); + } + + public void LogDispatcher(string message) + { + QueueDebuggerMessage(new(0, DebuggerMessageType.DispatcherStep, message)); + } + + public void Dispose() + { + _listener.Stop(); + _socket?.Dispose(); + } + + private void QueueDebuggerMessage(DebuggerMessageFrame frame) + { + _outQueue.Enqueue(frame.ToBytes()); + } + + private void MessageRecieveLoop() + { + while (_socket.Connected) + { + DebuggerMessageFrame frame; + while ((frame = DebugRemoting.ReceiveDebuggerMessage(_socket)) == null) + ; + + switch (frame.Type) + { + default: + Trace.WriteLine(TraceCategory.MarkupDebug, "Recieved unknown debugger message of type '{0}'.", frame.Type); + break; + } + } + } + + private void MessageSendLoop() + { + while (_socket.Connected) + { + while (_outQueue.Count == 0) ; + + var frameBytes = _outQueue.Dequeue(); + _socket.Send(BitConverter.GetBytes(frameBytes.Length)); + _socket.Send(frameBytes); + } + } + + private void ConnectLoop() + { + while (!_listener.Pending()) ; + + _socket = _listener.AcceptSocket(); + + System.Threading.Thread receiveThread = new(MessageRecieveLoop); + System.Threading.Thread sendThread = new(MessageSendLoop); + receiveThread.Start(); + sendThread.Start(); + } +}