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); + } +}