diff --git a/src/BizHawk.Client.Common/Api/ClientWebSocketWrapper.cs b/src/BizHawk.Client.Common/Api/ClientWebSocketWrapper.cs new file mode 100644 index 0000000000..2b34d1c326 --- /dev/null +++ b/src/BizHawk.Client.Common/Api/ClientWebSocketWrapper.cs @@ -0,0 +1,66 @@ +#nullable enable + +using System; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BizHawk.Client.Common +{ + public struct ClientWebSocketWrapper + { + private ClientWebSocket? _w; + + /// calls getter (unless closed/disposed, then is alwars returned) + public WebSocketState State => _w?.State ?? WebSocketState.Closed; + + public ClientWebSocketWrapper(Uri uri, CancellationToken? cancellationToken = null) + { + _w = new ClientWebSocket(); + _w.ConnectAsync(uri, cancellationToken ?? CancellationToken.None).Wait(); + } + + /// calls + /// also calls (wrapper property will continue to work, method calls will throw ) + public Task Close(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken? cancellationToken = null) + { + if (_w == null) throw new ObjectDisposedException(nameof(_w)); + var task = _w.CloseAsync(closeStatus, statusDescription, cancellationToken ?? CancellationToken.None); + _w?.Dispose(); + _w = null; + return task; + } + + /// calls + public Task Receive(ArraySegment buffer, CancellationToken? cancellationToken = null) + => _w?.ReceiveAsync(buffer, cancellationToken ?? CancellationToken.None) + ?? throw new ObjectDisposedException(nameof(_w)); + + /// calls + public string Receive(int bufferCap, CancellationToken? cancellationToken = null) + { + if (_w == null) throw new ObjectDisposedException(nameof(_w)); + var buffer = new byte[bufferCap]; + var result = Receive(new ArraySegment(buffer), cancellationToken ?? CancellationToken.None).Result; + return Encoding.UTF8.GetString(buffer, 0, result.Count); + } + + /// calls + public Task Send(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken? cancellationToken = null) + => _w?.SendAsync(buffer, messageType, endOfMessage, cancellationToken ?? CancellationToken.None) + ?? throw new ObjectDisposedException(nameof(_w)); + + /// calls + public Task Send(string message, bool endOfMessage, CancellationToken? cancellationToken = null) + { + if (_w == null) throw new ObjectDisposedException(nameof(_w)); + return Send( + new ArraySegment(Encoding.UTF8.GetBytes(message)), + WebSocketMessageType.Text, + endOfMessage, + cancellationToken ?? CancellationToken.None + ); + } + } +} diff --git a/src/BizHawk.Client.Common/Api/Interfaces/ICommApi.cs b/src/BizHawk.Client.Common/Api/Interfaces/ICommApi.cs index e5ea358be1..6dab4d8ad7 100644 --- a/src/BizHawk.Client.Common/Api/Interfaces/ICommApi.cs +++ b/src/BizHawk.Client.Common/Api/Interfaces/ICommApi.cs @@ -10,6 +10,8 @@ namespace BizHawk.Client.Common SocketServer? Sockets { get; } + WebSocketServer? WebSockets { get; } + string? HttpTest(); string? HttpTestGet(); diff --git a/src/BizHawk.Client.Common/Api/WebSocketClient.cs b/src/BizHawk.Client.Common/Api/WebSocketClient.cs deleted file mode 100644 index 3bdcd2bbd2..0000000000 --- a/src/BizHawk.Client.Common/Api/WebSocketClient.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net.WebSockets; -using System.Text; -using System.Threading; - -namespace BizHawk.Client.Common -{ - public class WebSocketClient - { - private Dictionary activeSockets; - - public WebSocketClient() - { - activeSockets = new Dictionary(); - } - - public string OpenSocket(string url) - { - var ws = new ClientWebSocket(); - var id = new Guid().ToString(); - - ws.ConnectAsync(new Uri(url), CancellationToken.None).Wait(); - - activeSockets[id] = ws; - - return id; - } - - public void SendToSocket(string id, string content, bool endOfMessage) - { - var ws = activeSockets[id]; - var msg = new ArraySegment(Encoding.UTF8.GetBytes(content)); - - ws.SendAsync(msg, WebSocketMessageType.Text, endOfMessage, CancellationToken.None); - } - - public string ReceiveFromSocket(string id, int maxRead) - { - var ws = activeSockets[id]; - - var rcvBytes = new byte[maxRead]; - var rcvBuffer = new ArraySegment(rcvBytes); - - var result = ws.ReceiveAsync(rcvBuffer, CancellationToken.None).Result; - string rcvMsg = Encoding.UTF8.GetString(rcvBuffer.Take(result.Count).ToArray()); - - return rcvMsg; - } - - public int GetSocketStatus(string id) - { - var ws = activeSockets[id]; - - return (int)ws.State; - } - - public void CloseSocket(string id, int status, string closeMessage) - { - var ws = activeSockets[id]; - - ws.CloseAsync((WebSocketCloseStatus)status, closeMessage, CancellationToken.None); - } - } -} \ No newline at end of file diff --git a/src/BizHawk.Client.Common/Api/WebSocketServer.cs b/src/BizHawk.Client.Common/Api/WebSocketServer.cs new file mode 100644 index 0000000000..da6a34a510 --- /dev/null +++ b/src/BizHawk.Client.Common/Api/WebSocketServer.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System; +using System.Threading; + +namespace BizHawk.Client.Common +{ + public sealed class WebSocketServer + { + public ClientWebSocketWrapper Open(Uri uri, CancellationToken? cancellationToken = null) => new ClientWebSocketWrapper(uri, cancellationToken); + } +} diff --git a/src/BizHawk.Client.Common/lua/CommonLibs/CommLuaLibrary.cs b/src/BizHawk.Client.Common/lua/CommonLibs/CommLuaLibrary.cs index 3561a02301..9cf3bab09f 100644 --- a/src/BizHawk.Client.Common/lua/CommonLibs/CommLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/CommonLibs/CommLuaLibrary.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; +using System.Net.WebSockets; using System.Text; using NLua; @@ -9,6 +12,8 @@ namespace BizHawk.Client.Common [Description("A library for communicating with other programs")] public sealed class CommLuaLibrary : DelegatingLuaLibrary { + private readonly IDictionary _websockets = new Dictionary(); + public CommLuaLibrary(Lua lua) : base(lua) { } @@ -237,41 +242,6 @@ namespace BizHawk.Client.Common return APIs.Comm.HTTP?.GetUrl; } - //[LuaMethodExample("local ws = bizsocket.open('wss://echo.websocket.org');")] - //[LuaMethod("websocket_open", "Opens a websocket and returns the id so that it can be retrieved later.")] - //public string OpenSocket(string url) - //{ - // return APIs.Comm.WebSocketClient?.OpenSocket(url); - //} - - //[LuaMethodExample("local ws = bizsocket.send(ws_id, 'some message', true);")] - //[LuaMethod("websocket_send", "Send a message to a certain websocket id (boolean flag endOfMessage)")] - //public void SendToSocket(string id, string content, bool endOfMessage) - //{ - // APIs.Comm.WebSocketClient?.SendToSocket(id, content, endOfMessage); - //} - - //[LuaMethodExample("local ws = bizsocket.receive(ws_id, max_read);")] - //[LuaMethod("websocket_receive", "Receive a message from a certain websocket id and a maximum number of bytes to read.")] - //public string ReceiveFromSocket(string id, int maxRead) - //{ - // return APIs.Comm.WebSocketClient?.ReceiveFromSocket(id, maxRead); - //} - - //[LuaMethodExample("local ws_status = bizsocket.getstatus(ws_id);")] - //[LuaMethod("websocket_getStatus", "Get a websocket's status")] - //public int? GetSocketStatus(string id) - //{ - // return APIs.Comm.WebSocketClient?.GetSocketStatus(id); - //} - - //[LuaMethodExample("local ws_status = bizsocket.close(ws_id, close_status);")] - //[LuaMethod("websocket_close", "Close a websocket connection with a close status, defined in section 11.7 of the web sockets protocol spec.")] - //public void CloseSocket(string id, int status, string closeMessage) - //{ - // APIs.Comm.WebSocketClient?.CloseSocket(id, status, closeMessage); - //} - private void CheckHttp() { if (APIs.Comm.HTTP == null) @@ -280,12 +250,45 @@ namespace BizHawk.Client.Common } } - //private void CheckWebSocket() - //{ - // if (APIs.Comm.WebSocketClient == null) - // { - // Log("WebSocketClient was not initialized"); - // } - //} + [LuaMethod("ws_open", "Opens a websocket and returns the id so that it can be retrieved later.")] + [LuaMethodExample("local ws_id = comm.ws_open(\"wss://echo.websocket.org\");")] + public string WebSocketOpen(string uri) + { + var wsServer = APIs.Comm.WebSockets; + if (wsServer == null) + { + Log("WebSocket server is somehow not available"); + return null; + } + var guid = new Guid(); + _websockets[guid] = wsServer.Open(new Uri(uri)); + return guid.ToString(); + } + + [LuaMethod("ws_send", "Send a message to a certain websocket id (boolean flag endOfMessage)")] + [LuaMethodExample("local ws = comm.ws_send(ws_id, \"some message\", true);")] + public void WebSocketSend(string guid, string content, bool endOfMessage) + { + if (_websockets.TryGetValue(Guid.Parse(guid), out var wrapper)) wrapper.Send(content, endOfMessage); + } + + [LuaMethod("ws_receive", "Receive a message from a certain websocket id and a maximum number of bytes to read")] + [LuaMethodExample("local ws = comm.ws_receive(ws_id, str_len);")] + public string WebSocketReceive(string guid, int bufferCap) => _websockets.TryGetValue(Guid.Parse(guid), out var wrapper) + ? wrapper.Receive(bufferCap) + : null; + + [LuaMethod("ws_get_status", "Get a websocket's status")] + [LuaMethodExample("local ws_status = comm.ws_get_status(ws_id);")] + public int? WebSocketGetStatus(string guid) => _websockets.TryGetValue(Guid.Parse(guid), out var wrapper) + ? (int) wrapper.State + : (int?) null; + + [LuaMethod("ws_close", "Close a websocket connection with a close status")] + [LuaMethodExample("local ws_status = comm.ws_close(ws_id, close_status);")] + public void WebSocketClose(string guid, WebSocketCloseStatus status, string closeMessage) + { + if (_websockets.TryGetValue(Guid.Parse(guid), out var wrapper)) wrapper.Close(status, closeMessage); + } } } \ No newline at end of file diff --git a/src/BizHawk.Client.Common/lua/LuaHelperLibs/WebSocketLuaLibrary.cs b/src/BizHawk.Client.Common/lua/LuaHelperLibs/WebSocketLuaLibrary.cs deleted file mode 100644 index a0556e5587..0000000000 --- a/src/BizHawk.Client.Common/lua/LuaHelperLibs/WebSocketLuaLibrary.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using NLua; - - -namespace BizHawk.Client.Common.lua.LuaHelperLibs -{ - [Description("A library exposing standard .NET string methods")] - public sealed class WebSocketLuaLibrary : LuaLibraryBase - { - private Dictionary activeSockets; - - public WebSocketLuaLibrary(Lua lua) : base(lua) - { - activeSockets = new Dictionary(); - } - - public WebSocketLuaLibrary(Lua lua, Action logOutputCallback) : base(lua, logOutputCallback) - { - activeSockets = new Dictionary(); - } - - public override string Name => "bizsocket"; - - [LuaMethodExample("local ws = bizsocket.open('wss://echo.websocket.org');")] - [LuaMethod("open", "Opens a websocket and returns the id so that it can be retrieved later.")] - public string OpenSocket(string url) - { - var ws = new ClientWebSocket(); - var id = new Guid().ToString(); - - ws.ConnectAsync(new Uri(url), CancellationToken.None).Wait(); - - activeSockets[id] = ws; - - return id; - } - - [LuaMethodExample("local ws = bizsocket.send(ws_id, 'some message', true);")] - [LuaMethod("send", "Send a message to a certain websocket id (boolean flag endOfMessage)")] - public void SendToSocket(string id, string content, bool endOfMessage) - { - var ws = activeSockets[id]; - var msg = new ArraySegment(Encoding.UTF8.GetBytes(content)); - - ws.SendAsync(msg, WebSocketMessageType.Text, endOfMessage, CancellationToken.None); - } - - [LuaMethodExample("local ws = bizsocket.receive(ws_id, max_read);")] - [LuaMethod("receive", "Receive a message from a certain websocket id and a maximum number of bytes to read.")] - public string ReceiveFromSocket(string id, int maxRead) - { - var ws = activeSockets[id]; - - var rcvBytes = new byte[maxRead]; - var rcvBuffer = new ArraySegment(rcvBytes); - - var result = ws.ReceiveAsync(rcvBuffer, CancellationToken.None).Result; - string rcvMsg = Encoding.UTF8.GetString(rcvBuffer.Take(result.Count).ToArray()); - - return rcvMsg; - } - - [LuaMethodExample("local ws_status = bizsocket.getstatus(ws_id);")] - [LuaMethod("getStatus", "Get a websocket's status")] - public int GetSocketStatus(string id) - { - var ws = activeSockets[id]; - - return (int)ws.State; - } - - [LuaMethodExample("local ws_status = bizsocket.close(ws_id, close_status);")] - [LuaMethod("close", "Close a websocket connection with a close status, defined in section 11.7 of the web sockets protocol spec.")] - public void CloseSocket(string id, int status, string closeMessage) - { - var ws = activeSockets[id]; - - ws.CloseAsync((WebSocketCloseStatus) status, closeMessage, CancellationToken.None); - } - } -} diff --git a/src/BizHawk.Client.EmuHawk/Api/Libraries/CommApi.cs b/src/BizHawk.Client.EmuHawk/Api/Libraries/CommApi.cs index 2135bfa5dd..023261ce95 100644 --- a/src/BizHawk.Client.EmuHawk/Api/Libraries/CommApi.cs +++ b/src/BizHawk.Client.EmuHawk/Api/Libraries/CommApi.cs @@ -8,6 +8,8 @@ namespace BizHawk.Client.EmuHawk { public sealed class CommApi : ICommApi { + private static readonly WebSocketServer _wsServer = new WebSocketServer(); + private readonly (HttpCommunication HTTP, MemoryMappedFiles MMF, SocketServer Sockets) _networkingHelpers; public HttpCommunication? HTTP => _networkingHelpers.HTTP; @@ -16,6 +18,8 @@ namespace BizHawk.Client.EmuHawk public SocketServer? Sockets => _networkingHelpers.Sockets; + public WebSocketServer? WebSockets => _wsServer; + public CommApi(Action logCallback, DisplayManager displayManager, InputManager inputManager, IMainFormForApi mainForm) { _networkingHelpers = mainForm.NetworkingHelpers;