From 737189dbaf9bc6147d6468a9bd9f12f428a52478 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 16 Jan 2024 15:15:32 +0100 Subject: [PATCH 01/17] rebase --- .../NetCoreServer/Handlers/EchoHandler.cs | 20 +- .../BrowserHttpHandler/BrowserHttpHandler.cs | 523 +++++++----------- .../BrowserHttpHandler/BrowserHttpInterop.cs | 108 ++-- .../System.Net.Http.Functional.Tests.csproj | 2 - .../src/CompatibilitySuppressions.xml | 12 - ....Runtime.InteropServices.JavaScript.csproj | 1 - .../InteropServices/JavaScript/JSHost.cs | 12 - .../SynchronizationContextExtensions.cs | 152 ----- .../JavaScript/WebWorkerTest.cs | 93 +++- src/mono/browser/runtime/exports-internal.ts | 8 +- src/mono/browser/runtime/http.ts | 189 ++++--- 11 files changed, 489 insertions(+), 631 deletions(-) delete mode 100644 src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs index fd05cff102d2e6..6888c57e11285b 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoHandler.cs @@ -4,6 +4,7 @@ using System; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -27,18 +28,33 @@ public static async Task InvokeAsync(HttpContext context) RequestInformation info = await RequestInformation.CreateAsync(context.Request); string echoJson = info.SerializeToJson(); + byte[] bytes = Encoding.UTF8.GetBytes(echoJson); + // Compute MD5 hash so that clients can verify the received data. using (MD5 md5 = MD5.Create()) { - byte[] bytes = Encoding.UTF8.GetBytes(echoJson); byte[] hash = md5.ComputeHash(bytes); string encodedHash = Convert.ToBase64String(hash); context.Response.Headers["Content-MD5"] = encodedHash; context.Response.ContentType = "application/json"; context.Response.ContentLength = bytes.Length; - await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); } + + if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay10sec")) + { + await context.Response.StartAsync(CancellationToken.None); + await context.Response.Body.FlushAsync(); + + await Task.Delay(10000); + } + else if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay1sec")) + { + await context.Response.StartAsync(CancellationToken.None); + await Task.Delay(1000); + } + + await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs index 218a51b441fe8a..7a7728ab7ea57a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -15,9 +16,6 @@ namespace System.Net.Http // the JavaScript objects have thread affinity, it is necessary that the continuations run the same thread as the start of the async method. internal sealed class BrowserHttpHandler : HttpMessageHandler { - private static readonly HttpRequestOptionsKey EnableStreamingRequest = new HttpRequestOptionsKey("WebAssemblyEnableStreamingRequest"); - private static readonly HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); - private static readonly HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); private bool _allowAutoRedirect = HttpHandlerDefaults.DefaultAutomaticRedirection; // flag to determine if the _allowAutoRedirect was explicitly set or not. private bool _isAllowAutoRedirectTouched; @@ -127,58 +125,107 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, throw new PlatformNotSupportedException(); } - private static async Task CallFetch(HttpRequestMessage request, CancellationToken cancellationToken, bool? allowAutoRedirect) + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - int headerCount = request.Headers.Count + request.Content?.Headers.Count ?? 0; - List headerNames = new List(headerCount); - List headerValues = new List(headerCount); - JSObject abortController = BrowserHttpInterop.CreateAbortController(); - CancellationTokenRegistration abortRegistration = cancellationToken.Register(static s => + bool? allowAutoRedirect = _isAllowAutoRedirectTouched ? AllowAutoRedirect : null; + var controller = new BrowserHttpController(request, allowAutoRedirect, cancellationToken); + return controller.CallFetch(); + } + } + + internal sealed class BrowserHttpController : IDisposable + { + private static readonly HttpRequestOptionsKey EnableStreamingRequest = new HttpRequestOptionsKey("WebAssemblyEnableStreamingRequest"); + private static readonly HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); + private static readonly HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); + + + internal JSObject _jsController; + private readonly CancellationTokenRegistration _abortRegistration; + private bool _isDisposed; + + private string[] _optionNames; + private object?[] _optionValues; + + private string[] _headerNames; + private string[] _headerValues; + private string uri; + private CancellationToken _cancellationToken; + private HttpRequestMessage _request; + + public BrowserHttpController(HttpRequestMessage request, bool? allowAutoRedirect, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (request.RequestUri == null) { - JSObject _abortController = (JSObject)s!; -#if FEATURE_WASM_THREADS - if (!_abortController.IsDisposed) - { - _abortController.SynchronizationContext.Send(static (JSObject __abortController) => - { - BrowserHttpInterop.AbortRequest(__abortController); - __abortController.Dispose(); - }, _abortController); - } -#else - if (!_abortController.IsDisposed) - { - BrowserHttpInterop.AbortRequest(_abortController); - _abortController.Dispose(); - } -#endif - }, abortController); - try + throw new ArgumentNullException(nameof(request.RequestUri)); + } + + _cancellationToken = cancellationToken; + _request = request; + + JSObject httpController = BrowserHttpInterop.CreateController(); + CancellationTokenRegistration abortRegistration = cancellationToken.Register(static s => { - if (request.RequestUri == null) + JSObject _httpController = (JSObject)s!; + + if (!_httpController.IsDisposed) { - throw new ArgumentNullException(nameof(request.RequestUri)); + BrowserHttpInterop.AbortRequest(_httpController); } + }, httpController); + + + _jsController = httpController; + _abortRegistration = abortRegistration; + + uri = request.RequestUri.IsAbsoluteUri ? request.RequestUri.AbsoluteUri : request.RequestUri.ToString(); + + bool hasFetchOptions = request.Options.TryGetValue(FetchOptions, out IDictionary? fetchOptions); + int optionCount = 1 + (allowAutoRedirect.HasValue ? 1 : 0) + (hasFetchOptions && fetchOptions != null ? fetchOptions.Count : 0); + int optionIndex = 0; - string uri = request.RequestUri.IsAbsoluteUri ? request.RequestUri.AbsoluteUri : request.RequestUri.ToString(); + // note there could be more values for each header name and so this is just name count + int headerCount = request.Headers.Count + (request.Content?.Headers.Count ?? 0); - bool hasFetchOptions = request.Options.TryGetValue(FetchOptions, out IDictionary? fetchOptions); - int optionCount = 1 + (allowAutoRedirect.HasValue ? 1 : 0) + (hasFetchOptions && fetchOptions != null ? fetchOptions.Count : 0); - int optionIndex = 0; - string[] optionNames = new string[optionCount]; - object?[] optionValues = new object?[optionCount]; + _optionNames = new string[optionCount]; + _optionValues = new object?[optionCount]; - optionNames[optionIndex] = "method"; - optionValues[optionIndex] = request.Method.Method; + _optionNames[optionIndex] = "method"; + _optionValues[optionIndex] = request.Method.Method; + optionIndex++; + if (allowAutoRedirect.HasValue) + { + _optionNames[optionIndex] = "redirect"; + _optionValues[optionIndex] = allowAutoRedirect.Value ? "follow" : "manual"; optionIndex++; - if (allowAutoRedirect.HasValue) + } + + if (hasFetchOptions && fetchOptions != null) + { + foreach (KeyValuePair item in fetchOptions) { - optionNames[optionIndex] = "redirect"; - optionValues[optionIndex] = allowAutoRedirect.Value ? "follow" : "manual"; + _optionNames[optionIndex] = item.Key; + _optionValues[optionIndex] = item.Value; optionIndex++; } + } - foreach (KeyValuePair> header in request.Headers) + var headerNames = new List(headerCount); + var headerValues = new List(headerCount); + + foreach (KeyValuePair> header in request.Headers) + { + foreach (string value in header.Value) + { + headerNames.Add(header.Key); + headerValues.Add(value); + } + } + + if (request.Content != null) + { + foreach (KeyValuePair> header in request.Content.Headers) { foreach (string value in header.Value) { @@ -186,117 +233,82 @@ private static async Task CallFetch(HttpRequestMessage reques headerValues.Add(value); } } + } + _headerNames = headerNames.ToArray(); + _headerValues = headerValues.ToArray(); + } - if (request.Content != null) - { - foreach (KeyValuePair> header in request.Content.Headers) - { - foreach (string value in header.Value) - { - headerNames.Add(header.Key); - headerValues.Add(value); - } - } - } + public async Task CallFetch() + { + _cancellationToken.ThrowIfCancellationRequested(); - if (hasFetchOptions && fetchOptions != null) - { - foreach (KeyValuePair item in fetchOptions) - { - optionNames[optionIndex] = item.Key; - optionValues[optionIndex] = item.Value; - optionIndex++; - } - } + BrowserHttpWriteStream? writeStream = null; + Task fetchPromise; + MemoryHandle? pinBuffer = null; + bool streamingRequestEnabled = false; - JSObject? fetchResponse; - cancellationToken.ThrowIfCancellationRequested(); - if (request.Content != null) + try + { + if (_request.Content != null) { - bool streamingEnabled = false; if (BrowserHttpInterop.SupportsStreamingRequest()) { - request.Options.TryGetValue(EnableStreamingRequest, out streamingEnabled); + _request.Options.TryGetValue(EnableStreamingRequest, out streamingRequestEnabled); } - if (streamingEnabled) + if (streamingRequestEnabled) { - using (JSObject transformStream = BrowserHttpInterop.CreateTransformStream()) - { - Task fetchPromise = BrowserHttpInterop.Fetch(uri, headerNames.ToArray(), headerValues.ToArray(), optionNames, optionValues, abortController, transformStream); - Task fetchTask = BrowserHttpInterop.CancelationHelper(fetchPromise, cancellationToken).AsTask(); // initialize fetch cancellation - - using (WasmHttpWriteStream stream = new WasmHttpWriteStream(transformStream)) - { - try - { - await request.Content.CopyToAsync(stream, cancellationToken).ConfigureAwait(true); - Task closePromise = BrowserHttpInterop.TransformStreamClose(transformStream); - await BrowserHttpInterop.CancelationHelper(closePromise, cancellationToken).ConfigureAwait(true); - } - catch (Exception) - { - BrowserHttpInterop.TransformStreamAbort(transformStream); - if (!abortController.IsDisposed) - { - BrowserHttpInterop.AbortRequest(abortController); - } - try - { - using (fetchResponse = await fetchTask.ConfigureAwait(true)) // observe exception - { - BrowserHttpInterop.AbortResponse(fetchResponse); - } - } - catch { /* ignore */ } - cancellationToken.ThrowIfCancellationRequested(); - throw; - } - } - - fetchResponse = await fetchTask.ConfigureAwait(true); - } + fetchPromise = BrowserHttpInterop.FetchStream(_jsController, uri, _headerNames, _headerValues, _optionNames, _optionValues); + writeStream = new BrowserHttpWriteStream(this); + await _request.Content.CopyToAsync(writeStream, _cancellationToken).ConfigureAwait(false); + var closePromise = BrowserHttpInterop.TransformStreamClose(_jsController); + await BrowserHttpInterop.CancellationHelper(closePromise, _cancellationToken, _jsController).ConfigureAwait(false); } else { - byte[] buffer = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(true); - cancellationToken.ThrowIfCancellationRequested(); + byte[] buffer = await _request.Content.ReadAsByteArrayAsync(_cancellationToken).ConfigureAwait(false); + _cancellationToken.ThrowIfCancellationRequested(); - Task fetchPromise = BrowserHttpInterop.Fetch(uri, headerNames.ToArray(), headerValues.ToArray(), optionNames, optionValues, abortController, buffer); - fetchResponse = await BrowserHttpInterop.CancelationHelper(fetchPromise, cancellationToken).ConfigureAwait(true); + Memory bufferMemory = buffer.AsMemory(); + pinBuffer = bufferMemory.Pin(); + fetchPromise = BrowserHttpInterop.FetchBytes(_jsController, uri, _headerNames, _headerValues, _optionNames, _optionValues, pinBuffer.Value, buffer.Length); } } else { - Task fetchPromise = BrowserHttpInterop.Fetch(uri, headerNames.ToArray(), headerValues.ToArray(), optionNames, optionValues, abortController); - fetchResponse = await BrowserHttpInterop.CancelationHelper(fetchPromise, cancellationToken).ConfigureAwait(true); + fetchPromise = BrowserHttpInterop.Fetch(_jsController, uri, _headerNames, _headerValues, _optionNames, _optionValues); } + await BrowserHttpInterop.CancellationHelper(fetchPromise, _cancellationToken, _jsController).ConfigureAwait(false); - return new WasmFetchResponse(fetchResponse, abortController, abortRegistration); + return ConvertResponse(); } catch (Exception ex) { - abortRegistration.Dispose(); - abortController.Dispose(); + BrowserHttpInterop.AbortRequest(_jsController); + + Dispose(); if (ex is JSException jse) { throw new HttpRequestException(jse.Message, jse); } throw; } + finally + { + pinBuffer?.Dispose(); + writeStream?.Dispose(); + } } - private static HttpResponseMessage ConvertResponse(HttpRequestMessage request, WasmFetchResponse fetchResponse) + private HttpResponseMessage ConvertResponse() { -#if FEATURE_WASM_THREADS - lock (fetchResponse.ThisLock) + lock (this) { -#endif - fetchResponse.ThrowIfDisposed(); - string? responseType = fetchResponse.FetchResponse!.GetPropertyAsString("type")!; - int status = fetchResponse.FetchResponse.GetPropertyAsInt32("status"); + ThrowIfDisposed(); + string? responseType = BrowserHttpInterop.GetResponseType(_jsController); + int status = BrowserHttpInterop.GetResponseStatus(_jsController); HttpResponseMessage responseMessage = new HttpResponseMessage((HttpStatusCode)status); - responseMessage.RequestMessage = request; + responseMessage.RequestMessage = _request; if (responseType == "opaqueredirect") { // Here we will set the ReasonPhrase so that it can be evaluated later. @@ -309,77 +321,75 @@ private static HttpResponseMessage ConvertResponse(HttpRequestMessage request, W responseMessage.SetReasonPhraseWithoutValidation(responseType); } - bool streamingEnabled = false; + bool streamingResponseEnabled = false; if (BrowserHttpInterop.SupportsStreamingResponse()) { - request.Options.TryGetValue(EnableStreamingResponse, out streamingEnabled); + _request.Options.TryGetValue(EnableStreamingResponse, out streamingResponseEnabled); } - responseMessage.Content = streamingEnabled - ? new StreamContent(new WasmHttpReadStream(fetchResponse)) - : new BrowserHttpContent(fetchResponse); + responseMessage.Content = streamingResponseEnabled + ? new StreamContent(new BrowserHttpReadStream(this)) + : new BrowserHttpContent(this); - - // Some of the headers may not even be valid header types in .NET thus we use TryAddWithoutValidation - // CORS will only allow access to certain headers on browser. - BrowserHttpInterop.GetResponseHeaders(fetchResponse.FetchResponse, responseMessage.Headers, responseMessage.Content.Headers); + BrowserHttpInterop.GetResponseHeaders(_jsController!, responseMessage.Headers, responseMessage.Content.Headers); return responseMessage; -#if FEATURE_WASM_THREADS } //lock -#endif } - protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public void ThrowIfDisposed() { - bool? allowAutoRedirect = _isAllowAutoRedirectTouched ? AllowAutoRedirect : null; -#if FEATURE_WASM_THREADS - return JSHost.CurrentOrMainJSSynchronizationContext.Post(() => + lock (this) { -#endif - return Impl(request, cancellationToken, allowAutoRedirect); -#if FEATURE_WASM_THREADS - }); -#endif + ObjectDisposedException.ThrowIf(_isDisposed, this); + } //lock + } - static async Task Impl(HttpRequestMessage request, CancellationToken cancellationToken, bool? allowAutoRedirect) + public void Dispose() + { + lock (this) { - WasmFetchResponse fetchRespose = await CallFetch(request, cancellationToken, allowAutoRedirect).ConfigureAwait(true); - return ConvertResponse(request, fetchRespose); + if (_isDisposed) + return; + _isDisposed = true; + } + _abortRegistration.Dispose(); + if (_jsController != null) + { + if (!_jsController.IsDisposed) + { + BrowserHttpInterop.AbortRequest(_jsController);// aborts also response + } + _jsController.Dispose(); } } } - internal sealed class WasmHttpWriteStream : Stream + internal sealed class BrowserHttpWriteStream : Stream { - private readonly JSObject _transformStream; - - public WasmHttpWriteStream(JSObject transformStream) + private readonly BrowserHttpController _controller; // we don't own it, we don't dispose it from here + public BrowserHttpWriteStream(BrowserHttpController controller) { - ArgumentNullException.ThrowIfNull(transformStream); + ArgumentNullException.ThrowIfNull(controller); - _transformStream = transformStream; + _controller = controller; } private Task WriteAsyncCore(ReadOnlyMemory buffer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); -#if FEATURE_WASM_THREADS - return _transformStream.SynchronizationContext.Post(() => Impl(this, buffer, cancellationToken)); -#else - return Impl(this, buffer, cancellationToken); -#endif - static async Task Impl(WasmHttpWriteStream self, ReadOnlyMemory buffer, CancellationToken cancellationToken) + + MemoryHandle pinBuffer = buffer.Pin(); + try { - using (Buffers.MemoryHandle handle = buffer.Pin()) - { - Task writePromise = TransformStreamWriteUnsafe(self._transformStream, buffer, handle); - await BrowserHttpInterop.CancelationHelper(writePromise, cancellationToken).ConfigureAwait(true); - } + Task writePromise = BrowserHttpInterop.TransformStreamWriteUnsafe(_controller._jsController, buffer, pinBuffer); + return BrowserHttpInterop.CancellationHelper(writePromise, cancellationToken, _controller._jsController); + } + catch + { + pinBuffer.Dispose(); + throw; } - - static unsafe Task TransformStreamWriteUnsafe(JSObject transformStream, ReadOnlyMemory buffer, Buffers.MemoryHandle handle) - => BrowserHttpInterop.TransformStreamWrite(transformStream, (nint)handle.Pointer, buffer.Length); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) @@ -399,7 +409,6 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati protected override void Dispose(bool disposing) { - _transformStream.Dispose(); } public override void Flush() @@ -436,159 +445,59 @@ public override void Write(byte[] buffer, int offset, int count) #endregion } - internal sealed class WasmFetchResponse : IDisposable - { -#if FEATURE_WASM_THREADS - public readonly object ThisLock = new object(); -#endif - public JSObject? FetchResponse; - private readonly JSObject _abortController; - private readonly CancellationTokenRegistration _abortRegistration; - private bool _isDisposed; - - public WasmFetchResponse(JSObject fetchResponse, JSObject abortController, CancellationTokenRegistration abortRegistration) - { - ArgumentNullException.ThrowIfNull(fetchResponse); - ArgumentNullException.ThrowIfNull(abortController); - - FetchResponse = fetchResponse; - _abortRegistration = abortRegistration; - _abortController = abortController; - } - - public void ThrowIfDisposed() - { -#if FEATURE_WASM_THREADS - lock (ThisLock) - { -#endif - ObjectDisposedException.ThrowIf(_isDisposed, this); -#if FEATURE_WASM_THREADS - } //lock -#endif - } - - public void Dispose() - { - if (_isDisposed) - return; - -#if FEATURE_WASM_THREADS - FetchResponse?.SynchronizationContext.Post(static (WasmFetchResponse self) => - { - lock (self.ThisLock) - { - if (!self._isDisposed) - { - self._isDisposed = true; - self._abortRegistration.Dispose(); - self._abortController.Dispose(); - if (!self.FetchResponse!.IsDisposed) - { - BrowserHttpInterop.AbortResponse(self.FetchResponse); - } - self.FetchResponse.Dispose(); - self.FetchResponse = null; - } - return Task.CompletedTask; - } - }, this); -#else - _isDisposed = true; - _abortRegistration.Dispose(); - _abortController.Dispose(); - if (FetchResponse != null) - { - if (!FetchResponse.IsDisposed) - { - BrowserHttpInterop.AbortResponse(FetchResponse); - } - FetchResponse.Dispose(); - FetchResponse = null; - } -#endif - } - } - internal sealed class BrowserHttpContent : HttpContent { private byte[]? _data; private int _length = -1; - private readonly WasmFetchResponse _fetchResponse; + private readonly BrowserHttpController _controller; - public BrowserHttpContent(WasmFetchResponse fetchResponse) + public BrowserHttpContent(BrowserHttpController fetchResponse) { ArgumentNullException.ThrowIfNull(fetchResponse); - _fetchResponse = fetchResponse; + _controller = fetchResponse; } // TODO allocate smaller buffer and call multiple times private async ValueTask GetResponseData(CancellationToken cancellationToken) { Task promise; -#if FEATURE_WASM_THREADS - lock (_fetchResponse.ThisLock) + lock (_controller) { -#endif if (_data != null) { return _data; } - _fetchResponse.ThrowIfDisposed(); - promise = BrowserHttpInterop.GetResponseLength(_fetchResponse.FetchResponse!); -#if FEATURE_WASM_THREADS + _controller.ThrowIfDisposed(); + promise = BrowserHttpInterop.GetResponseLength(_controller._jsController!); } //lock -#endif - _length = await BrowserHttpInterop.CancelationHelper(promise, cancellationToken, _fetchResponse.FetchResponse).ConfigureAwait(true); -#if FEATURE_WASM_THREADS - lock (_fetchResponse.ThisLock) + _length = await BrowserHttpInterop.CancellationHelper(promise, cancellationToken, _controller._jsController).ConfigureAwait(false); + lock (_controller) { -#endif _data = new byte[_length]; - BrowserHttpInterop.GetResponseBytes(_fetchResponse.FetchResponse!, new Span(_data)); + BrowserHttpInterop.GetResponseBytes(_controller._jsController!, new Span(_data)); return _data; -#if FEATURE_WASM_THREADS } //lock -#endif } - protected override Task CreateContentReadStreamAsync() + protected override async Task CreateContentReadStreamAsync() { - _fetchResponse.ThrowIfDisposed(); -#if FEATURE_WASM_THREADS - return _fetchResponse.FetchResponse!.SynchronizationContext.Post(() => Impl(this)); -#else - return Impl(this); -#endif - static async Task Impl(BrowserHttpContent self) - { - self._fetchResponse.ThrowIfDisposed(); - byte[] data = await self.GetResponseData(CancellationToken.None).ConfigureAwait(true); - return new MemoryStream(data, writable: false); - } + _controller.ThrowIfDisposed(); + byte[] data = await GetResponseData(CancellationToken.None).ConfigureAwait(false); + return new MemoryStream(data, writable: false); } protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsync(stream, context, CancellationToken.None); - protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(stream, nameof(stream)); - _fetchResponse.ThrowIfDisposed(); -#if FEATURE_WASM_THREADS - return _fetchResponse.FetchResponse!.SynchronizationContext.Post(() => Impl(this, stream, cancellationToken)); -#else - return Impl(this, stream, cancellationToken); -#endif + _controller.ThrowIfDisposed(); - static async Task Impl(BrowserHttpContent self, Stream stream, CancellationToken cancellationToken) - { - self._fetchResponse.ThrowIfDisposed(); - byte[] data = await self.GetResponseData(cancellationToken).ConfigureAwait(true); - await stream.WriteAsync(data, cancellationToken).ConfigureAwait(true); - } + byte[] data = await GetResponseData(cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(data, cancellationToken).ConfigureAwait(false); } protected internal override bool TryComputeLength(out long length) @@ -605,52 +514,40 @@ protected internal override bool TryComputeLength(out long length) protected override void Dispose(bool disposing) { - _fetchResponse.Dispose(); + _controller.Dispose(); base.Dispose(disposing); } } - internal sealed class WasmHttpReadStream : Stream + internal sealed class BrowserHttpReadStream : Stream { - private WasmFetchResponse _fetchResponse; + private BrowserHttpController _controller; // we own the object and have to dispose it - public WasmHttpReadStream(WasmFetchResponse fetchResponse) + public BrowserHttpReadStream(BrowserHttpController fetchResponse) { - _fetchResponse = fetchResponse; + _controller = fetchResponse; } - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(buffer, nameof(buffer)); - _fetchResponse.ThrowIfDisposed(); -#if FEATURE_WASM_THREADS - return await _fetchResponse.FetchResponse!.SynchronizationContext.Post(() => Impl(this, buffer, cancellationToken)).ConfigureAwait(true); -#else - return await Impl(this, buffer, cancellationToken).ConfigureAwait(true); -#endif + _controller.ThrowIfDisposed(); - static async Task Impl(WasmHttpReadStream self, Memory buffer, CancellationToken cancellationToken) + MemoryHandle pinBuffer = buffer.Pin(); + try { - self._fetchResponse.ThrowIfDisposed(); - Task promise; - using (Buffers.MemoryHandle handle = buffer.Pin()) - { -#if FEATURE_WASM_THREADS - lock (self._fetchResponse.ThisLock) - { -#endif - self._fetchResponse.ThrowIfDisposed(); - promise = GetStreamedResponseBytesUnsafe(self._fetchResponse, buffer, handle); -#if FEATURE_WASM_THREADS - } //lock -#endif - int response = await BrowserHttpInterop.CancelationHelper(promise, cancellationToken, self._fetchResponse.FetchResponse).ConfigureAwait(true); - return response; - } + _controller.ThrowIfDisposed(); + + var promise = BrowserHttpInterop.GetStreamedResponseBytesUnsafe(_controller, buffer, pinBuffer); + var task = BrowserHttpInterop.CancellationHelper(promise, cancellationToken, _controller._jsController); + return new ValueTask(task); - unsafe static Task GetStreamedResponseBytesUnsafe(WasmFetchResponse _fetchResponse, Memory buffer, Buffers.MemoryHandle handle) - => BrowserHttpInterop.GetStreamedResponseBytes(_fetchResponse.FetchResponse!, (IntPtr)handle.Pointer, buffer.Length); } + finally + { + pinBuffer.Dispose(); + } + } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -665,7 +562,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel protected override void Dispose(bool disposing) { - _fetchResponse.Dispose(); + _controller.Dispose(); } public override void Flush() diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs index b942bccd4c760a..be4e3921cf5407 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.IO; +using System.Buffers; using System.Net.Http.Headers; using System.Runtime.InteropServices.JavaScript; using System.Threading; @@ -17,47 +17,53 @@ internal static partial class BrowserHttpInterop [JSImport("INTERNAL.http_wasm_supports_streaming_response")] public static partial bool SupportsStreamingResponse(); - [JSImport("INTERNAL.http_wasm_create_abort_controler")] - public static partial JSObject CreateAbortController(); + [JSImport("INTERNAL.http_wasm_create_controller")] + public static partial JSObject CreateController(); [JSImport("INTERNAL.http_wasm_abort_request")] public static partial void AbortRequest( - JSObject abortController); + JSObject httpController); [JSImport("INTERNAL.http_wasm_abort_response")] public static partial void AbortResponse( - JSObject fetchResponse); - - [JSImport("INTERNAL.http_wasm_create_transform_stream")] - public static partial JSObject CreateTransformStream(); + JSObject httpController); [JSImport("INTERNAL.http_wasm_transform_stream_write")] public static partial Task TransformStreamWrite( - JSObject transformStream, + JSObject httpController, IntPtr bufferPtr, int bufferLength); + public static unsafe Task TransformStreamWriteUnsafe(JSObject httpController, ReadOnlyMemory buffer, Buffers.MemoryHandle handle) + => TransformStreamWrite(httpController, (nint)handle.Pointer, buffer.Length); + [JSImport("INTERNAL.http_wasm_transform_stream_close")] public static partial Task TransformStreamClose( - JSObject transformStream); - - [JSImport("INTERNAL.http_wasm_transform_stream_abort")] - public static partial void TransformStreamAbort( - JSObject transformStream); + JSObject httpController); [JSImport("INTERNAL.http_wasm_get_response_header_names")] private static partial string[] _GetResponseHeaderNames( - JSObject fetchResponse); + JSObject httpController); [JSImport("INTERNAL.http_wasm_get_response_header_values")] private static partial string[] _GetResponseHeaderValues( - JSObject fetchResponse); + JSObject httpController); + + [JSImport("INTERNAL.http_wasm_get_response_status")] + public static partial int GetResponseStatus( + JSObject httpController); + + [JSImport("INTERNAL.http_wasm_get_response_type")] + public static partial string GetResponseType( + JSObject httpController); - public static void GetResponseHeaders(JSObject fetchResponse, HttpHeaders resposeHeaders, HttpHeaders contentHeaders) + public static void GetResponseHeaders(JSObject httpController, HttpHeaders resposeHeaders, HttpHeaders contentHeaders) { - string[] headerNames = _GetResponseHeaderNames(fetchResponse); - string[] headerValues = _GetResponseHeaderValues(fetchResponse); + string[] headerNames = _GetResponseHeaderNames(httpController); + string[] headerValues = _GetResponseHeaderValues(httpController); + // Some of the headers may not even be valid header types in .NET thus we use TryAddWithoutValidation + // CORS will only allow access to certain headers on browser. for (int i = 0; i < headerNames.Length; i++) { if (!resposeHeaders.TryAddWithoutValidation(headerNames[i], headerValues[i])) @@ -67,43 +73,38 @@ public static void GetResponseHeaders(JSObject fetchResponse, HttpHeaders respos } } - [JSImport("INTERNAL.http_wasm_fetch")] - public static partial Task Fetch( + public static partial Task Fetch( + JSObject httpController, string uri, string[] headerNames, string[] headerValues, string[] optionNames, - [JSMarshalAs>] object?[] optionValues, - JSObject abortControler); + [JSMarshalAs>] object?[] optionValues); [JSImport("INTERNAL.http_wasm_fetch_stream")] - public static partial Task Fetch( + public static partial Task FetchStream( + JSObject httpController, string uri, string[] headerNames, string[] headerValues, string[] optionNames, - [JSMarshalAs>] object?[] optionValues, - JSObject abortControler, - JSObject transformStream); + [JSMarshalAs>] object?[] optionValues); [JSImport("INTERNAL.http_wasm_fetch_bytes")] - private static partial Task FetchBytes( + private static partial Task FetchBytes( + JSObject httpController, string uri, string[] headerNames, string[] headerValues, string[] optionNames, [JSMarshalAs>] object?[] optionValues, - JSObject abortControler, IntPtr bodyPtr, int bodyLength); - public static unsafe Task Fetch(string uri, string[] headerNames, string[] headerValues, string[] optionNames, object?[] optionValues, JSObject abortControler, byte[] body) + public static unsafe Task FetchBytes(JSObject httpController, string uri, string[] headerNames, string[] headerValues, string[] optionNames, object?[] optionValues, MemoryHandle pinBuffer, int bodyLength) { - fixed (byte* ptr = body) - { - return FetchBytes(uri, headerNames, headerValues, optionNames, optionValues, abortControler, (IntPtr)ptr, body.Length); - } + return FetchBytes(httpController, uri, headerNames, headerValues, optionNames, optionValues, (IntPtr)pinBuffer.Pointer, bodyLength); } [JSImport("INTERNAL.http_wasm_get_streamed_response_bytes")] @@ -112,6 +113,10 @@ public static partial Task GetStreamedResponseBytes( IntPtr bufferPtr, int bufferLength); + public static unsafe Task GetStreamedResponseBytesUnsafe(BrowserHttpController _fetchResponse, Memory buffer, MemoryHandle handle) + => GetStreamedResponseBytes(_fetchResponse._jsController!, (IntPtr)handle.Pointer, buffer.Length); + + [JSImport("INTERNAL.http_wasm_get_response_length")] public static partial Task GetResponseLength( JSObject fetchResponse); @@ -122,8 +127,12 @@ public static partial int GetResponseBytes( [JSMarshalAs] Span buffer); - public static async ValueTask CancelationHelper(Task promise, CancellationToken cancellationToken, JSObject? fetchResponse = null) + public static async Task CancellationHelper(Task promise, CancellationToken cancellationToken, JSObject jsController) { + if (cancellationToken.IsCancellationRequested) + { + throw Http.CancellationHelper.CreateOperationCanceledException(null, cancellationToken); + } if (promise.IsCompletedSuccessfully) { return; @@ -132,46 +141,49 @@ public static async ValueTask CancelationHelper(Task promise, CancellationToken { using (var operationRegistration = cancellationToken.Register(static s => { - (Task _promise, JSObject? _fetchResponse) = ((Task, JSObject?))s!; - CancelablePromise.CancelPromise(_promise, static (JSObject? __fetchResponse) => + (Task _promise, JSObject _jsController) = ((Task, JSObject))s!; + CancelablePromise.CancelPromise(_promise, static (JSObject __jsController) => { - if (__fetchResponse != null) + if (!__jsController.IsDisposed) { - AbortResponse(__fetchResponse); + AbortResponse(__jsController); } - }, _fetchResponse); - }, (promise, fetchResponse))) + }, _jsController); + }, (promise, jsController))) { await promise.ConfigureAwait(true); } } catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested) { - throw CancellationHelper.CreateOperationCanceledException(oce, cancellationToken); + throw Http.CancellationHelper.CreateOperationCanceledException(oce, cancellationToken); } catch (JSException jse) { if (jse.Message.StartsWith("AbortError", StringComparison.Ordinal)) { - throw CancellationHelper.CreateOperationCanceledException(jse, CancellationToken.None); + throw Http.CancellationHelper.CreateOperationCanceledException(jse, CancellationToken.None); } if (cancellationToken.IsCancellationRequested) { - throw CancellationHelper.CreateOperationCanceledException(jse, cancellationToken); + throw Http.CancellationHelper.CreateOperationCanceledException(jse, cancellationToken); } throw new HttpRequestException(jse.Message, jse); } } - public static async ValueTask CancelationHelper(Task promise, CancellationToken cancellationToken, JSObject? fetchResponse = null) + public static async Task CancellationHelper(Task promise, CancellationToken cancellationToken, JSObject jsController) { + if (cancellationToken.IsCancellationRequested) + { + throw Http.CancellationHelper.CreateOperationCanceledException(null, cancellationToken); + } if (promise.IsCompletedSuccessfully) { return promise.Result; } - await CancelationHelper((Task)promise, cancellationToken, fetchResponse).ConfigureAwait(true); - return await promise.ConfigureAwait(true); + await CancellationHelper((Task)promise, cancellationToken, jsController).ConfigureAwait(false); + return promise.Result; } } - } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 773d73a39a0943..2451c867a9489d 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -16,8 +16,6 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) $(DefineConstants);TargetsWindows $(DefineConstants);TARGETS_BROWSER - - <_XUnitBackgroundExec>false diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.xml b/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.xml index 74336f2b956571..188158a51fdd20 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.xml +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/CompatibilitySuppressions.xml @@ -37,12 +37,6 @@ runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll - - CP0001 - T:System.Runtime.InteropServices.JavaScript.SynchronizationContextExtension - ref/net9.0/System.Runtime.InteropServices.JavaScript.dll - runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll - CP0001 T:System.Runtime.InteropServices.JavaScript.CancelablePromise @@ -55,10 +49,4 @@ ref/net9.0/System.Runtime.InteropServices.JavaScript.dll runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll - - CP0002 - M:System.Runtime.InteropServices.JavaScript.JSHost.get_CurrentOrMainJSSynchronizationContext - ref/net9.0/System.Runtime.InteropServices.JavaScript.dll - runtimes/browser/lib/net9.0/System.Runtime.InteropServices.JavaScript.dll - diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj index 1b8664dc435917..c0d36600f09b44 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System.Runtime.InteropServices.JavaScript.csproj @@ -45,7 +45,6 @@ - diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs index b5238826e5bb79..0a685e996882da 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHost.cs @@ -50,17 +50,5 @@ public static Task ImportAsync(string moduleName, string moduleUrl, Ca return JSHostImplementation.ImportAsync(moduleName, moduleUrl, cancellationToken); } - public static SynchronizationContext CurrentOrMainJSSynchronizationContext - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if FEATURE_WASM_THREADS - return (JSProxyContext.ExecutionContext ?? JSProxyContext.MainThreadContext).SynchronizationContext; -#else - return null!; -#endif - } - } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs deleted file mode 100644 index 6f63db9cfd3375..00000000000000 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/SynchronizationContextExtensions.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; -using System.Threading.Tasks; - -namespace System.Runtime.InteropServices.JavaScript -{ - /// - /// Extensions of SynchronizationContext which propagate errors and return values - /// - public static class SynchronizationContextExtension - { - public static void Send(this SynchronizationContext self, Action body, T value) - { - Exception? exc = default; - self.Send((_value) => - { - try - { - body((T)_value!); - } - catch (Exception ex) - { - exc = ex; - } - }, value); - if (exc != null) - { - throw exc; - } - } - - public static TRes Send(this SynchronizationContext self, Func body) - { - TRes? value = default; - Exception? exc = default; - self.Send((_) => - { - try - { - value = body(); - } - catch (Exception ex) - { - exc = ex; - } - }, null); - if (exc != null) - { - throw exc; - } - return value!; - } - - public static Task Post(this SynchronizationContext self, Func> body) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - self.Post(async (_) => - { - try - { - var value = await body().ConfigureAwait(false); - tcs.TrySetResult(value); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }, null); - return tcs.Task; - } - - public static Task Post(this SynchronizationContext? self, Func> body, T1 p1) - { - if (self == null) return body(p1); - - TaskCompletionSource tcs = new TaskCompletionSource(); - self.Post(async (_) => - { - try - { - var value = await body(p1).ConfigureAwait(false); - tcs.TrySetResult(value); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }, null); - return tcs.Task; - } - - public static Task Post(this SynchronizationContext self, Func body, T1 p1) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - self.Post(async (_) => - { - try - { - await body(p1).ConfigureAwait(false); - tcs.TrySetResult(); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }, null); - return tcs.Task; - } - - public static Task Post(this SynchronizationContext self, Func body) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - self.Post(async (_) => - { - try - { - await body().ConfigureAwait(false); - tcs.TrySetResult(); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }, null); - return tcs.Task; - } - - public static TRes Send(this SynchronizationContext self, Func body, T1 p1) - { - TRes? value = default; - Exception? exc = default; - self.Send((_) => - { - try - { - value = body(p1); - } - catch (Exception ex) - { - exc = ex; - } - }, null); - if (exc != null) - { - throw exc; - } - return value!; - } - } -} diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index cd8e24d6343e70..92116ee717afb9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -28,7 +28,7 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // JS setTimeout till after JSWebWorker close // synchronous .Wait for JS setTimeout on the same thread -> deadlock problem **7)** - public class WebWorkerTest + public class WebWorkerTest : IAsyncLifetime { const int TimeoutMilliseconds = 300; @@ -600,5 +600,96 @@ public Task WebSocketClient_CancelInDifferentThread(Executor executor1, Executor } #endregion + + #region HTTP + + [Theory, MemberData(nameof(GetTargetThreads))] + public async Task HttpClient_ContentInSameThread(Executor executor) + { + var cts = new CancellationTokenSource(TimeoutMilliseconds); + var uri = WebWorkerTestHelper.GetOriginUrl() + "/_framework/blazor.boot.json"; + + await executor.Execute(async () => + { + using var client = new HttpClient(); + using var response = await client.GetAsync(uri); + response.EnsureSuccessStatusCode(); + var body = await response.Content.ReadAsStringAsync(); + Assert.StartsWith("{", body); + }, cts.Token); + } + + private static HttpRequestOptionsKey WebAssemblyEnableStreamingRequestKey = new("WebAssemblyEnableStreamingRequest"); + private static HttpRequestOptionsKey WebAssemblyEnableStreamingResponseKey = new("WebAssemblyEnableStreamingResponse"); + private static string HelloJson = "{'hello':'world'}".Replace('\'', '"'); + private static string EchoStart = "{\"Method\":\"POST\",\"Url\":\"/Echo.ashx"; + + private Task HttpClient_ActionInDifferentThread(string url, Executor executor1, Executor executor2, Func e2Job) + { + var cts = new CancellationTokenSource(TimeoutMilliseconds); + + var e1Job = async (Task e2done, TaskCompletionSource e1State) => + { + using var ms = new MemoryStream(); + await ms.WriteAsync(Encoding.UTF8.GetBytes(HelloJson)); + + using var req = new HttpRequestMessage(HttpMethod.Post, url); + req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); + req.Content = new StreamContent(ms); + using var client = new HttpClient(); + var pr = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + using var response = await pr; + + // share the state with the E2 continuation + e1State.SetResult(response); + + await e2done; + }; + return ActionsInDifferentThreads(executor1, executor2, e1Job, e2Job, cts); + } + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task HttpClient_ContentInDifferentThread(Executor executor1, Executor executor2) + { + var url = WebWorkerTestHelper.LocalHttpEcho + "?guid=" + Guid.NewGuid(); + await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => + { + response.EnsureSuccessStatusCode(); + var body = await response.Content.ReadAsStringAsync(); + Assert.StartsWith(EchoStart, body); + }); + } + + [Theory, MemberData(nameof(GetTargetThreads2x))] + public async Task HttpClient_CancelInDifferentThread(Executor executor1, Executor executor2) + { + var url = WebWorkerTestHelper.LocalHttpEcho + "?delay10sec=true&guid=" + Guid.NewGuid(); + await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => + { + CancellationTokenSource cts = new CancellationTokenSource(); + await Assert.ThrowsAsync(async () => + { + var promise = response.Content.ReadAsStringAsync(cts.Token); + cts.Cancel(); + await promise; + }); + }); + } + + #endregion + + public static bool _isWarmupDone; + + public async Task InitializeAsync() + { + if (_isWarmupDone) + { + return; + } + await Task.Delay(500); + _isWarmupDone = true; + } + + public Task DisposeAsync() => Task.CompletedTask; } } diff --git a/src/mono/browser/runtime/exports-internal.ts b/src/mono/browser/runtime/exports-internal.ts index f81a7b95a5b07e..d174432d1209e8 100644 --- a/src/mono/browser/runtime/exports-internal.ts +++ b/src/mono/browser/runtime/exports-internal.ts @@ -4,7 +4,7 @@ import { mono_wasm_cancel_promise } from "./cancelable-promise"; import cwraps, { profiler_c_functions } from "./cwraps"; import { mono_wasm_send_dbg_command_with_parms, mono_wasm_send_dbg_command, mono_wasm_get_dbg_command_info, mono_wasm_get_details, mono_wasm_release_object, mono_wasm_call_function_on, mono_wasm_debugger_resume, mono_wasm_detach_debugger, mono_wasm_raise_debug_event, mono_wasm_change_debugger_log_level, mono_wasm_debugger_attached } from "./debug"; -import { http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, http_wasm_create_abort_controler, http_wasm_abort_request, http_wasm_abort_response, http_wasm_create_transform_stream, http_wasm_transform_stream_write, http_wasm_transform_stream_close, http_wasm_transform_stream_abort, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_bytes, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes } from "./http"; +import { http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, http_wasm_create_controller, http_wasm_abort_request, http_wasm_abort_response, http_wasm_transform_stream_write, http_wasm_transform_stream_close, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_bytes, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes, http_wasm_get_response_type, http_wasm_get_response_status } from "./http"; import { exportedRuntimeAPI, Module, runtimeHelpers } from "./globals"; import { get_property, set_property, has_property, get_typeof_property, get_global_this, dynamic_import } from "./invoke-js"; import { mono_wasm_stringify_as_error_with_stack } from "./logging"; @@ -71,13 +71,13 @@ export function export_internal(): any { // BrowserHttpHandler http_wasm_supports_streaming_request, http_wasm_supports_streaming_response, - http_wasm_create_abort_controler, + http_wasm_create_controller, + http_wasm_get_response_type, + http_wasm_get_response_status, http_wasm_abort_request, http_wasm_abort_response, - http_wasm_create_transform_stream, http_wasm_transform_stream_write, http_wasm_transform_stream_close, - http_wasm_transform_stream_abort, http_wasm_fetch, http_wasm_fetch_stream, http_wasm_fetch_bytes, diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index 64026166f1a593..49ee49d9eb7780 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import BuildConfiguration from "consts:configuration"; + import { wrap_as_cancelable_promise } from "./cancelable-promise"; import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert } from "./globals"; import { assert_js_interop } from "./invoke-js"; @@ -18,6 +20,11 @@ function verifyEnvironment() { } } +function commonAsserts(controller: HttpController) { + assert_js_interop(); + mono_assert(controller, "expected controller"); +} + export function http_wasm_supports_streaming_request(): boolean { // Detecting streaming request support works like this: // If the browser doesn't support a particular body type, it calls toString() on the object and uses the result as the body. @@ -45,19 +52,27 @@ export function http_wasm_supports_streaming_response(): boolean { return typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function"; } -export function http_wasm_create_abort_controler(): AbortController { +export function http_wasm_create_controller(): HttpController { verifyEnvironment(); - return new AbortController(); + assert_js_interop(); + const controller: Partial = { + __abort_controller: new AbortController() + }; + return controller as HttpController; } -export function http_wasm_abort_request(abort_controller: AbortController): void { - abort_controller.abort(); +export function http_wasm_abort_request(controller: HttpController): void { + if (controller.__writer) { + controller.__writer.abort(); + } + http_wasm_abort_response(controller); } -export function http_wasm_abort_response(res: ResponseExtension): void { - res.__abort_controller.abort(); - if (res.__reader) { - res.__reader.cancel().catch((err) => { +export function http_wasm_abort_response(controller: HttpController): void { + if (BuildConfiguration === "Debug") commonAsserts(controller); + controller.__abort_controller.abort(); + if (controller.__reader) { + controller.__reader.cancel().catch((err) => { if (err && err.name !== "AbortError") { Module.err("Error in http_wasm_abort_response: " + err); } @@ -66,57 +81,54 @@ export function http_wasm_abort_response(res: ResponseExtension): void { } } -export function http_wasm_create_transform_stream(): TransformStreamExtension { - const transform_stream = new TransformStream() as TransformStreamExtension; - transform_stream.__writer = transform_stream.writable.getWriter(); - return transform_stream; -} - -export function http_wasm_transform_stream_write(ts: TransformStreamExtension, bufferPtr: VoidPtr, bufferLength: number): ControllablePromise { +export function http_wasm_transform_stream_write(controller: HttpController, bufferPtr: VoidPtr, bufferLength: number): ControllablePromise { + if (BuildConfiguration === "Debug") commonAsserts(controller); mono_assert(bufferLength > 0, "expected bufferLength > 0"); // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); const copy = view.slice() as Uint8Array; return wrap_as_cancelable_promise(async () => { - mono_assert(ts.__fetch_promise, "expected fetch promise"); + mono_assert(controller.__fetch_promise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 - await Promise.race([ts.__writer.ready, ts.__fetch_promise]); - await Promise.race([ts.__writer.write(copy), ts.__fetch_promise]); + await Promise.race([controller.__writer.ready, controller.__fetch_promise]); + await Promise.race([controller.__writer.write(copy), controller.__fetch_promise]); }); } -export function http_wasm_transform_stream_close(ts: TransformStreamExtension): ControllablePromise { +export function http_wasm_transform_stream_close(controller: HttpController): ControllablePromise { + mono_assert(controller, "expected controller"); return wrap_as_cancelable_promise(async () => { - mono_assert(ts.__fetch_promise, "expected fetch promise"); + mono_assert(controller.__fetch_promise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 - await Promise.race([ts.__writer.ready, ts.__fetch_promise]); - await Promise.race([ts.__writer.close(), ts.__fetch_promise]); + await Promise.race([controller.__writer.ready, controller.__fetch_promise]); + await Promise.race([controller.__writer.close(), controller.__fetch_promise]); }); } -export function http_wasm_transform_stream_abort(ts: TransformStreamExtension): void { - ts.__writer.abort(); -} - -export function http_wasm_fetch_stream(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, body: TransformStreamExtension): ControllablePromise { - const fetch_promise = http_wasm_fetch(url, header_names, header_values, option_names, option_values, abort_controller, body.readable); - body.__fetch_promise = fetch_promise; +export function http_wasm_fetch_stream(controller: HttpController, url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[]): ControllablePromise { + if (BuildConfiguration === "Debug") commonAsserts(controller); + controller.__transform_stream = new TransformStream(); + controller.__writer = controller.__transform_stream.writable.getWriter(); + const fetch_promise = http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, controller.__transform_stream.readable); return fetch_promise; } -export function http_wasm_fetch_bytes(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, bodyPtr: VoidPtr, bodyLength: number): ControllablePromise { +export function http_wasm_fetch_bytes(controller: HttpController, url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], bodyPtr: VoidPtr, bodyLength: number): ControllablePromise { + if (BuildConfiguration === "Debug") commonAsserts(controller); // the bodyPtr is pinned by the caller const view = new Span(bodyPtr, bodyLength, MemoryViewType.Byte); const copy = view.slice() as Uint8Array; - return http_wasm_fetch(url, header_names, header_values, option_names, option_values, abort_controller, copy); + return http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, copy); } -export function http_wasm_fetch(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, body: Uint8Array | ReadableStream | null): ControllablePromise { +export function http_wasm_fetch(controller: HttpController, url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], body: Uint8Array | ReadableStream | null): ControllablePromise { + if (BuildConfiguration === "Debug") commonAsserts(controller); verifyEnvironment(); assert_js_interop(); mono_assert(url && typeof url === "string", "expected url string"); mono_assert(header_names && header_values && Array.isArray(header_names) && Array.isArray(header_values) && header_names.length === header_values.length, "expected headerNames and headerValues arrays"); mono_assert(option_names && option_values && Array.isArray(option_names) && Array.isArray(option_values) && option_names.length === option_values.length, "expected headerNames and headerValues arrays"); + const headers = new Headers(); for (let i = 0; i < header_names.length; i++) { headers.append(header_names[i], header_values[i]); @@ -124,7 +136,7 @@ export function http_wasm_fetch(url: string, header_names: string[], header_valu const options: any = { body, headers, - signal: abort_controller.signal + signal: controller.__abort_controller.signal }; if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) { options.duplex = "half"; @@ -132,101 +144,110 @@ export function http_wasm_fetch(url: string, header_names: string[], header_valu for (let i = 0; i < option_names.length; i++) { options[option_names[i]] = option_values[i]; } - - return wrap_as_cancelable_promise(async () => { - const res = await loaderHelpers.fetch_like(url, options) as ResponseExtension; - res.__abort_controller = abort_controller; - return res; + controller.__fetch_promise = wrap_as_cancelable_promise(() => { + return loaderHelpers.fetch_like(url, options).then((res) => { + controller.__response = res; + controller.__headerNames = []; + controller.__headerValues = []; + if (res.headers && (res.headers).entries) { + const entries: Iterable = (res.headers).entries(); + + for (const pair of entries) { + controller.__headerNames.push(pair[0]); + controller.__headerValues.push(pair[1]); + } + } + return res; + }); }); + return controller.__fetch_promise as ControllablePromise; } -function get_response_headers(res: ResponseExtension): void { - if (!res.__headerNames) { - res.__headerNames = []; - res.__headerValues = []; - if (res.headers && (res.headers).entries) { - const entries: Iterable = (res.headers).entries(); +export function http_wasm_get_response_header_names(controller: HttpController): string[] { + if (BuildConfiguration === "Debug") commonAsserts(controller); + return controller.__headerNames; +} - for (const pair of entries) { - res.__headerNames.push(pair[0]); - res.__headerValues.push(pair[1]); - } - } - } +export function http_wasm_get_response_type(controller: HttpController): string | undefined { + if (BuildConfiguration === "Debug") commonAsserts(controller); + return controller.__response?.type; } -export function http_wasm_get_response_header_names(res: ResponseExtension): string[] { - get_response_headers(res); - return res.__headerNames; +export function http_wasm_get_response_status(controller: HttpController): number { + if (BuildConfiguration === "Debug") commonAsserts(controller); + return controller.__response?.status ?? 0; } -export function http_wasm_get_response_header_values(res: ResponseExtension): string[] { - get_response_headers(res); - return res.__headerValues; + +export function http_wasm_get_response_header_values(controller: HttpController): string[] { + if (BuildConfiguration === "Debug") commonAsserts(controller); + return controller.__headerValues; } -export function http_wasm_get_response_length(res: ResponseExtension): ControllablePromise { +export function http_wasm_get_response_length(controller: HttpController): ControllablePromise { + if (BuildConfiguration === "Debug") commonAsserts(controller); return wrap_as_cancelable_promise(async () => { - const buffer = await res.arrayBuffer(); - res.__buffer = buffer; - res.__source_offset = 0; + const buffer = await controller.__response!.arrayBuffer(); + controller.__buffer = buffer; + controller.__source_offset = 0; return buffer.byteLength; }); } -export function http_wasm_get_response_bytes(res: ResponseExtension, view: Span): number { - mono_assert(res.__buffer, "expected resoved arrayBuffer"); - if (res.__source_offset == res.__buffer!.byteLength) { +export function http_wasm_get_response_bytes(controller: HttpController, view: Span): number { + mono_assert(controller, "expected controller"); + mono_assert(controller.__buffer, "expected resoved arrayBuffer"); + if (controller.__source_offset == controller.__buffer!.byteLength) { return 0; } - const source_view = new Uint8Array(res.__buffer!, res.__source_offset); + const source_view = new Uint8Array(controller.__buffer!, controller.__source_offset); view.set(source_view, 0); const bytes_read = Math.min(view.byteLength, source_view.byteLength); - res.__source_offset += bytes_read; + controller.__source_offset += bytes_read; return bytes_read; } -export function http_wasm_get_streamed_response_bytes(res: ResponseExtension, bufferPtr: VoidPtr, bufferLength: number): ControllablePromise { +export function http_wasm_get_streamed_response_bytes(controller: HttpController, bufferPtr: VoidPtr, bufferLength: number): ControllablePromise { + if (BuildConfiguration === "Debug") commonAsserts(controller); // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrap_as_cancelable_promise(async () => { - if (!res.__reader) { - res.__reader = res.body!.getReader(); + if (!controller.__reader) { + controller.__reader = controller.__response!.body!.getReader(); } - if (!res.__chunk) { - res.__chunk = await res.__reader.read(); - res.__source_offset = 0; + if (!controller.__chunk) { + controller.__chunk = await controller.__reader.read(); + controller.__source_offset = 0; } - if (res.__chunk.done) { + if (controller.__chunk!.done) { return 0; } - const remaining_source = res.__chunk.value.byteLength - res.__source_offset; + const remaining_source = controller.__chunk.value.byteLength - controller.__source_offset; mono_assert(remaining_source > 0, "expected remaining_source to be greater than 0"); const bytes_copied = Math.min(remaining_source, view.byteLength); - const source_view = res.__chunk.value.subarray(res.__source_offset, res.__source_offset + bytes_copied); + const source_view = controller.__chunk.value.subarray(controller.__source_offset, controller.__source_offset + bytes_copied); view.set(source_view, 0); - res.__source_offset += bytes_copied; + controller.__source_offset += bytes_copied; if (remaining_source == bytes_copied) { - res.__chunk = undefined; + controller.__chunk = undefined; } return bytes_copied; }); } -interface TransformStreamExtension extends TransformStream { - __writer: WritableStreamDefaultWriter - __fetch_promise?: Promise -} - -interface ResponseExtension extends Response { +interface HttpController { + __abort_controller: AbortController __buffer?: ArrayBuffer __reader?: ReadableStreamDefaultReader __chunk?: ReadableStreamReadResult __source_offset: number - __abort_controller: AbortController - __headerNames: string[]; + __headerNames: string[];//response headers __headerValues: string[]; + __writer: WritableStreamDefaultWriter + __fetch_promise?: ControllablePromise + __response?: Response + __transform_stream?: TransformStream } From 85d6a0f578b997c199e0a9609ea2019d9dca9e93 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 17 Jan 2024 10:50:10 +0100 Subject: [PATCH 02/17] 75123 --- .../tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Add_URL.cs | 1 - src/mono/browser/runtime/dotnet.d.ts | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Add_URL.cs b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Add_URL.cs index 5bd95c0849eeee..205ad7ebd026ee 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Add_URL.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSchema/XmlSchemaSet/TC_SchemaSet_Add_URL.cs @@ -64,7 +64,6 @@ public void v3() //----------------------------------------------------------------------------------- [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/75123", typeof(PlatformDetection), nameof(PlatformDetection.IsWasmThreadingSupported))] //[Variation(Desc = "v4 - ns = valid, URL = invalid")] public void v4() { diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index 5dde3a3e265ca1..88fd7791f760b4 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -354,7 +354,11 @@ type AssetBehaviors = SingleAssetBehaviors | /** * The javascript module for threads. */ - | "symbols"; + | "symbols" +/** + * Load segmentation rules file for Hybrid Globalization. + */ + | "segmentation-rules"; declare const enum GlobalizationMode { /** * Load sharded ICU data. From 08da5aac78ffe302b8ecb2c046faf510f06b9a63 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 17 Jan 2024 18:10:13 +0100 Subject: [PATCH 03/17] more --- .../InteropServices/JavaScript/JSWebWorker.cs | 42 +++++++------------ ...me.InteropServices.JavaScript.Tests.csproj | 2 + 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs index cb9016471923e6..403a55c970165a 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs @@ -32,13 +32,17 @@ public static Task RunAsync(Func body) public static Task RunAsync(Func> body, CancellationToken cancellationToken) { - var instance = new JSWebWorkerInstance(body, null, cancellationToken); + var instance = new JSWebWorkerInstance(body, cancellationToken); return instance.Start(); } public static Task RunAsync(Func body, CancellationToken cancellationToken) { - var instance = new JSWebWorkerInstance(null, body, cancellationToken); + var instance = new JSWebWorkerInstance(async () => + { + await body().ConfigureAwait(false); + return 0; + }, cancellationToken); return instance.Start(); } @@ -49,20 +53,18 @@ internal sealed class JSWebWorkerInstance : IDisposable private Thread _thread; private CancellationToken _cancellationToken; private CancellationTokenRegistration? _cancellationRegistration; - private Func>? _bodyRes; - private Func? _bodyVoid; - private Task? _resultTask; + private Func> _body; + private Task? _resultTask; private bool _isDisposed; - public JSWebWorkerInstance(Func>? bodyRes, Func? bodyVoid, CancellationToken cancellationToken) + public JSWebWorkerInstance(Func> bodyRes, CancellationToken cancellationToken) { _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _thread = new Thread(ThreadMain); _resultTask = null; _cancellationToken = cancellationToken; _cancellationRegistration = null; - _bodyRes = bodyRes; - _bodyVoid = bodyVoid; + _body = bodyRes; JSHostImplementation.SetHasExternalEventLoop(_thread); } @@ -80,7 +82,7 @@ public Task Start() _thread.Start(); } return t; - }, _cancellationToken, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Current); + }, _cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()); } else { @@ -110,17 +112,10 @@ private void ThreadMain() _jsSynchronizationContext = JSSynchronizationContext.InstallWebWorkerInterop(false, _cancellationToken); var childScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - if (_bodyRes != null) - { - _resultTask = _bodyRes(); - } - else - { - _resultTask = _bodyVoid!(); - } + // This code is exiting thread ThreadMain() before all promises are resolved. // the continuation is executed by setTimeout() callback of the WebWorker thread. - _resultTask.ContinueWith(PropagateCompletionAndDispose, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, childScheduler); + _body().ContinueWith(PropagateCompletionAndDispose, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, childScheduler); } catch (Exception ex) { @@ -129,7 +124,7 @@ private void ThreadMain() } // run actions on correct thread - private void PropagateCompletionAndDispose(Task result) + private void PropagateCompletionAndDispose(Task result) { _resultTask = result; @@ -189,14 +184,7 @@ private void PropagateCompletion() } else { - if (_bodyRes != null) - { - _taskCompletionSource.TrySetResult(((Task)_resultTask).Result); - } - else - { - _taskCompletionSource.TrySetResult(default!); - } + _taskCompletionSource.TrySetResult(_resultTask.Result); } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj index ab0e7ac77251ef..1ddadf3c3acea9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj @@ -14,6 +14,8 @@ $(DefineConstants);DISABLE_LEGACY_JS_INTEROP true + + $(WasmXHarnessMonoArgs) --setenv=XHARNESS_LOG_TEST_START=true From f07acd536783653e2b48a151a10db5acbc42166f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 17 Jan 2024 18:36:55 +0100 Subject: [PATCH 04/17] more --- src/mono/browser/runtime/http.ts | 117 +++++++++++---------- src/mono/browser/runtime/loader/logging.ts | 6 +- 2 files changed, 65 insertions(+), 58 deletions(-) diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index 49ee49d9eb7780..6f0b67fc0cd675 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -56,23 +56,23 @@ export function http_wasm_create_controller(): HttpController { verifyEnvironment(); assert_js_interop(); const controller: Partial = { - __abort_controller: new AbortController() + abortController: new AbortController() }; return controller as HttpController; } export function http_wasm_abort_request(controller: HttpController): void { - if (controller.__writer) { - controller.__writer.abort(); + if (controller.streamWriter) { + controller.streamWriter.abort(); } http_wasm_abort_response(controller); } export function http_wasm_abort_response(controller: HttpController): void { if (BuildConfiguration === "Debug") commonAsserts(controller); - controller.__abort_controller.abort(); - if (controller.__reader) { - controller.__reader.cancel().catch((err) => { + controller.abortController.abort(); + if (controller.streamReader) { + controller.streamReader.cancel().catch((err) => { if (err && err.name !== "AbortError") { Module.err("Error in http_wasm_abort_response: " + err); } @@ -88,28 +88,28 @@ export function http_wasm_transform_stream_write(controller: HttpController, buf const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); const copy = view.slice() as Uint8Array; return wrap_as_cancelable_promise(async () => { - mono_assert(controller.__fetch_promise, "expected fetch promise"); + mono_assert(controller.fetchResponsePromise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 - await Promise.race([controller.__writer.ready, controller.__fetch_promise]); - await Promise.race([controller.__writer.write(copy), controller.__fetch_promise]); + await Promise.race([controller.streamWriter.ready, controller.fetchResponsePromise]); + await Promise.race([controller.streamWriter.write(copy), controller.fetchResponsePromise]); }); } export function http_wasm_transform_stream_close(controller: HttpController): ControllablePromise { mono_assert(controller, "expected controller"); return wrap_as_cancelable_promise(async () => { - mono_assert(controller.__fetch_promise, "expected fetch promise"); + mono_assert(controller.fetchResponsePromise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 - await Promise.race([controller.__writer.ready, controller.__fetch_promise]); - await Promise.race([controller.__writer.close(), controller.__fetch_promise]); + await Promise.race([controller.streamWriter.ready, controller.fetchResponsePromise]); + await Promise.race([controller.streamWriter.close(), controller.fetchResponsePromise]); }); } export function http_wasm_fetch_stream(controller: HttpController, url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[]): ControllablePromise { if (BuildConfiguration === "Debug") commonAsserts(controller); - controller.__transform_stream = new TransformStream(); - controller.__writer = controller.__transform_stream.writable.getWriter(); - const fetch_promise = http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, controller.__transform_stream.readable); + const transformStream = new TransformStream(); + controller.streamWriter = transformStream.writable.getWriter(); + const fetch_promise = http_wasm_fetch(controller, url, header_names, header_values, option_names, option_values, transformStream.readable); return fetch_promise; } @@ -136,7 +136,7 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ const options: any = { body, headers, - signal: controller.__abort_controller.signal + signal: controller.abortController.signal }; if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) { options.duplex = "half"; @@ -144,66 +144,66 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ for (let i = 0; i < option_names.length; i++) { options[option_names[i]] = option_values[i]; } - controller.__fetch_promise = wrap_as_cancelable_promise(() => { + controller.fetchResponsePromise = wrap_as_cancelable_promise(() => { return loaderHelpers.fetch_like(url, options).then((res) => { - controller.__response = res; - controller.__headerNames = []; - controller.__headerValues = []; + controller.fetchResponse = res; + controller.responseHeaderNames = []; + controller.responseHeaderValues = []; if (res.headers && (res.headers).entries) { const entries: Iterable = (res.headers).entries(); for (const pair of entries) { - controller.__headerNames.push(pair[0]); - controller.__headerValues.push(pair[1]); + controller.responseHeaderNames.push(pair[0]); + controller.responseHeaderValues.push(pair[1]); } } return res; }); }); - return controller.__fetch_promise as ControllablePromise; + return controller.fetchResponsePromise as ControllablePromise; } export function http_wasm_get_response_header_names(controller: HttpController): string[] { if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.__headerNames; + return controller.responseHeaderNames; } export function http_wasm_get_response_type(controller: HttpController): string | undefined { if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.__response?.type; + return controller.fetchResponse?.type; } export function http_wasm_get_response_status(controller: HttpController): number { if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.__response?.status ?? 0; + return controller.fetchResponse?.status ?? 0; } export function http_wasm_get_response_header_values(controller: HttpController): string[] { if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.__headerValues; + return controller.responseHeaderValues; } export function http_wasm_get_response_length(controller: HttpController): ControllablePromise { if (BuildConfiguration === "Debug") commonAsserts(controller); return wrap_as_cancelable_promise(async () => { - const buffer = await controller.__response!.arrayBuffer(); - controller.__buffer = buffer; - controller.__source_offset = 0; + const buffer = await controller.fetchResponse!.arrayBuffer(); + controller.responseBuffer = buffer; + controller.currentBufferOffset = 0; return buffer.byteLength; }); } export function http_wasm_get_response_bytes(controller: HttpController, view: Span): number { mono_assert(controller, "expected controller"); - mono_assert(controller.__buffer, "expected resoved arrayBuffer"); - if (controller.__source_offset == controller.__buffer!.byteLength) { + mono_assert(controller.responseBuffer, "expected resoved arrayBuffer"); + if (controller.currentBufferOffset == controller.responseBuffer!.byteLength) { return 0; } - const source_view = new Uint8Array(controller.__buffer!, controller.__source_offset); + const source_view = new Uint8Array(controller.responseBuffer!, controller.currentBufferOffset); view.set(source_view, 0); const bytes_read = Math.min(view.byteLength, source_view.byteLength); - controller.__source_offset += bytes_read; + controller.currentBufferOffset += bytes_read; return bytes_read; } @@ -212,26 +212,26 @@ export function http_wasm_get_streamed_response_bytes(controller: HttpController // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrap_as_cancelable_promise(async () => { - if (!controller.__reader) { - controller.__reader = controller.__response!.body!.getReader(); + if (!controller.streamReader) { + controller.streamReader = controller.fetchResponse!.body!.getReader(); } - if (!controller.__chunk) { - controller.__chunk = await controller.__reader.read(); - controller.__source_offset = 0; + if (!controller.currentStreamReaderChunk) { + controller.currentStreamReaderChunk = await controller.streamReader.read(); + controller.currentBufferOffset = 0; } - if (controller.__chunk!.done) { + if (controller.currentStreamReaderChunk!.done) { return 0; } - const remaining_source = controller.__chunk.value.byteLength - controller.__source_offset; + const remaining_source = controller.currentStreamReaderChunk.value.byteLength - controller.currentBufferOffset; mono_assert(remaining_source > 0, "expected remaining_source to be greater than 0"); const bytes_copied = Math.min(remaining_source, view.byteLength); - const source_view = controller.__chunk.value.subarray(controller.__source_offset, controller.__source_offset + bytes_copied); + const source_view = controller.currentStreamReaderChunk.value.subarray(controller.currentBufferOffset, controller.currentBufferOffset + bytes_copied); view.set(source_view, 0); - controller.__source_offset += bytes_copied; + controller.currentBufferOffset += bytes_copied; if (remaining_source == bytes_copied) { - controller.__chunk = undefined; + controller.currentStreamReaderChunk = undefined; } return bytes_copied; @@ -239,15 +239,22 @@ export function http_wasm_get_streamed_response_bytes(controller: HttpController } interface HttpController { - __abort_controller: AbortController - __buffer?: ArrayBuffer - __reader?: ReadableStreamDefaultReader - __chunk?: ReadableStreamReadResult - __source_offset: number - __headerNames: string[];//response headers - __headerValues: string[]; - __writer: WritableStreamDefaultWriter - __fetch_promise?: ControllablePromise - __response?: Response - __transform_stream?: TransformStream + abortController: AbortController + + // streaming request + streamReader?: ReadableStreamDefaultReader + + // response + fetchResponsePromise?: ControllablePromise + fetchResponse?: Response + responseHeaderNames: string[]; + responseHeaderValues: string[]; + currentBufferOffset: number + + // non-streaming response + responseBuffer?: ArrayBuffer + + // streaming response + streamWriter: WritableStreamDefaultWriter + currentStreamReaderChunk?: ReadableStreamReadResult } diff --git a/src/mono/browser/runtime/loader/logging.ts b/src/mono/browser/runtime/loader/logging.ts index 108c354e23a668..e8fba663b78109 100644 --- a/src/mono/browser/runtime/loader/logging.ts +++ b/src/mono/browser/runtime/loader/logging.ts @@ -98,13 +98,13 @@ export function setup_proxy_console(id: string, console: Console, origin: string ...console }; - setupWS(); - const consoleUrl = `${origin}/console`.replace("https://", "wss://").replace("http://", "ws://"); consoleWebSocket = new WebSocket(consoleUrl); consoleWebSocket.addEventListener("error", logWSError); consoleWebSocket.addEventListener("close", logWSClose); + + setupWS(); } export function teardown_proxy_console(message?: string) { @@ -135,7 +135,7 @@ export function teardown_proxy_console(message?: string) { } function send(msg: string) { - if (consoleWebSocket.readyState === WebSocket.OPEN) { + if (consoleWebSocket && consoleWebSocket.readyState === WebSocket.OPEN) { consoleWebSocket.send(msg); } else { From ff63f697ce4da96377bb6fb81bd46be8db449646 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 17 Jan 2024 18:39:02 +0100 Subject: [PATCH 05/17] add HTTP to MT smoke test --- src/libraries/tests.proj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index a822869d3a39a3..acf917e4e85ca4 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -607,9 +607,7 @@ --> - $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) true diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index 92116ee717afb9..8a6fac2a924736 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -30,10 +30,33 @@ namespace System.Runtime.InteropServices.JavaScript.Tests public class WebWorkerTest : IAsyncLifetime { - const int TimeoutMilliseconds = 300; + const int TimeoutMilliseconds = 5000; + + public static bool _isWarmupDone; + + public async Task InitializeAsync() + { + if (_isWarmupDone) + { + return; + } + await Task.Delay(500); + _isWarmupDone = true; + } + + public Task DisposeAsync() => Task.CompletedTask; #region Executors + private CancellationTokenSource CreateTestCaseTimeoutSource() + { + var cts = new CancellationTokenSource(TimeoutMilliseconds); + cts.Token.Register(() => { + Console.WriteLine($"Unexpected test case timeout at {DateTime.Now.ToString("u")} ManagedThreadId:{Environment.CurrentManagedThreadId}"); + }); + return cts; + } + public static IEnumerable GetTargetThreads() { return Enum.GetValues().Select(type => new object[] { new Executor(type) }); @@ -677,19 +700,5 @@ await Assert.ThrowsAsync(async () => } #endregion - - public static bool _isWarmupDone; - - public async Task InitializeAsync() - { - if (_isWarmupDone) - { - return; - } - await Task.Delay(500); - _isWarmupDone = true; - } - - public Task DisposeAsync() => Task.CompletedTask; } } From 6de377cd74e6de7cad7fae394b3ff209465099c6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 12:36:00 +0100 Subject: [PATCH 07/17] feedback Co-authored-by: campersau --- .../BrowserHttpHandler/BrowserHttpHandler.cs | 75 +++++++------------ .../BrowserHttpHandler/BrowserHttpInterop.cs | 4 +- src/mono/browser/runtime/http.ts | 52 +++++++------ 3 files changed, 60 insertions(+), 71 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs index 7a7728ab7ea57a..3f30e3c6a2c291 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs @@ -112,13 +112,8 @@ public bool AllowAutoRedirect public const bool SupportsProxy = false; public const bool SupportsRedirectConfiguration = true; -#if FEATURE_WASM_THREADS - private ConcurrentDictionary? _properties; - public IDictionary Properties => _properties ??= new ConcurrentDictionary(); -#else private Dictionary? _properties; public IDictionary Properties => _properties ??= new Dictionary(); -#endif protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { @@ -139,20 +134,17 @@ internal sealed class BrowserHttpController : IDisposable private static readonly HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); private static readonly HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); - - internal JSObject _jsController; + internal readonly JSObject _jsController; private readonly CancellationTokenRegistration _abortRegistration; + private readonly string[] _optionNames; + private readonly object?[] _optionValues; + private readonly string[] _headerNames; + private readonly string[] _headerValues; + private readonly string uri; + private readonly CancellationToken _cancellationToken; + private readonly HttpRequestMessage _request; private bool _isDisposed; - private string[] _optionNames; - private object?[] _optionValues; - - private string[] _headerNames; - private string[] _headerValues; - private string uri; - private CancellationToken _cancellationToken; - private HttpRequestMessage _request; - public BrowserHttpController(HttpRequestMessage request, bool? allowAutoRedirect, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); @@ -244,7 +236,6 @@ public async Task CallFetch() BrowserHttpWriteStream? writeStream = null; Task fetchPromise; - MemoryHandle? pinBuffer = null; bool streamingRequestEnabled = false; try @@ -270,8 +261,9 @@ public async Task CallFetch() _cancellationToken.ThrowIfCancellationRequested(); Memory bufferMemory = buffer.AsMemory(); - pinBuffer = bufferMemory.Pin(); - fetchPromise = BrowserHttpInterop.FetchBytes(_jsController, uri, _headerNames, _headerValues, _optionNames, _optionValues, pinBuffer.Value, buffer.Length); + // http_wasm_fetch_byte makes a copy of the bytes synchronously, so we can un-pin it synchronously + using MemoryHandle pinBuffer = bufferMemory.Pin(); + fetchPromise = BrowserHttpInterop.FetchBytes(_jsController, uri, _headerNames, _headerValues, _optionNames, _optionValues, pinBuffer, buffer.Length); } } else @@ -284,9 +276,7 @@ public async Task CallFetch() } catch (Exception ex) { - BrowserHttpInterop.AbortRequest(_jsController); - - Dispose(); + Dispose(); // will also abort request if (ex is JSException jse) { throw new HttpRequestException(jse.Message, jse); @@ -295,7 +285,6 @@ public async Task CallFetch() } finally { - pinBuffer?.Dispose(); writeStream?.Dispose(); } } @@ -378,18 +367,12 @@ public BrowserHttpWriteStream(BrowserHttpController controller) private Task WriteAsyncCore(ReadOnlyMemory buffer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + _controller.ThrowIfDisposed(); - MemoryHandle pinBuffer = buffer.Pin(); - try - { - Task writePromise = BrowserHttpInterop.TransformStreamWriteUnsafe(_controller._jsController, buffer, pinBuffer); - return BrowserHttpInterop.CancellationHelper(writePromise, cancellationToken, _controller._jsController); - } - catch - { - pinBuffer.Dispose(); - throw; - } + // http_wasm_transform_stream_write makes a copy of the bytes synchronously, so we can dispose the handle synchronously + using MemoryHandle pinBuffer = buffer.Pin(); + Task writePromise = BrowserHttpInterop.TransformStreamWriteUnsafe(_controller._jsController, buffer, pinBuffer); + return BrowserHttpInterop.CancellationHelper(writePromise, cancellationToken, _controller._jsController); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) @@ -451,10 +434,10 @@ internal sealed class BrowserHttpContent : HttpContent private int _length = -1; private readonly BrowserHttpController _controller; - public BrowserHttpContent(BrowserHttpController fetchResponse) + public BrowserHttpContent(BrowserHttpController controller) { - ArgumentNullException.ThrowIfNull(fetchResponse); - _controller = fetchResponse; + ArgumentNullException.ThrowIfNull(controller); + _controller = controller; } // TODO allocate smaller buffer and call multiple times @@ -483,7 +466,6 @@ private async ValueTask GetResponseData(CancellationToken cancellationTo protected override async Task CreateContentReadStreamAsync() { - _controller.ThrowIfDisposed(); byte[] data = await GetResponseData(CancellationToken.None).ConfigureAwait(false); return new MemoryStream(data, writable: false); } @@ -494,7 +476,6 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext? protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(stream, nameof(stream)); - _controller.ThrowIfDisposed(); byte[] data = await GetResponseData(cancellationToken).ConfigureAwait(false); await stream.WriteAsync(data, cancellationToken).ConfigureAwait(false); @@ -523,31 +504,31 @@ internal sealed class BrowserHttpReadStream : Stream { private BrowserHttpController _controller; // we own the object and have to dispose it - public BrowserHttpReadStream(BrowserHttpController fetchResponse) + public BrowserHttpReadStream(BrowserHttpController controller) { - _controller = fetchResponse; + _controller = controller; } - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(buffer, nameof(buffer)); _controller.ThrowIfDisposed(); MemoryHandle pinBuffer = buffer.Pin(); + int bytesCount; try { _controller.ThrowIfDisposed(); - var promise = BrowserHttpInterop.GetStreamedResponseBytesUnsafe(_controller, buffer, pinBuffer); - var task = BrowserHttpInterop.CancellationHelper(promise, cancellationToken, _controller._jsController); - return new ValueTask(task); - + var promise = BrowserHttpInterop.GetStreamedResponseBytesUnsafe(_controller._jsController, buffer, pinBuffer); + bytesCount = await BrowserHttpInterop.CancellationHelper(promise, cancellationToken, _controller._jsController).ConfigureAwait(false); } finally { + // this must be after await, because http_wasm_get_streamed_response_bytes is using the buffer in a continuation pinBuffer.Dispose(); } - + return bytesCount; } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs index be4e3921cf5407..4042a40d3ef14a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs @@ -113,8 +113,8 @@ public static partial Task GetStreamedResponseBytes( IntPtr bufferPtr, int bufferLength); - public static unsafe Task GetStreamedResponseBytesUnsafe(BrowserHttpController _fetchResponse, Memory buffer, MemoryHandle handle) - => GetStreamedResponseBytes(_fetchResponse._jsController!, (IntPtr)handle.Pointer, buffer.Length); + public static unsafe Task GetStreamedResponseBytesUnsafe(JSObject jsController, Memory buffer, MemoryHandle handle) + => GetStreamedResponseBytes(jsController, (IntPtr)handle.Pointer, buffer.Length); [JSImport("INTERNAL.http_wasm_get_response_length")] diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index 6f0b67fc0cd675..ad7073994dccce 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -88,6 +88,7 @@ export function http_wasm_transform_stream_write(controller: HttpController, buf const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); const copy = view.slice() as Uint8Array; return wrap_as_cancelable_promise(async () => { + mono_assert(controller.streamWriter, "expected streamWriter"); mono_assert(controller.fetchResponsePromise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 await Promise.race([controller.streamWriter.ready, controller.fetchResponsePromise]); @@ -98,6 +99,7 @@ export function http_wasm_transform_stream_write(controller: HttpController, buf export function http_wasm_transform_stream_close(controller: HttpController): ControllablePromise { mono_assert(controller, "expected controller"); return wrap_as_cancelable_promise(async () => { + mono_assert(controller.streamWriter, "expected streamWriter"); mono_assert(controller.fetchResponsePromise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 await Promise.race([controller.streamWriter.ready, controller.fetchResponsePromise]); @@ -144,30 +146,27 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ for (let i = 0; i < option_names.length; i++) { options[option_names[i]] = option_values[i]; } + // make the fetch cancellable controller.fetchResponsePromise = wrap_as_cancelable_promise(() => { - return loaderHelpers.fetch_like(url, options).then((res) => { - controller.fetchResponse = res; - controller.responseHeaderNames = []; - controller.responseHeaderValues = []; - if (res.headers && (res.headers).entries) { - const entries: Iterable = (res.headers).entries(); - - for (const pair of entries) { - controller.responseHeaderNames.push(pair[0]); - controller.responseHeaderValues.push(pair[1]); - } + return loaderHelpers.fetch_like(url, options); + }); + // avoid processing headers if the fetch is cancelled + controller.fetchResponsePromise.then((res) => { + controller.fetchResponse = res; + controller.responseHeaderNames = []; + controller.responseHeaderValues = []; + if (res.headers && (res.headers).entries) { + const entries: Iterable = (res.headers).entries(); + + for (const pair of entries) { + controller.responseHeaderNames.push(pair[0]); + controller.responseHeaderValues.push(pair[1]); } - return res; - }); + } }); return controller.fetchResponsePromise as ControllablePromise; } -export function http_wasm_get_response_header_names(controller: HttpController): string[] { - if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.responseHeaderNames; -} - export function http_wasm_get_response_type(controller: HttpController): string | undefined { if (BuildConfiguration === "Debug") commonAsserts(controller); return controller.fetchResponse?.type; @@ -179,8 +178,15 @@ export function http_wasm_get_response_status(controller: HttpController): numbe } +export function http_wasm_get_response_header_names(controller: HttpController): string[] { + if (BuildConfiguration === "Debug") commonAsserts(controller); + mono_assert(controller.responseHeaderNames, "expected responseHeaderNames"); + return controller.responseHeaderNames; +} + export function http_wasm_get_response_header_values(controller: HttpController): string[] { if (BuildConfiguration === "Debug") commonAsserts(controller); + mono_assert(controller.responseHeaderValues, "expected responseHeaderValues"); return controller.responseHeaderValues; } @@ -197,6 +203,7 @@ export function http_wasm_get_response_length(controller: HttpController): Contr export function http_wasm_get_response_bytes(controller: HttpController, view: Span): number { mono_assert(controller, "expected controller"); mono_assert(controller.responseBuffer, "expected resoved arrayBuffer"); + mono_assert(controller.currentBufferOffset != undefined, "expected currentBufferOffset"); if (controller.currentBufferOffset == controller.responseBuffer!.byteLength) { return 0; } @@ -212,6 +219,7 @@ export function http_wasm_get_streamed_response_bytes(controller: HttpController // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrap_as_cancelable_promise(async () => { + mono_assert(controller.currentBufferOffset != undefined, "expected currentBufferOffset"); if (!controller.streamReader) { controller.streamReader = controller.fetchResponse!.body!.getReader(); } @@ -247,14 +255,14 @@ interface HttpController { // response fetchResponsePromise?: ControllablePromise fetchResponse?: Response - responseHeaderNames: string[]; - responseHeaderValues: string[]; - currentBufferOffset: number + responseHeaderNames?: string[]; + responseHeaderValues?: string[]; + currentBufferOffset?: number // non-streaming response responseBuffer?: ArrayBuffer // streaming response - streamWriter: WritableStreamDefaultWriter + streamWriter?: WritableStreamDefaultWriter currentStreamReaderChunk?: ReadableStreamReadResult } From 91c748a99c27a3604a570eb27088c742c1e2c241 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 13:26:18 +0100 Subject: [PATCH 08/17] feedback --- src/mono/browser/runtime/http.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index ad7073994dccce..75743b7f2cac56 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -55,7 +55,7 @@ export function http_wasm_supports_streaming_response(): boolean { export function http_wasm_create_controller(): HttpController { verifyEnvironment(); assert_js_interop(); - const controller: Partial = { + const controller: HttpController = { abortController: new AbortController() }; return controller as HttpController; @@ -151,7 +151,7 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ return loaderHelpers.fetch_like(url, options); }); // avoid processing headers if the fetch is cancelled - controller.fetchResponsePromise.then((res) => { + controller.fetchResponsePromise.then((res: Response) => { controller.fetchResponse = res; controller.responseHeaderNames = []; controller.responseHeaderValues = []; @@ -164,7 +164,7 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ } } }); - return controller.fetchResponsePromise as ControllablePromise; + return controller.fetchResponsePromise; } export function http_wasm_get_response_type(controller: HttpController): string | undefined { @@ -253,7 +253,7 @@ interface HttpController { streamReader?: ReadableStreamDefaultReader // response - fetchResponsePromise?: ControllablePromise + fetchResponsePromise?: ControllablePromise fetchResponse?: Response responseHeaderNames?: string[]; responseHeaderValues?: string[]; From 69ddae1f1839eabc03d75fe383e5341d6c7b30ab Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 19 Jan 2024 13:30:29 +0100 Subject: [PATCH 09/17] Update src/mono/browser/runtime/http.ts Co-authored-by: campersau --- src/mono/browser/runtime/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index 75743b7f2cac56..b1b8bc3477cd87 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -58,7 +58,7 @@ export function http_wasm_create_controller(): HttpController { const controller: HttpController = { abortController: new AbortController() }; - return controller as HttpController; + return controller; } export function http_wasm_abort_request(controller: HttpController): void { From 4e67c24350659d8dfa05b68a1fb2ad341fb3dbd1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 15:53:48 +0100 Subject: [PATCH 10/17] feedback --- .../BrowserHttpHandler/BrowserHttpHandler.cs | 6 ++-- .../BrowserHttpHandler/BrowserHttpInterop.cs | 18 +++-------- .../src/System/Net/Http/CancellationHelper.cs | 10 +++++- .../InteropServices/JavaScript/JSWebWorker.cs | 29 ++++++++++------- .../Marshaling/JSMarshalerArgument.Task.cs | 6 ++-- ...me.InteropServices.JavaScript.Tests.csproj | 3 -- src/mono/browser/runtime/http.ts | 32 +++++++++---------- 7 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs index 3f30e3c6a2c291..bbcd625d036d6a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs @@ -232,7 +232,7 @@ public BrowserHttpController(HttpRequestMessage request, bool? allowAutoRedirect public async Task CallFetch() { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationHelper.ThrowIfCancellationRequested(_cancellationToken); BrowserHttpWriteStream? writeStream = null; Task fetchPromise; @@ -258,7 +258,7 @@ public async Task CallFetch() else { byte[] buffer = await _request.Content.ReadAsByteArrayAsync(_cancellationToken).ConfigureAwait(false); - _cancellationToken.ThrowIfCancellationRequested(); + CancellationHelper.ThrowIfCancellationRequested(_cancellationToken); Memory bufferMemory = buffer.AsMemory(); // http_wasm_fetch_byte makes a copy of the bytes synchronously, so we can un-pin it synchronously @@ -366,7 +366,7 @@ public BrowserHttpWriteStream(BrowserHttpController controller) private Task WriteAsyncCore(ReadOnlyMemory buffer, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + CancellationHelper.ThrowIfCancellationRequested(cancellationToken); _controller.ThrowIfDisposed(); // http_wasm_transform_stream_write makes a copy of the bytes synchronously, so we can dispose the handle synchronously diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs index 4042a40d3ef14a..c37be3fc11f2bf 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs @@ -129,10 +129,8 @@ public static partial int GetResponseBytes( public static async Task CancellationHelper(Task promise, CancellationToken cancellationToken, JSObject jsController) { - if (cancellationToken.IsCancellationRequested) - { - throw Http.CancellationHelper.CreateOperationCanceledException(null, cancellationToken); - } + Http.CancellationHelper.ThrowIfCancellationRequested(cancellationToken); + if (promise.IsCompletedSuccessfully) { return; @@ -156,7 +154,7 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc } catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested) { - throw Http.CancellationHelper.CreateOperationCanceledException(oce, cancellationToken); + Http.CancellationHelper.ThrowIfCancellationRequested(oce, cancellationToken); } catch (JSException jse) { @@ -164,20 +162,14 @@ public static async Task CancellationHelper(Task promise, CancellationToken canc { throw Http.CancellationHelper.CreateOperationCanceledException(jse, CancellationToken.None); } - if (cancellationToken.IsCancellationRequested) - { - throw Http.CancellationHelper.CreateOperationCanceledException(jse, cancellationToken); - } + Http.CancellationHelper.ThrowIfCancellationRequested(jse, cancellationToken); throw new HttpRequestException(jse.Message, jse); } } public static async Task CancellationHelper(Task promise, CancellationToken cancellationToken, JSObject jsController) { - if (cancellationToken.IsCancellationRequested) - { - throw Http.CancellationHelper.CreateOperationCanceledException(null, cancellationToken); - } + Http.CancellationHelper.ThrowIfCancellationRequested(cancellationToken); if (promise.IsCompletedSuccessfully) { return promise.Result; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CancellationHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CancellationHelper.cs index e6d18e4a67e0db..cf6fdd6b5f48fe 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CancellationHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CancellationHelper.cs @@ -35,10 +35,18 @@ private static void ThrowOperationCanceledException(Exception? innerException, C /// Throws a cancellation exception if cancellation has been requested via . /// The token to check for a cancellation request. internal static void ThrowIfCancellationRequested(CancellationToken cancellationToken) + { + ThrowIfCancellationRequested(innerException: null, cancellationToken); + } + + /// Throws a cancellation exception if cancellation has been requested via . + /// The inner exception to wrap. May be null. + /// The token to check for a cancellation request. + internal static void ThrowIfCancellationRequested(Exception? innerException, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { - ThrowOperationCanceledException(innerException: null, cancellationToken); + ThrowOperationCanceledException(innerException, cancellationToken); } } } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs index fe569c515d4d6e..69242c0b75bf76 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSWebWorker.cs @@ -48,23 +48,28 @@ public static Task RunAsync(Func body, CancellationToken cancellationToken internal sealed class JSWebWorkerInstance : IDisposable { - private JSSynchronizationContext? _jsSynchronizationContext; - private TaskCompletionSource _taskCompletionSource; - private Thread _thread; - private CancellationToken _cancellationToken; + private readonly TaskCompletionSource _taskCompletionSource; + private readonly Thread _thread; + private readonly CancellationToken _cancellationToken; + private readonly Func> _body; + private CancellationTokenRegistration? _cancellationRegistration; - private Func> _body; + private JSSynchronizationContext? _jsSynchronizationContext; private Task? _resultTask; private bool _isDisposed; - public JSWebWorkerInstance(Func> bodyRes, CancellationToken cancellationToken) + public JSWebWorkerInstance(Func> body, CancellationToken cancellationToken) { - _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.DenyChildAttach | TaskCreationOptions.RunContinuationsAsynchronously | TaskCreationOptions.HideScheduler); + // Task created from this TCS is consumed by external caller, on outer thread. + // We don't want the continuations of that task to run on JSWebWorker + // only the tasks created inside of the callback should run in JSWebWorker + // TODO TaskCreationOptions.HideScheduler ? + _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _thread = new Thread(ThreadMain); _resultTask = null; _cancellationToken = cancellationToken; _cancellationRegistration = null; - _body = bodyRes; + _body = body; JSHostImplementation.SetHasExternalEventLoop(_thread); } @@ -84,10 +89,11 @@ public Task Start() } if (t.IsCanceled) { - throw new OperationCanceledException(self._cancellationToken); + throw new OperationCanceledException("Cancelled while waiting for underlying WebWorker to become available.", self._cancellationToken); } throw t.Exception!; - }, this, _cancellationToken, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); + // ideally this will execute on UI thread quickly: ExecuteSynchronously + }, this, _cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()); } else { @@ -171,6 +177,7 @@ private void PropagateCompletionAndDispose(Task result) Dispose(); } + // TODO use https://github.com/dotnet/runtime/pull/97077 private void PropagateCompletion() { if (_resultTask!.IsFaulted) @@ -186,7 +193,7 @@ private void PropagateCompletion() } else if (_resultTask.IsCanceled) { - _taskCompletionSource.TrySetCanceled(); + _taskCompletionSource.TrySetCanceled(_cancellationToken); } else { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs index c788d4dc2d9a9f..9c048849e92e73 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs @@ -49,7 +49,8 @@ public unsafe void ToManaged(out Task? value) lock (ctx) { PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle); - TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.RunContinuationsAsynchronously); + // we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously + TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { if (arguments_buffer == null) @@ -98,7 +99,8 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback lock (ctx) { var holder = ctx.GetPromiseHolder(slot.GCHandle); - TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.RunContinuationsAsynchronously); + // we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously + TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { if (arguments_buffer == null) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj index dbb37ba5da2c15..1ddadf3c3acea9 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj @@ -1,8 +1,5 @@ - $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) true diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index b1b8bc3477cd87..fc23b1b9950888 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -89,10 +89,10 @@ export function http_wasm_transform_stream_write(controller: HttpController, buf const copy = view.slice() as Uint8Array; return wrap_as_cancelable_promise(async () => { mono_assert(controller.streamWriter, "expected streamWriter"); - mono_assert(controller.fetchResponsePromise, "expected fetch promise"); + mono_assert(controller.responsePromise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 - await Promise.race([controller.streamWriter.ready, controller.fetchResponsePromise]); - await Promise.race([controller.streamWriter.write(copy), controller.fetchResponsePromise]); + await Promise.race([controller.streamWriter.ready, controller.responsePromise]); + await Promise.race([controller.streamWriter.write(copy), controller.responsePromise]); }); } @@ -100,10 +100,10 @@ export function http_wasm_transform_stream_close(controller: HttpController): Co mono_assert(controller, "expected controller"); return wrap_as_cancelable_promise(async () => { mono_assert(controller.streamWriter, "expected streamWriter"); - mono_assert(controller.fetchResponsePromise, "expected fetch promise"); + mono_assert(controller.responsePromise, "expected fetch promise"); // race with fetch because fetch does not cancel the ReadableStream see https://bugs.chromium.org/p/chromium/issues/detail?id=1480250 - await Promise.race([controller.streamWriter.ready, controller.fetchResponsePromise]); - await Promise.race([controller.streamWriter.close(), controller.fetchResponsePromise]); + await Promise.race([controller.streamWriter.ready, controller.responsePromise]); + await Promise.race([controller.streamWriter.close(), controller.responsePromise]); }); } @@ -147,12 +147,12 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ options[option_names[i]] = option_values[i]; } // make the fetch cancellable - controller.fetchResponsePromise = wrap_as_cancelable_promise(() => { + controller.responsePromise = wrap_as_cancelable_promise(() => { return loaderHelpers.fetch_like(url, options); }); // avoid processing headers if the fetch is cancelled - controller.fetchResponsePromise.then((res: Response) => { - controller.fetchResponse = res; + controller.responsePromise.then((res: Response) => { + controller.response = res; controller.responseHeaderNames = []; controller.responseHeaderValues = []; if (res.headers && (res.headers).entries) { @@ -164,17 +164,17 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ } } }); - return controller.fetchResponsePromise; + return controller.responsePromise; } export function http_wasm_get_response_type(controller: HttpController): string | undefined { if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.fetchResponse?.type; + return controller.response?.type; } export function http_wasm_get_response_status(controller: HttpController): number { if (BuildConfiguration === "Debug") commonAsserts(controller); - return controller.fetchResponse?.status ?? 0; + return controller.response?.status ?? 0; } @@ -193,7 +193,7 @@ export function http_wasm_get_response_header_values(controller: HttpController) export function http_wasm_get_response_length(controller: HttpController): ControllablePromise { if (BuildConfiguration === "Debug") commonAsserts(controller); return wrap_as_cancelable_promise(async () => { - const buffer = await controller.fetchResponse!.arrayBuffer(); + const buffer = await controller.response!.arrayBuffer(); controller.responseBuffer = buffer; controller.currentBufferOffset = 0; return buffer.byteLength; @@ -221,7 +221,7 @@ export function http_wasm_get_streamed_response_bytes(controller: HttpController return wrap_as_cancelable_promise(async () => { mono_assert(controller.currentBufferOffset != undefined, "expected currentBufferOffset"); if (!controller.streamReader) { - controller.streamReader = controller.fetchResponse!.body!.getReader(); + controller.streamReader = controller.response!.body!.getReader(); } if (!controller.currentStreamReaderChunk) { controller.currentStreamReaderChunk = await controller.streamReader.read(); @@ -253,8 +253,8 @@ interface HttpController { streamReader?: ReadableStreamDefaultReader // response - fetchResponsePromise?: ControllablePromise - fetchResponse?: Response + responsePromise?: ControllablePromise + response?: Response responseHeaderNames?: string[]; responseHeaderValues?: string[]; currentBufferOffset?: number From a5f6aed407d297e4197550a6d12b5e25a17a4d9b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 16:04:12 +0100 Subject: [PATCH 11/17] test cleanup --- .../JavaScript/WebWorkerTest.cs | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index 8a6fac2a924736..0f2d794804db9c 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -51,7 +51,8 @@ public async Task InitializeAsync() private CancellationTokenSource CreateTestCaseTimeoutSource() { var cts = new CancellationTokenSource(TimeoutMilliseconds); - cts.Token.Register(() => { + cts.Token.Register(() => + { Console.WriteLine($"Unexpected test case timeout at {DateTime.Now.ToString("u")} ManagedThreadId:{Environment.CurrentManagedThreadId}"); }); return cts; @@ -78,7 +79,7 @@ public static IEnumerable GetTargetThreads2x() [Theory, MemberData(nameof(GetTargetThreads))] public async Task Executor_Cancellation(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); TaskCompletionSource ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var canceledTask = executor.Execute(() => @@ -98,7 +99,7 @@ public async Task Executor_Cancellation(Executor executor) [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSDelay_Cancellation(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); TaskCompletionSource ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var canceledTask = executor.Execute(async () => { @@ -119,9 +120,9 @@ public async Task JSDelay_Cancellation(Executor executor) [Fact] public async Task JSSynchronizationContext_Send_Post_Items_Cancellation() { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); - ManualResetEventSlim blocker=new ManualResetEventSlim(false); + ManualResetEventSlim blocker = new ManualResetEventSlim(false); TaskCompletionSource never = new TaskCompletionSource(); SynchronizationContext capturedSynchronizationContext = null; TaskCompletionSource jswReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -191,12 +192,12 @@ public async Task JSSynchronizationContext_Send_Post_Items_Cancellation() [Fact] public async Task JSSynchronizationContext_Send_Post_To_Canceled() { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); TaskCompletionSource never = new TaskCompletionSource(); SynchronizationContext capturedSynchronizationContext = null; TaskCompletionSource jswReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - JSObject capturedGlobalThis=null; + JSObject capturedGlobalThis = null; var canceledTask = JSWebWorker.RunAsync(() => { @@ -249,7 +250,7 @@ public async Task JSSynchronizationContext_Send_Post_To_Canceled() [Fact] public async Task JSWebWorker_Abandon_Running() { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); TaskCompletionSource never = new TaskCompletionSource(); TaskCompletionSource ready = new TaskCompletionSource(); @@ -274,7 +275,7 @@ public async Task JSWebWorker_Abandon_Running() [Fact] public async Task JSWebWorker_Abandon_Running_JS() { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); TaskCompletionSource ready = new TaskCompletionSource(); @@ -300,7 +301,7 @@ public async Task JSWebWorker_Abandon_Running_JS() [Theory, MemberData(nameof(GetTargetThreads))] public async Task Executor_Propagates(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); bool hit = false; var failedTask = executor.Execute(() => { @@ -320,7 +321,7 @@ public async Task Executor_Propagates(Executor executor) [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedConsole(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(() => { Console.WriteLine("C# Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId); @@ -331,7 +332,7 @@ await executor.Execute(() => [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSConsole(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(() => { WebWorkerTestHelper.Log("JS Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); @@ -342,7 +343,7 @@ await executor.Execute(() => [Theory, MemberData(nameof(GetTargetThreads))] public async Task NativeThreadId(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await executor.StickyAwait(WebWorkerTestHelper.InitializeAsync(), cts.Token); @@ -366,7 +367,7 @@ await executor.Execute(async () => public async Task ThreadingTimer(Executor executor) { var hit = false; - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { TaskCompletionSource tcs = new TaskCompletionSource(); @@ -388,7 +389,7 @@ await executor.Execute(async () => [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSDelay_ContinueWith(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); @@ -404,7 +405,7 @@ await WebWorkerTestHelper.JSDelay(10).ContinueWith(_ => [Theory, MemberData(nameof(GetTargetThreads))] public async Task JSDelay_ConfigureAwait_True(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await executor.StickyAwait(WebWorkerTestHelper.CreateDelay(), cts.Token); @@ -419,7 +420,7 @@ await executor.Execute(async () => public async Task ManagedDelay_ContinueWith(Executor executor) { var hit = false; - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await Task.Delay(10, cts.Token).ContinueWith(_ => @@ -433,7 +434,7 @@ await Task.Delay(10, cts.Token).ContinueWith(_ => [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedDelay_ConfigureAwait_True(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await Task.Delay(10, cts.Token).ConfigureAwait(true); @@ -445,7 +446,7 @@ await executor.Execute(async () => [Theory, MemberData(nameof(GetTargetThreads))] public async Task ManagedYield(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => { await Task.Yield(); @@ -497,7 +498,7 @@ private async Task ActionsInDifferentThreads(Executor executor1, Executor exe [Theory, MemberData(nameof(GetTargetThreads2x))] public async Task JSObject_CapturesAffinity(Executor executor1, Executor executor2) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); var e1Job = async (Task e2done, TaskCompletionSource e1State) => { @@ -532,7 +533,7 @@ public async Task JSObject_CapturesAffinity(Executor executor1, Executor executo [Theory, MemberData(nameof(GetTargetThreads))] public async Task WebSocketClient_ContentInSameThread(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); var message = "hello"; @@ -557,7 +558,7 @@ await executor.Execute(async () => [Theory, MemberData(nameof(GetTargetThreads2x))] public Task WebSocketClient_ResponseCloseInDifferentThread(Executor executor1, Executor executor2) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); var message = "hello"; @@ -592,7 +593,7 @@ public Task WebSocketClient_ResponseCloseInDifferentThread(Executor executor1, E [Theory, MemberData(nameof(GetTargetThreads2x))] public Task WebSocketClient_CancelInDifferentThread(Executor executor1, Executor executor2) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = new CancellationTokenSource(); var uri = new Uri(WebWorkerTestHelper.LocalWsEcho + "?guid=" + Guid.NewGuid()); var message = ".delay5sec"; // this will make the loopback server slower @@ -629,7 +630,7 @@ public Task WebSocketClient_CancelInDifferentThread(Executor executor1, Executor [Theory, MemberData(nameof(GetTargetThreads))] public async Task HttpClient_ContentInSameThread(Executor executor) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); var uri = WebWorkerTestHelper.GetOriginUrl() + "/_framework/blazor.boot.json"; await executor.Execute(async () => @@ -649,7 +650,7 @@ await executor.Execute(async () => private Task HttpClient_ActionInDifferentThread(string url, Executor executor1, Executor executor2, Func e2Job) { - var cts = new CancellationTokenSource(TimeoutMilliseconds); + var cts = CreateTestCaseTimeoutSource(); var e1Job = async (Task e2done, TaskCompletionSource e1State) => { @@ -689,9 +690,9 @@ public async Task HttpClient_CancelInDifferentThread(Executor executor1, Executo var url = WebWorkerTestHelper.LocalHttpEcho + "?delay10sec=true&guid=" + Guid.NewGuid(); await HttpClient_ActionInDifferentThread(url, executor1, executor2, async (HttpResponseMessage response) => { - CancellationTokenSource cts = new CancellationTokenSource(); await Assert.ThrowsAsync(async () => { + CancellationTokenSource cts = new CancellationTokenSource(); var promise = response.Content.ReadAsStringAsync(cts.Token); cts.Cancel(); await promise; From bef203ec1dcc87bc9cd2f7ec5d7648e246e5b973 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 17:53:39 +0100 Subject: [PATCH 12/17] fix --- .gitattributes | 2 +- src/mono/browser/runtime/http.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitattributes b/.gitattributes index 55a35b1afffff0..2aa4cb2e2a9e2b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -77,4 +77,4 @@ src/tests/JIT/Performance/CodeQuality/BenchmarksGame/reverse-complement/revcomp- src/tests/JIT/Performance/CodeQuality/BenchmarksGame/reverse-complement/revcomp-input25000.txt text eol=lf src/tests/JIT/Performance/CodeQuality/BenchmarksGame/k-nucleotide/knucleotide-input.txt text eol=lf src/tests/JIT/Performance/CodeQuality/BenchmarksGame/k-nucleotide/knucleotide-input-big.txt text eol=lf -src/mono/wasm/runtime/dotnet.d.ts text eol=lf +src/mono/browser/runtime/dotnet.d.ts text eol=lf diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index fc23b1b9950888..a679f2f0a66bed 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -219,15 +219,15 @@ export function http_wasm_get_streamed_response_bytes(controller: HttpController // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrap_as_cancelable_promise(async () => { - mono_assert(controller.currentBufferOffset != undefined, "expected currentBufferOffset"); + mono_assert(controller.response, "expected response"); if (!controller.streamReader) { - controller.streamReader = controller.response!.body!.getReader(); + controller.streamReader = controller.response.body!.getReader(); } - if (!controller.currentStreamReaderChunk) { + if (!controller.currentStreamReaderChunk || controller.currentBufferOffset === undefined) { controller.currentStreamReaderChunk = await controller.streamReader.read(); controller.currentBufferOffset = 0; } - if (controller.currentStreamReaderChunk!.done) { + if (controller.currentStreamReaderChunk.done) { return 0; } From 3bbefbd70cafdef304dfcfc23af85d3ae597338b Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 19:02:39 +0100 Subject: [PATCH 13/17] fix --- src/mono/browser/runtime/http.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index a679f2f0a66bed..bf29d8e0eaa142 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -163,6 +163,8 @@ export function http_wasm_fetch(controller: HttpController, url: string, header_ controller.responseHeaderValues.push(pair[1]); } } + }).catch(() => { + // ignore }); return controller.responsePromise; } From 758243b1a8f0b0869ede720d64cb8ff6aeee8ef6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 19 Jan 2024 19:42:00 +0100 Subject: [PATCH 14/17] postpone RunContinuationsAsynchronously --- .../JavaScript/Marshaling/JSMarshalerArgument.Task.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs index 9c048849e92e73..d00636cbbfea7d 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Marshaling/JSMarshalerArgument.Task.cs @@ -50,7 +50,8 @@ public unsafe void ToManaged(out Task? value) { PromiseHolder holder = ctx.GetPromiseHolder(slot.GCHandle); // we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously - TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously); + // TODO TaskCreationOptions.RunContinuationsAsynchronously + TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { if (arguments_buffer == null) @@ -100,7 +101,8 @@ public unsafe void ToManaged(out Task? value, ArgumentToManagedCallback { var holder = ctx.GetPromiseHolder(slot.GCHandle); // we want to run the continuations on the original thread which called the JSImport, so RunContinuationsAsynchronously, rather than ExecuteSynchronously - TaskCompletionSource tcs = new TaskCompletionSource(holder, TaskCreationOptions.RunContinuationsAsynchronously); + // TODO TaskCreationOptions.RunContinuationsAsynchronously + TaskCompletionSource tcs = new TaskCompletionSource(holder); ToManagedCallback callback = (JSMarshalerArgument* arguments_buffer) => { if (arguments_buffer == null) From b924f1cdb3c8bc3e7b56e40bd6efb4a65e831fa5 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Sat, 20 Jan 2024 11:01:55 +0100 Subject: [PATCH 15/17] fix --- .../Runtime/InteropServices/JavaScript/WebWorkerTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index 0f2d794804db9c..c0c03addd6bb87 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -93,7 +93,7 @@ public async Task Executor_Cancellation(Executor executor) cts.Cancel(); - await Assert.ThrowsAsync(() => canceledTask); + await Assert.ThrowsAnyAsync(() => canceledTask); } [Theory, MemberData(nameof(GetTargetThreads))] @@ -114,7 +114,7 @@ public async Task JSDelay_Cancellation(Executor executor) cts.Cancel(); - await Assert.ThrowsAsync(() => canceledTask); + await Assert.ThrowsAnyAsync(() => canceledTask); } [Fact] @@ -181,7 +181,7 @@ public async Task JSSynchronizationContext_Send_Post_Items_Cancellation() // this will unblock the current pending work item blocker.Set(); - await Assert.ThrowsAsync(() => canceledSend); + await Assert.ThrowsAnyAsync(() => canceledSend); await canceledPost; // this shouldn't throw Assert.False(shouldNotHitSend); @@ -616,7 +616,7 @@ public Task WebSocketClient_CancelInDifferentThread(Executor executor1, Executor CancellationTokenSource cts2 = new CancellationTokenSource(); var resTask = client.ReceiveAsync(receive, cts2.Token); cts2.Cancel(); - var ex = await Assert.ThrowsAsync(() => resTask); + var ex = await Assert.ThrowsAnyAsync(() => resTask); Assert.Equal(cts2.Token, ex.CancellationToken); }; From 3e989ca198c3ab23b3001389c168ed8a54efb2a2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Sat, 20 Jan 2024 18:31:28 +0100 Subject: [PATCH 16/17] ActiveIssue https://github.com/dotnet/runtime/issues/96628 --- src/libraries/tests.proj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 523467c9503a2d..2595b93e91b4b8 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -604,9 +604,11 @@ +