diff --git a/src/libraries/Common/src/System/Net/WebSockets/WebSocketValidate.cs b/src/libraries/Common/src/System/Net/WebSockets/WebSocketValidate.cs index 7c97c082d26f8..e087677be4608 100644 --- a/src/libraries/Common/src/System/Net/WebSockets/WebSocketValidate.cs +++ b/src/libraries/Common/src/System/Net/WebSockets/WebSocketValidate.cs @@ -23,10 +23,15 @@ internal static partial class WebSocketValidate internal const int MaxDeflateWindowBits = 15; internal const int MaxControlFramePayloadLength = 123; +#if TARGET_BROWSER + private const int ValidCloseStatusCodesFrom = 3000; + private const int ValidCloseStatusCodesTo = 4999; +#else private const int CloseStatusCodeAbort = 1006; private const int CloseStatusCodeFailedTLSHandshake = 1015; private const int InvalidCloseStatusCodesFrom = 0; private const int InvalidCloseStatusCodesTo = 999; +#endif // [0x21, 0x7E] except separators "()<>@,;:\\\"/[]?={} ". private static readonly SearchValues s_validSubprotocolChars = @@ -84,11 +89,15 @@ internal static void ValidateCloseStatus(WebSocketCloseStatus closeStatus, strin } int closeStatusCode = (int)closeStatus; - +#if TARGET_BROWSER + // as defined in https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code + if (closeStatus != WebSocketCloseStatus.NormalClosure && (closeStatusCode < ValidCloseStatusCodesFrom || closeStatusCode > ValidCloseStatusCodesTo)) +#else if ((closeStatusCode >= InvalidCloseStatusCodesFrom && closeStatusCode <= InvalidCloseStatusCodesTo) || closeStatusCode == CloseStatusCodeAbort || closeStatusCode == CloseStatusCodeFailedTLSHandshake) +#endif { // CloseStatus 1006 means Aborted - this will never appear on the wire and is reflected by calling WebSocket.Abort throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCloseStatusCode, diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs index f4e5562600015..8304f2d115607 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs @@ -24,11 +24,11 @@ public static async Task InvokeAsync(HttpContext context) if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay10sec")) { - Thread.Sleep(10000); + await Task.Delay(10000); } else if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay20sec")) { - Thread.Sleep(20000); + await Task.Delay(20000); } try @@ -124,14 +124,15 @@ await socket.CloseAsync( } bool sendMessage = false; + string receivedMessage = null; if (receiveResult.MessageType == WebSocketMessageType.Text) { - string receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); + receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); if (receivedMessage == ".close") { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); } - if (receivedMessage == ".shutdown") + else if (receivedMessage == ".shutdown") { await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); } @@ -161,6 +162,14 @@ await socket.SendAsync( !replyWithPartialMessages, CancellationToken.None); } + if (receivedMessage == ".closeafter") + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + } + else if (receivedMessage == ".shutdownafter") + { + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + } } } } diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs index 121fa8595c65d..879d45ca0da57 100644 --- a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs +++ b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs @@ -24,6 +24,8 @@ internal sealed class BrowserWebSocket : WebSocket private WebSocketState _state; private bool _disposed; private bool _aborted; + private bool _closeReceived; + private bool _closeSent; private int[] responseStatus = new int[3]; private MemoryHandle? responseStatusHandle; @@ -37,7 +39,7 @@ public override WebSocketState State lock (_thisLock) { #endif - if (_innerWebSocket == null || _disposed || (_state != WebSocketState.Connecting && _state != WebSocketState.Open && _state != WebSocketState.CloseSent)) + if (_innerWebSocket == null || _disposed || _state == WebSocketState.Aborted || _state == WebSocketState.Closed) { return _state; } @@ -46,15 +48,9 @@ public override WebSocketState State #endif #if FEATURE_WASM_THREADS - return FastState = _innerWebSocket!.SynchronizationContext.Send(static (BrowserWebSocket self) => - { - lock (self._thisLock) - { - return GetReadyState(self._innerWebSocket!); - } //lock - }, this); + return _innerWebSocket!.SynchronizationContext.Send(GetReadyState, this); #else - return FastState = GetReadyState(_innerWebSocket!); + return GetReadyState(this); #endif } } @@ -148,7 +144,7 @@ public override Task SendAsync(ArraySegment buffer, WebSocketMessageType m ThrowIfDisposed(); // fast check of previous _state instead of GetReadyState(), the readyState would be validated on JS side - if (FastState != WebSocketState.Open) + if (FastState != WebSocketState.Open && FastState != WebSocketState.CloseReceived) { throw new InvalidOperationException(SR.net_WebSockets_NotConnected); } @@ -240,7 +236,7 @@ public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? { throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, state, "Connecting, Open, CloseSent, Aborted")); } - if(state != WebSocketState.Open && state != WebSocketState.Connecting && state != WebSocketState.Aborted) + if (state == WebSocketState.CloseSent) { return Task.CompletedTask; } @@ -280,10 +276,6 @@ public override Task CloseAsync(WebSocketCloseStatus closeStatus, string? status { throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, state, "Connecting, Open, CloseSent, Aborted")); } - if (state != WebSocketState.Open && state != WebSocketState.Connecting && state != WebSocketState.Aborted && state != WebSocketState.CloseSent) - { - return Task.CompletedTask; - } #if FEATURE_WASM_THREADS promise = CloseAsyncCore(closeStatus, statusDescription, state != WebSocketState.Aborted, cancellationToken); @@ -387,12 +379,13 @@ private void CreateCore(Uri uri, List? requestedSubProtocols) string[]? subProtocols = requestedSubProtocols?.ToArray(); var onClose = (int code, string reason) => { - _closeStatus = (WebSocketCloseStatus)code; - _closeStatusDescription = reason; #if FEATURE_WASM_THREADS lock (_thisLock) { #endif + _closeStatus = (WebSocketCloseStatus)code; + _closeStatusDescription = reason; + _closeReceived = true; WebSocketState state = State; if (state == WebSocketState.Connecting || state == WebSocketState.Open || state == WebSocketState.CloseSent) { @@ -545,6 +538,8 @@ private static WebSocketReceiveResult ConvertResponse(BrowserWebSocket self) WebSocketMessageType messageType = (WebSocketMessageType)self.responseStatus[typeIndex]; if (messageType == WebSocketMessageType.Close) { + self._closeReceived = true; + self.FastState = self._closeSent ? WebSocketState.Closed : WebSocketState.CloseReceived; return new WebSocketReceiveResult(self.responseStatus[countIndex], messageType, self.responseStatus[endIndex] != 0, self.CloseStatus, self.CloseStatusDescription); } return new WebSocketReceiveResult(self.responseStatus[countIndex], messageType, self.responseStatus[endIndex] != 0); @@ -552,8 +547,12 @@ private static WebSocketReceiveResult ConvertResponse(BrowserWebSocket self) private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, bool waitForCloseReceived, CancellationToken cancellationToken) { - _closeStatus = closeStatus; - _closeStatusDescription = statusDescription; + if (!_closeReceived) + { + _closeStatus = closeStatus; + _closeStatusDescription = statusDescription; + } + _closeSent = true; var closeTask = BrowserInterop.WebSocketClose(_innerWebSocket!, (int)closeStatus, statusDescription, waitForCloseReceived) ?? Task.CompletedTask; await CancelationHelper(closeTask, cancellationToken, FastState).ConfigureAwait(true); @@ -562,6 +561,10 @@ private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? stat lock (_thisLock) { #endif + if (waitForCloseReceived) + { + _closeReceived = true; + } var state = State; if (state == WebSocketState.Open || state == WebSocketState.Connecting || state == WebSocketState.CloseSent) { @@ -614,18 +617,42 @@ private async Task CancelationHelper(Task jsTask, CancellationToken cancellation } } - private static WebSocketState GetReadyState(JSObject innerWebSocket) + private static WebSocketState GetReadyState(BrowserWebSocket self) { - var readyState = BrowserInterop.GetReadyState(innerWebSocket); - // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState - return readyState switch - { - 0 => WebSocketState.Connecting, // 0 (CONNECTING) - 1 => WebSocketState.Open, // 1 (OPEN) - 2 => WebSocketState.CloseSent, // 2 (CLOSING) - 3 => WebSocketState.Closed, // 3 (CLOSED) - _ => WebSocketState.None - }; +#if FEATURE_WASM_THREADS + lock (self._thisLock) + { +#endif + var readyState = BrowserInterop.GetReadyState(self._innerWebSocket); + // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState + var st = readyState switch + { + 0 => WebSocketState.Connecting, // 0 (CONNECTING) + 1 => WebSocketState.Open, // 1 (OPEN) + 2 => WebSocketState.CloseSent, // 2 (CLOSING) + 3 => WebSocketState.Closed, // 3 (CLOSED) + _ => WebSocketState.None + }; + if (st == WebSocketState.Closed || st == WebSocketState.CloseSent) + { + if (self._closeReceived && self._closeSent) + { + st = WebSocketState.Closed; + } + else if (self._closeReceived && !self._closeSent) + { + st = WebSocketState.CloseReceived; + } + else if (!self._closeReceived && self._closeSent) + { + st = WebSocketState.CloseSent; + } + } + self.FastState = st; + return st; +#if FEATURE_WASM_THREADS + } //lock +#endif } #endregion diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 9e33810098f7d..6f1e6faa49019 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -33,12 +33,12 @@ public class CloseTest : ClientWebSocketTestBase public CloseTest(ITestOutputHelper output) : base(output) { } - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServersAndBoolean))] public async Task CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) { - const string closeWebSocketMetaCommand = ".close"; + const string shutdownWebSocketMetaCommand = ".shutdown"; using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) { @@ -46,7 +46,7 @@ public async Task CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCl _output.WriteLine("SendAsync starting."); await cws.SendAsync( - WebSocketData.GetBufferFromText(closeWebSocketMetaCommand), + WebSocketData.GetBufferFromText(shutdownWebSocketMetaCommand), WebSocketMessageType.Text, true, cts.Token); @@ -59,26 +59,27 @@ await cws.SendAsync( // Verify received server-initiated close message. Assert.Equal(WebSocketCloseStatus.NormalClosure, recvResult.CloseStatus); - Assert.Equal(closeWebSocketMetaCommand, recvResult.CloseStatusDescription); + Assert.Equal(shutdownWebSocketMetaCommand, recvResult.CloseStatusDescription); Assert.Equal(WebSocketMessageType.Close, recvResult.MessageType); // Verify current websocket state as CloseReceived which indicates only partial close. Assert.Equal(WebSocketState.CloseReceived, cws.State); Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); - Assert.Equal(closeWebSocketMetaCommand, cws.CloseStatusDescription); + Assert.Equal(shutdownWebSocketMetaCommand, cws.CloseStatusDescription); // Send back close message to acknowledge server-initiated close. _output.WriteLine("Close starting."); + var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidMessageType : (WebSocketCloseStatus)3210; await (useCloseOutputAsync ? - cws.CloseOutputAsync(WebSocketCloseStatus.InvalidMessageType, string.Empty, cts.Token) : - cws.CloseAsync(WebSocketCloseStatus.InvalidMessageType, string.Empty, cts.Token)); + cws.CloseOutputAsync(closeStatus, string.Empty, cts.Token) : + cws.CloseAsync(closeStatus, string.Empty, cts.Token)); _output.WriteLine("Close done."); Assert.Equal(WebSocketState.Closed, cws.State); // Verify that there is no follow-up echo close message back from the server by // making sure the close code and message are the same as from the first server close message. Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); - Assert.Equal(closeWebSocketMetaCommand, cws.CloseStatusDescription); + Assert.Equal(shutdownWebSocketMetaCommand, cws.CloseStatusDescription); } } @@ -233,8 +234,7 @@ public async Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri serve { var cts = new CancellationTokenSource(TimeOutMilliseconds); - // See issue for Browser websocket differences https://github.com/dotnet/runtime/issues/45538 - var closeStatus = PlatformDetection.IsBrowser ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.InvalidPayloadData; + var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; string closeDescription = "CloseOutputAsync_Client_InvalidPayloadData"; await cws.SendAsync(WebSocketData.GetBufferFromText(message), WebSocketMessageType.Text, true, cts.Token); @@ -262,7 +262,64 @@ public async Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri serve } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] + public async Task CloseOutputAsync_ServerInitiated_CanReceive(Uri server) + { + string message = "Hello WebSockets!"; + var expectedCloseStatus = WebSocketCloseStatus.NormalClosure; + var expectedCloseDescription = ".shutdownafter"; + + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + { + var cts = new CancellationTokenSource(TimeOutMilliseconds); + + await cws.SendAsync( + WebSocketData.GetBufferFromText(expectedCloseDescription), + WebSocketMessageType.Text, + true, + cts.Token); + + // Should be able to receive the message echoed by the server. + var recvBuffer = new byte[100]; + var segmentRecv = new ArraySegment(recvBuffer); + WebSocketReceiveResult recvResult = await cws.ReceiveAsync(segmentRecv, cts.Token); + Assert.Equal(expectedCloseDescription.Length, recvResult.Count); + segmentRecv = new ArraySegment(segmentRecv.Array, 0, recvResult.Count); + Assert.Equal(expectedCloseDescription, WebSocketData.GetTextFromBuffer(segmentRecv)); + Assert.Null(recvResult.CloseStatus); + Assert.Null(recvResult.CloseStatusDescription); + + // Should be able to receive a shutdown message. + segmentRecv = new ArraySegment(recvBuffer); + recvResult = await cws.ReceiveAsync(segmentRecv, cts.Token); + Assert.Equal(0, recvResult.Count); + Assert.Equal(expectedCloseStatus, recvResult.CloseStatus); + Assert.Equal(expectedCloseDescription, recvResult.CloseStatusDescription); + + // Verify WebSocket state + Assert.Equal(expectedCloseStatus, cws.CloseStatus); + Assert.Equal(expectedCloseDescription, cws.CloseStatusDescription); + + Assert.Equal(WebSocketState.CloseReceived, cws.State); + + // Should be able to send. + await cws.SendAsync(WebSocketData.GetBufferFromText(message), WebSocketMessageType.Text, true, cts.Token); + + // Cannot change the close status/description with the final close. + var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; + var closeDescription = "CloseOutputAsync_Client_Description"; + + await cws.CloseAsync(closeStatus, closeDescription, cts.Token); + + Assert.Equal(expectedCloseStatus, cws.CloseStatus); + Assert.Equal(expectedCloseDescription, cws.CloseStatusDescription); + Assert.Equal(WebSocketState.Closed, cws.State); + } + } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] public async Task CloseOutputAsync_ServerInitiated_CanSend(Uri server) @@ -299,7 +356,7 @@ await cws.SendAsync( await cws.SendAsync(WebSocketData.GetBufferFromText(message), WebSocketMessageType.Text, true, cts.Token); // Cannot change the close status/description with the final close. - var closeStatus = WebSocketCloseStatus.InvalidPayloadData; + var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; var closeDescription = "CloseOutputAsync_Client_Description"; await cws.CloseAsync(closeStatus, closeDescription, cts.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 18694fcd97a1a..f45ed7fd9c14c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -249,7 +249,7 @@ public async Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] // This will also pass when no exception is thrown. Current implementation doesn't throw. - [ActiveIssue("https://github.com/dotnet/runtime/issues/83517", typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/83517", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] public async Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) { using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) diff --git a/src/mono/wasm/runtime/web-socket.ts b/src/mono/wasm/runtime/web-socket.ts index 7469d27cd7501..5cf400aca536a 100644 --- a/src/mono/wasm/runtime/web-socket.ts +++ b/src/mono/wasm/runtime/web-socket.ts @@ -5,7 +5,7 @@ import MonoWasmThreads from "consts:monoWasmThreads"; import { prevent_timer_throttling } from "./scheduling"; import { Queue } from "./queue"; -import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, createPromiseController, mono_assert } from "./globals"; +import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, createPromiseController, loaderHelpers, mono_assert } from "./globals"; import { setI32, localHeapViewU8 } from "./memory"; import { VoidPtr } from "./types/emscripten"; import { PromiseController } from "./types/internal"; @@ -19,12 +19,15 @@ const wasm_ws_pending_send_buffer_type = Symbol.for("wasm ws_pending_send_buffer const wasm_ws_pending_receive_event_queue = Symbol.for("wasm ws_pending_receive_event_queue"); const wasm_ws_pending_receive_promise_queue = Symbol.for("wasm ws_pending_receive_promise_queue"); const wasm_ws_pending_open_promise = Symbol.for("wasm ws_pending_open_promise"); +const wasm_ws_pending_open_promise_used = Symbol.for("wasm wasm_ws_pending_open_promise_used"); const wasm_ws_pending_close_promises = Symbol.for("wasm ws_pending_close_promises"); const wasm_ws_pending_send_promises = Symbol.for("wasm ws_pending_send_promises"); const wasm_ws_is_aborted = Symbol.for("wasm ws_is_aborted"); const wasm_ws_on_closed = Symbol.for("wasm ws_on_closed"); +const wasm_ws_close_sent = Symbol.for("wasm wasm_ws_close_sent"); +const wasm_ws_close_received = Symbol.for("wasm wasm_ws_close_received"); const wasm_ws_receive_status_ptr = Symbol.for("wasm ws_receive_status_ptr"); -let mono_wasm_web_socket_close_warning = false; + const ws_send_buffer_blocking_threshold = 65536; const emptyBuffer = new Uint8Array(); @@ -58,18 +61,22 @@ export function ws_wasm_create(uri: string, sub_protocols: string[] | null, rece ws.binaryType = "arraybuffer"; const local_on_open = () => { if (ws[wasm_ws_is_aborted]) return; + if (loaderHelpers.is_exited()) return; open_promise_control.resolve(ws); prevent_timer_throttling(); }; const local_on_message = (ev: MessageEvent) => { if (ws[wasm_ws_is_aborted]) return; + if (loaderHelpers.is_exited()) return; _mono_wasm_web_socket_on_message(ws, ev); prevent_timer_throttling(); }; const local_on_close = (ev: CloseEvent) => { ws.removeEventListener("message", local_on_message); if (ws[wasm_ws_is_aborted]) return; + if (loaderHelpers.is_exited()) return; + ws[wasm_ws_close_received] = true; onClosed(ev.code, ev.reason); // this reject would not do anything if there was already "open" before it. @@ -93,6 +100,7 @@ export function ws_wasm_create(uri: string, sub_protocols: string[] | null, rece }; const local_on_error = (ev: any) => { if (ws[wasm_ws_is_aborted]) return; + if (loaderHelpers.is_exited()) return; ws.removeEventListener("message", local_on_message); const error = new Error(ev.message || "WebSocket error"); mono_log_warn("WebSocket error", error); @@ -116,12 +124,23 @@ export function ws_wasm_create(uri: string, sub_protocols: string[] | null, rece export function ws_wasm_open(ws: WebSocketExtension): Promise | null { mono_assert(!!ws, "ERR17: expected ws instance"); const open_promise_control = ws[wasm_ws_pending_open_promise]; + ws[wasm_ws_pending_open_promise_used] = true; return open_promise_control.promise; } export function ws_wasm_send(ws: WebSocketExtension, buffer_ptr: VoidPtr, buffer_length: number, message_type: number, end_of_message: boolean): Promise | null { mono_assert(!!ws, "ERR17: expected ws instance"); + if (ws[wasm_ws_is_aborted] || ws[wasm_ws_close_sent]) { + return Promise.reject(new Error("InvalidState: The WebSocket is not connected.")); + } + + if (ws.readyState === WebSocket.CLOSED) { + // this is server initiated close but not partial close + // because CloseOutputAsync_ServerInitiated_CanSend expectations, we don't fail here + return null; + } + const buffer_view = new Uint8Array(localHeapViewU8().buffer, buffer_ptr, buffer_length); const whole_buffer = _mono_wasm_web_socket_send_buffering(ws, buffer_view, message_type, end_of_message); @@ -135,14 +154,18 @@ export function ws_wasm_send(ws: WebSocketExtension, buffer_ptr: VoidPtr, buffer export function ws_wasm_receive(ws: WebSocketExtension, buffer_ptr: VoidPtr, buffer_length: number): Promise | null { mono_assert(!!ws, "ERR18: expected ws instance"); + // we can't quickly return if wasm_ws_close_received==true, because there could be pending messages + if (ws[wasm_ws_is_aborted]) { + const receive_status_ptr = ws[wasm_ws_receive_status_ptr]; + setI32(receive_status_ptr, 0); // count + setI32(receive_status_ptr + 4, 2); // type:close + setI32(receive_status_ptr + 8, 1);// end_of_message: true + return null; + } + const receive_event_queue = ws[wasm_ws_pending_receive_event_queue]; const receive_promise_queue = ws[wasm_ws_pending_receive_promise_queue]; - const readyState = ws.readyState; - if (readyState != WebSocket.OPEN && readyState != WebSocket.CLOSING) { - throw new Error(`InvalidState: ${readyState} The WebSocket is not connected.`); - } - if (receive_event_queue.getLength()) { mono_assert(receive_promise_queue.getLength() == 0, "ERR20: Invalid WS state"); @@ -151,6 +174,16 @@ export function ws_wasm_receive(ws: WebSocketExtension, buffer_ptr: VoidPtr, buf return null; } + + const readyState = ws.readyState; + if (readyState == WebSocket.CLOSED) { + const receive_status_ptr = ws[wasm_ws_receive_status_ptr]; + setI32(receive_status_ptr, 0); // count + setI32(receive_status_ptr + 4, 2); // type:close + setI32(receive_status_ptr + 8, 1);// end_of_message: true + return null; + } + const { promise, promise_control } = createPromiseController(); const receive_promise_control = promise_control as ReceivePromiseControl; receive_promise_control.buffer_ptr = buffer_ptr; @@ -163,10 +196,11 @@ export function ws_wasm_receive(ws: WebSocketExtension, buffer_ptr: VoidPtr, buf export function ws_wasm_close(ws: WebSocketExtension, code: number, reason: string | null, wait_for_close_received: boolean): Promise | null { mono_assert(!!ws, "ERR19: expected ws instance"); - if (ws.readyState == WebSocket.CLOSED) { + if (ws[wasm_ws_is_aborted] || ws[wasm_ws_close_sent] || ws.readyState == WebSocket.CLOSED) { return null; } + ws[wasm_ws_close_sent] = true; if (wait_for_close_received) { const { promise, promise_control } = createPromiseController(); ws[wasm_ws_pending_close_promises].push(promise_control); @@ -179,10 +213,6 @@ export function ws_wasm_close(ws: WebSocketExtension, code: number, reason: stri return promise; } else { - if (!mono_wasm_web_socket_close_warning) { - mono_wasm_web_socket_close_warning = true; - mono_log_warn("WARNING: Web browsers do not support closing the output side of a WebSocket. CloseOutputAsync has closed the socket and discarded any incoming messages."); - } if (typeof reason === "string") { ws.close(code, reason); } else { @@ -195,6 +225,10 @@ export function ws_wasm_close(ws: WebSocketExtension, code: number, reason: stri export function ws_wasm_abort(ws: WebSocketExtension): void { mono_assert(!!ws, "ERR18: expected ws instance"); + if (ws[wasm_ws_is_aborted] || ws[wasm_ws_close_sent]) { + return; + } + ws[wasm_ws_is_aborted] = true; reject_promises(ws, new Error("OperationCanceledException")); @@ -211,7 +245,12 @@ export function ws_wasm_abort(ws: WebSocketExtension): void { function reject_promises(ws: WebSocketExtension, error: Error) { const open_promise_control = ws[wasm_ws_pending_open_promise]; - if (open_promise_control) { + const open_promise_used = ws[wasm_ws_pending_open_promise_used]; + + // when `open_promise_used` is false, we should not reject it, + // because it would be unhandled rejection. Nobody is subscribed yet. + // The subscription comes on the next call, which is `ws_wasm_open`, but cancelation/abort could happen in the meantime. + if (open_promise_control && open_promise_used) { open_promise_control.reject(error); } for (const close_promise_control of ws[wasm_ws_pending_close_promises]) { @@ -399,10 +438,13 @@ type WebSocketExtension = WebSocket & { [wasm_ws_pending_receive_event_queue]: Queue; [wasm_ws_pending_receive_promise_queue]: Queue; [wasm_ws_pending_open_promise]: PromiseController + [wasm_ws_pending_open_promise_used]: boolean [wasm_ws_pending_send_promises]: PromiseController[] [wasm_ws_pending_close_promises]: PromiseController[] [wasm_ws_is_aborted]: boolean [wasm_ws_on_closed]: IDisposable + [wasm_ws_close_received]: boolean + [wasm_ws_close_sent]: boolean [wasm_ws_receive_status_ptr]: VoidPtr [wasm_ws_pending_send_buffer_offset]: number [wasm_ws_pending_send_buffer_type]: number