diff --git a/eng/illink.targets b/eng/illink.targets
index 0f0c3cab3c45d7..5304ecd8d1eb34 100644
--- a/eng/illink.targets
+++ b/eng/illink.targets
@@ -20,8 +20,6 @@
$(IntermediateOutputPath)
$(ILLinkDirectory)ILLink.Descriptors.xml
-
- $(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml
$(IntermediateOutputPath)ILLink.Descriptors.xml
$(IntermediateOutputPath)ILLink.Substitutions.xml
@@ -41,6 +39,9 @@
+
+
@@ -210,7 +211,7 @@
$(ILLinkArgs) -b true
$(ILLinkArgs) --preserve-symbol-paths
- $(ILLinkArgs) -x "$(ILLinkDescriptorsLibraryBuildXml)"
+ $(ILLinkArgs) -x "@(ILLinkDescriptorsLibraryBuildXml->'%(FullPath)', '" -x "')"
$(ILLinkArgs) --substitutions "$(ILLinkSubstitutionsLibraryBuildXml)"
$(ILLinkArgs) --link-attributes "@(ILLinkSuppressionsLibraryBuildXml->'%(FullPath)', '" --link-attributes "')"
+
+ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
+ $(DefineConstants);TARGET_BROWSER
+ $(DefineConstants);TARGET_WASI
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/WasiHttpHandler/WasiHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/WasiHttpHandler/WasiHttpHandler.cs
index ee45aebef883e4..dcccb989910960 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/WasiHttpHandler/WasiHttpHandler.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/WasiHttpHandler/WasiHttpHandler.cs
@@ -314,7 +314,7 @@ ITypes.OutgoingRequest request
}
else
{
- await WasiEventLoop.RegisterWasiPollable(future.Subscribe()).ConfigureAwait(false);
+ await RegisterWasiPollable(future.Subscribe()).ConfigureAwait(false);
}
}
}
@@ -461,19 +461,22 @@ private static async Task SendContentAsync(HttpContent? content, Stream stream)
}
}
- private static class WasiEventLoop
+ private static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
- internal static Task RegisterWasiPollable(IPoll.Pollable pollable)
- {
- var handle = pollable.Handle;
- pollable.Handle = 0;
- return CallRegisterWasiPollable((Thread)null!, handle);
+ var handle = pollable.Handle;
+
+ // this will effectively neutralize Dispose() of the Pollable()
+ // because in the CoreLib we create another instance, which will dispose it
+ pollable.Handle = 0;
+ GC.SuppressFinalize(pollable);
+
+ return CallRegisterWasiPollableHandle((Thread)null!, handle);
- [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RegisterWasiPollable")]
- static extern Task CallRegisterWasiPollable(Thread t, int handle);
- }
}
+ [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RegisterWasiPollableHandle")]
+ private static extern Task CallRegisterWasiPollableHandle(Thread t, int handle);
+
private sealed class InputStream : Stream
{
private ITypes.IncomingBody body;
@@ -559,8 +562,7 @@ CancellationToken cancellationToken
var buffer = result;
if (buffer.Length == 0)
{
- await WasiEventLoop
- .RegisterWasiPollable(stream.Subscribe())
+ await RegisterWasiPollable(stream.Subscribe())
.ConfigureAwait(false);
}
else
@@ -697,7 +699,7 @@ CancellationToken cancellationToken
var count = (int)stream.CheckWrite();
if (count == 0)
{
- await WasiEventLoop.RegisterWasiPollable(stream.Subscribe()).ConfigureAwait(false);
+ await RegisterWasiPollable(stream.Subscribe()).ConfigureAwait(false);
}
else if (offset == limit)
{
diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.WASI.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.WASI.xml
new file mode 100644
index 00000000000000..435d0ef7bdfc70
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.WASI.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index e9a354d8fcc8aa..032299180e78d9 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -64,10 +64,9 @@
+
+
-
- $(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.xml
-
@@ -2804,6 +2803,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Unix.cs
index 2535f8a4139668..359c889c975b87 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Unix.cs
@@ -5,6 +5,7 @@
using System.Runtime;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
+using System.Threading.Tasks;
namespace System.Threading
{
@@ -12,15 +13,26 @@ public sealed partial class Thread
{
// these methods are temporarily accessed via UnsafeAccessor from generated code until we have it in public API, probably in WASI preview3 and promises
#if TARGET_WASI
- internal static System.Threading.Tasks.Task RegisterWasiPollable(int handle)
+ internal static System.Threading.Tasks.Task RegisterWasiPollableHandle(int handle)
{
- return WasiEventLoop.RegisterWasiPollable(handle);
+ return WasiEventLoop.RegisterWasiPollableHandle(handle);
}
- internal static void DispatchWasiEventLoop()
+ internal static int PollWasiEventLoopUntilResolved(Task mainTask)
{
- WasiEventLoop.DispatchWasiEventLoop();
+ while (!mainTask.IsCompleted)
+ {
+ WasiEventLoop.DispatchWasiEventLoop();
+ }
+ var exception = mainTask.Exception;
+ if (exception is not null)
+ {
+ throw exception;
+ }
+
+ return mainTask.Result;
}
+
#endif
// the closest analog to Sleep(0) on Unix is sched_yield
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiEventLoop.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiEventLoop.cs
index b4e1fda158c6ff..379c5c001c08a6 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiEventLoop.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiEventLoop.cs
@@ -9,43 +9,64 @@ namespace System.Threading
{
internal static class WasiEventLoop
{
- private static List<(IPoll.Pollable, TaskCompletionSource)> pollables = new();
+ private static List> s_pollables = new();
- internal static Task RegisterWasiPollable(int handle)
+ internal static Task RegisterWasiPollableHandle(int handle)
{
- var source = new TaskCompletionSource(TaskCreationOptions.AttachedToParent);
- pollables.Add((new IPoll.Pollable(new IPoll.Pollable.THandle(handle)), source));
- return source.Task;
+ // note that this is duplicate of the original Pollable
+ // the original should be neutralized without disposing the handle
+ var pollableCpy = new IPoll.Pollable(new IPoll.Pollable.THandle(handle));
+ return RegisterWasiPollable(pollableCpy);
+ }
+
+ internal static Task RegisterWasiPollable(IPoll.Pollable pollable)
+ {
+ var tcs = new TaskCompletionSource(pollable);
+ var weakRef = new WeakReference(tcs);
+ s_pollables.Add(weakRef);
+ return tcs.Task;
}
internal static void DispatchWasiEventLoop()
{
ThreadPoolWorkQueue.Dispatch();
- if (WasiEventLoop.pollables.Count > 0)
+ if (s_pollables.Count > 0)
{
- var pollables = WasiEventLoop.pollables;
- WasiEventLoop.pollables = new();
- var arguments = new List();
- var sources = new List();
- foreach ((var pollable, var source) in pollables)
+ var pollables = s_pollables;
+ s_pollables = new List>(pollables.Count);
+ var arguments = new List(pollables.Count);
+ var indexes = new List(pollables.Count);
+ for (var i = 0; i < pollables.Count; i++)
{
- arguments.Add(pollable);
- sources.Add(source);
+ var weakRef = pollables[i];
+ if (weakRef.TryGetTarget(out TaskCompletionSource? tcs))
+ {
+ var pollable = (IPoll.Pollable)tcs!.Task.AsyncState!;
+ arguments.Add(pollable);
+ indexes.Add(i);
+ }
}
- var results = PollInterop.Poll(arguments);
+
+ // this is blocking until at least one pollable resolves
+ var readyIndexes = PollInterop.Poll(arguments);
+
var ready = new bool[arguments.Count];
- foreach (var result in results)
+ foreach (int readyIndex in readyIndexes)
{
- ready[result] = true;
- arguments[(int)result].Dispose();
- sources[(int)result].SetResult();
+ ready[readyIndex] = true;
+ arguments[readyIndex].Dispose();
+ var weakRef = pollables[indexes[readyIndex]];
+ if (weakRef.TryGetTarget(out TaskCompletionSource? tcs))
+ {
+ tcs!.SetResult();
+ }
}
for (var i = 0; i < arguments.Count; ++i)
{
if (!ready[i])
{
- WasiEventLoop.pollables.Add((arguments[i], sources[i]));
+ s_pollables.Add(pollables[indexes[i]]);
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiPollWorld.wit.imports.wasi.clocks.v0_2_1.MonotonicClockInterop.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiPollWorld.wit.imports.wasi.clocks.v0_2_1.MonotonicClockInterop.cs
new file mode 100644
index 00000000000000..1a0ef026540eb5
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/WasiPollWorld.wit.imports.wasi.clocks.v0_2_1.MonotonicClockInterop.cs
@@ -0,0 +1,81 @@
+// Generated by `wit-bindgen` 0.29.0. DO NOT EDIT!
+//
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Collections;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace WasiPollWorld.wit.imports.wasi.clocks.v0_2_1
+{
+ internal static class MonotonicClockInterop {
+
+ internal static class NowWasmInterop
+ {
+ [DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "now"), WasmImportLinkage]
+ internal static extern long wasmImportNow();
+
+ }
+
+ internal static unsafe ulong Now()
+ {
+ var result = NowWasmInterop.wasmImportNow();
+ return unchecked((ulong)(result));
+
+ //TODO: free alloc handle (interopString) if exists
+ }
+
+ internal static class ResolutionWasmInterop
+ {
+ [DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "resolution"), WasmImportLinkage]
+ internal static extern long wasmImportResolution();
+
+ }
+
+ internal static unsafe ulong Resolution()
+ {
+ var result = ResolutionWasmInterop.wasmImportResolution();
+ return unchecked((ulong)(result));
+
+ //TODO: free alloc handle (interopString) if exists
+ }
+
+ internal static class SubscribeInstantWasmInterop
+ {
+ [DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "subscribe-instant"), WasmImportLinkage]
+ internal static extern int wasmImportSubscribeInstant(long p0);
+
+ }
+
+ internal static unsafe global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable SubscribeInstant(ulong when)
+ {
+ var result = SubscribeInstantWasmInterop.wasmImportSubscribeInstant(unchecked((long)(when)));
+ var resource = new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable(new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable.THandle(result));
+ return resource;
+
+ //TODO: free alloc handle (interopString) if exists
+ }
+
+ internal static class SubscribeDurationWasmInterop
+ {
+ [DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "subscribe-duration"), WasmImportLinkage]
+ internal static extern int wasmImportSubscribeDuration(long p0);
+
+ }
+
+ internal static unsafe global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable SubscribeDuration(ulong when)
+ {
+ var result = SubscribeDurationWasmInterop.wasmImportSubscribeDuration(unchecked((long)(when)));
+ var resource = new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable(new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable.THandle(result));
+ return resource;
+
+ //TODO: free alloc handle (interopString) if exists
+ }
+
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/generate-wasi-poll-bindings.sh b/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/generate-wasi-poll-bindings.sh
index bbc088167661e7..fd20df8e9df20f 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/generate-wasi-poll-bindings.sh
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Wasi/generate-wasi-poll-bindings.sh
@@ -17,6 +17,7 @@ tar xzf v0.2.1.tar.gz
cat >wasi-http-0.2.1/wit/world.wit <
+
diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs
index dce59b97046fd7..47795f22067067 100644
--- a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs
+++ b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Browser.Mono.cs
@@ -9,6 +9,9 @@
namespace System.Threading
{
+#if FEATURE_WASM_MANAGED_THREADS
+#error when compiled with FEATURE_WASM_MANAGED_THREADS, we use TimerQueue.Portable.cs
+#endif
//
// Browser-specific implementation of Timer
// Based on TimerQueue.Portable.cs
diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.Mono.cs
index 309f9df6c31b18..20c017cce1fe17 100644
--- a/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.Mono.cs
+++ b/src/mono/System.Private.CoreLib/src/System/Threading/TimerQueue.Wasi.Mono.cs
@@ -3,30 +3,155 @@
using System.Collections.Generic;
using System.Diagnostics;
-using System.Runtime.CompilerServices;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using WasiPollWorld.wit.imports.wasi.clocks.v0_2_1;
namespace System.Threading
{
+#if FEATURE_WASM_MANAGED_THREADS
+#error when compiled with FEATURE_WASM_MANAGED_THREADS, we will use TimerQueue.Portable.cs
+#endif
//
- // Wasi-specific implementation of Timer
+ // Wasi implementation of Timer, single-threaded, on top of pollable and wasi:clocks
// Based on TimerQueue.Portable.cs
// Not thread safe
//
- internal partial class TimerQueue
+ internal sealed partial class TimerQueue
{
private static long TickCount64 => Environment.TickCount64;
+ private static List? s_scheduledTimers;
+ private static List? s_scheduledTimersToFire;
+ private static long s_shortestDueTimeMs = long.MaxValue;
+ // this means that it's in the s_scheduledTimers collection, not that it's the one which would run on the next TimeoutCallback
+ private bool _isScheduled;
+ private long _scheduledDueTimeMs;
private TimerQueue(int _)
{
- throw new PlatformNotSupportedException();
}
-#pragma warning disable CA1822 // Mark members as static
+
+ private static void TimerHandler(object _)
+ {
+ try
+ {
+ s_shortestDueTimeMs = long.MaxValue;
+
+ long currentTimeMs = TickCount64;
+ SetNextTimer(PumpTimerQueue(currentTimeMs), currentTimeMs);
+ }
+ catch (Exception e)
+ {
+ Environment.FailFast("TimerQueue.TimerHandler failed", e);
+ }
+ }
+
+ // this is called with shortest of timers scheduled on the particular TimerQueue
private bool SetTimer(uint actualDuration)
{
- throw new PlatformNotSupportedException();
+ Debug.Assert((int)actualDuration >= 0);
+ long currentTimeMs = TickCount64;
+ if (!_isScheduled)
+ {
+ s_scheduledTimers ??= new List(Instances.Length);
+ s_scheduledTimersToFire ??= new List(Instances.Length);
+ s_scheduledTimers.Add(this);
+ _isScheduled = true;
+ }
+
+ _scheduledDueTimeMs = currentTimeMs + (int)actualDuration;
+
+ SetNextTimer(ShortestDueTime(), currentTimeMs);
+
+ return true;
+ }
+
+ // shortest time of all TimerQueues
+ private static unsafe void SetNextTimer(long shortestDueTimeMs, long currentTimeMs)
+ {
+ if (shortestDueTimeMs == long.MaxValue)
+ {
+ return;
+ }
+
+ // this also covers s_shortestDueTimeMs = long.MaxValue when none is scheduled
+ if (s_shortestDueTimeMs > shortestDueTimeMs)
+ {
+ s_shortestDueTimeMs = shortestDueTimeMs;
+ ulong shortestWaitMs = (ulong)Math.Max((long)(shortestDueTimeMs - currentTimeMs), 0);
+
+ // `SubscribeDuration` expects nanoseconds:
+ var pollable = MonotonicClockInterop.SubscribeDuration(shortestWaitMs * 1000 * 1000);
+ Task task = WasiEventLoop.RegisterWasiPollable(pollable);
+ task.ContinueWith(TimerHandler, TaskScheduler.Default);
+ }
+ }
+
+ private static long ShortestDueTime()
+ {
+ if (s_scheduledTimers == null)
+ {
+ return long.MaxValue;
+ }
+
+ long shortestDueTimeMs = long.MaxValue;
+ var timers = s_scheduledTimers!;
+ for (int i = timers.Count - 1; i >= 0; --i)
+ {
+ TimerQueue timer = timers[i];
+ if (timer._scheduledDueTimeMs < shortestDueTimeMs)
+ {
+ shortestDueTimeMs = timer._scheduledDueTimeMs;
+ }
+ }
+
+ return shortestDueTimeMs;
+ }
+
+ private static long PumpTimerQueue(long currentTimeMs)
+ {
+ if (s_scheduledTimersToFire == null)
+ {
+ return ShortestDueTime();
+ }
+
+ List timersToFire = s_scheduledTimersToFire!;
+ List timers;
+ timers = s_scheduledTimers!;
+ long shortestDueTimeMs = long.MaxValue;
+ for (int i = timers.Count - 1; i >= 0; --i)
+ {
+ TimerQueue timer = timers[i];
+ long waitDurationMs = timer._scheduledDueTimeMs - currentTimeMs;
+ if (waitDurationMs <= 0)
+ {
+ timer._isScheduled = false;
+ timersToFire.Add(timer);
+
+ int lastIndex = timers.Count - 1;
+ if (i != lastIndex)
+ {
+ timers[i] = timers[lastIndex];
+ }
+ timers.RemoveAt(lastIndex);
+ continue;
+ }
+
+ if (timer._scheduledDueTimeMs < shortestDueTimeMs)
+ {
+ shortestDueTimeMs = timer._scheduledDueTimeMs;
+ }
+ }
+
+ if (timersToFire.Count > 0)
+ {
+ foreach (TimerQueue timerToFire in timersToFire)
+ {
+ timerToFire.FireNextTimers();
+ }
+ timersToFire.Clear();
+ }
+
+ return shortestDueTimeMs;
}
-#pragma warning restore CA1822
}
}
diff --git a/src/mono/mono.proj b/src/mono/mono.proj
index 88ae8c08861970..b45f8150d251a2 100644
--- a/src/mono/mono.proj
+++ b/src/mono/mono.proj
@@ -346,11 +346,15 @@ JS_ENGINES = [NODE_JS]
-
-
+
MainAsync(string[] args)
{
using HttpClient client = new();
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.Accept.Clear();
- client.DefaultRequestHeaders.Accept.Add(
- new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
- client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
+ client.DefaultRequestHeaders.Add("User-Agent", "dotnet WASI unit test");
- var query="https://api.github.com/orgs/dotnet/repos?per_page=1";
+ var query="https://corefx-net-http11.azurewebsites.net/Echo.ashx";
var json = await client.GetStringAsync(query);
Console.WriteLine();
Console.WriteLine("GET "+query);
Console.WriteLine();
Console.WriteLine(json);
+
+ return 0;
}
- private static class WasiEventLoop
+ public static int Main(string[] args)
{
- internal static void DispatchWasiEventLoop()
- {
- CallDispatchWasiEventLoop((Thread)null!);
+ return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
- [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "DispatchWasiEventLoop")]
- static extern void CallDispatchWasiEventLoop(Thread t);
- }
+ [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
+ static extern int PollWasiEventLoopUntilResolved(Thread t, Task mainTask);
}
+
}
diff --git a/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs b/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs
index ae3fab94bbdc88..62f23c2648899c 100644
--- a/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs
+++ b/src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs
@@ -693,10 +693,14 @@ public static int Main()
.MultiplyWithSingleArgs(true, false) /*aot*/
.UnwrapItemsAsArrays();
- protected CommandResult RunWithoutBuild(string config, string id)
+ protected CommandResult RunWithoutBuild(string config, string id, bool enableHttp = false)
{
// wasmtime --wasi http is necessary because the default dotnet.wasm (without native rebuild depends on wasi:http world)
- string runArgs = $"run --no-build -c {config} --forward-exit-code --extra-host-arg=--wasi --extra-host-arg=http";
+ string runArgs = $"run --no-build -c {config} --forward-exit-code";
+ if (enableHttp)
+ {
+ runArgs += " --extra-host-arg=--wasi --extra-host-arg=http";
+ }
runArgs += " x y z";
int expectedExitCode = 42;
CommandResult res = new RunCommand(s_buildEnv, _testOutput, label: id)
diff --git a/src/mono/wasi/Wasi.Build.Tests/HttpTests.cs b/src/mono/wasi/Wasi.Build.Tests/HttpTests.cs
new file mode 100644
index 00000000000000..1b977ef9e39f25
--- /dev/null
+++ b/src/mono/wasi/Wasi.Build.Tests/HttpTests.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+using Wasm.Build.Tests;
+
+#nullable enable
+
+namespace Wasi.Build.Tests;
+
+public class HttpTests : BuildTestBase
+{
+ public HttpTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+ : base(output, buildContext)
+ {
+ }
+
+ [Theory]
+ [MemberData(nameof(TestDataForConsolePublishAndRun))]
+ public void HttpBuildThenRunThenPublish(string config, bool singleFileBundle, bool aot)
+ {
+ string id = $"{config}_{GetRandomId()}";
+ string projectFile = CreateWasmTemplateProject(id, "wasiconsole");
+ string projectName = Path.GetFileNameWithoutExtension(projectFile);
+ File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "Http.cs"), Path.Combine(_projectDir!, "Program.cs"), true);
+
+ var buildArgs = new BuildArgs(projectName, config, aot, id, null);
+ buildArgs = ExpandBuildArgs(buildArgs);
+
+ string extraProperties = "true";
+ if (aot)
+ extraProperties += "true<_WasmDevel>false";
+ if (singleFileBundle)
+ extraProperties += "true";
+ if (!string.IsNullOrEmpty(extraProperties))
+ AddItemsPropertiesToProject(projectFile, extraProperties);
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ DotnetWasmFromRuntimePack: true,
+ CreateProject: false,
+ Publish: false,
+ TargetFramework: BuildTestBase.DefaultTargetFramework));
+ RunWithoutBuild(config, id, true);
+
+ if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product))
+ throw new XunitException($"Test bug: could not get the build product in the cache");
+
+ File.Move(product!.LogFile, Path.ChangeExtension(product.LogFile!, ".first.binlog"));
+
+ _testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}");
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ DotnetWasmFromRuntimePack: true,
+ CreateProject: false,
+ Publish: true,
+ TargetFramework: BuildTestBase.DefaultTargetFramework,
+ UseCache: false,
+ ExpectSuccess: !(config == "Debug" && aot)));
+ }
+
+}
diff --git a/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs b/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs
index 5a909d54b13744..e1887ab525f086 100644
--- a/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs
+++ b/src/mono/wasi/Wasi.Build.Tests/WasiTemplateTests.cs
@@ -50,7 +50,7 @@ public void ConsoleBuildAndRunAOT(string config, bool singleFileBundle)
CreateProject: false,
Publish: false,
TargetFramework: BuildTestBase.DefaultTargetFramework));
- RunWithoutBuild(config, id);
+ RunWithoutBuild(config, id, true);
}
[Theory]
@@ -80,7 +80,7 @@ public void ConsoleBuildThenRunThenPublish(string config, bool singleFileBundle,
CreateProject: false,
Publish: false,
TargetFramework: BuildTestBase.DefaultTargetFramework));
- RunWithoutBuild(config, id);
+ RunWithoutBuild(config, id, true);
if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product))
throw new XunitException($"Test bug: could not get the build product in the cache");
diff --git a/src/mono/wasi/testassets/Http.cs b/src/mono/wasi/testassets/Http.cs
new file mode 100644
index 00000000000000..d8043bf017ca7e
--- /dev/null
+++ b/src/mono/wasi/testassets/Http.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Net.Http.Headers;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Runtime.CompilerServices;
+
+// keep in sync with src\mono\sample\wasi\http-p2\Program.cs
+public static class WasiMainWrapper
+{
+ public static async Task MainAsync(string[] args)
+ {
+ Console.WriteLine("Hello, Wasi Console!");
+ for (int i = 0; i < args.Length; i ++)
+ Console.WriteLine($"args[{i}] = {args[i]}");
+
+ using HttpClient client = new();
+ client.Timeout = Timeout.InfiniteTimeSpan;
+ client.DefaultRequestHeaders.Accept.Clear();
+ client.DefaultRequestHeaders.Add("User-Agent", "dotnet WASI unit test");
+
+ var query="https://corefx-net-http11.azurewebsites.net/Echo.ashx";
+ var json = await client.GetStringAsync(query);
+
+ Console.WriteLine();
+ Console.WriteLine("GET "+query);
+ Console.WriteLine();
+ Console.WriteLine(json);
+
+ return 42;
+ }
+
+ public static int Main(string[] args)
+ {
+ return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));
+
+ [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
+ static extern int PollWasiEventLoopUntilResolved(Thread t, Task mainTask);
+ }
+}