diff --git a/eng/testing/scenarios/BuildWasmAppsJobsListCoreCLR.txt b/eng/testing/scenarios/BuildWasmAppsJobsListCoreCLR.txt index 1d66c27562b7bf..4f567f5cba1082 100644 --- a/eng/testing/scenarios/BuildWasmAppsJobsListCoreCLR.txt +++ b/eng/testing/scenarios/BuildWasmAppsJobsListCoreCLR.txt @@ -10,4 +10,5 @@ Wasm.Build.Tests.SatelliteLoadingTests Wasm.Build.Tests.WasmBuildAppTest Wasm.Build.Tests.WasmRunOutOfAppBundleTests Wasm.Build.Tests.WasmTemplateTests -Wasm.Build.Tests.MaxParallelDownloadsTests \ No newline at end of file +Wasm.Build.Tests.MaxParallelDownloadsTests +Wasm.Build.Tests.LibraryInitializerTests diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs index c6393044f3846e..4e993991471be5 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs @@ -64,6 +64,7 @@ public abstract class BuildTestBase : IClassFixture !s_buildEnv.IsWorkload; public static bool IsWorkloadWithMultiThreadingForDefaultFramework => s_buildEnv.IsWorkloadWithMultiThreadingForDefaultFramework; public static bool IsMonoRuntime => s_buildEnv.IsMonoRuntime; + public static bool IsCoreClrRuntime => s_buildEnv.IsCoreClrRuntime; public static bool UseWebcil => s_buildEnv.UseWebcil; public static string GetNuGetConfigPath() => Path.Combine(BuildEnvironment.TestDataPath, "nuget.config"); diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index dd190f9037cf12..dcb7014120803e 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -28,6 +28,7 @@ public class BuildEnvironment public bool UseWebcil { get; init; } public bool IsWorkloadWithMultiThreadingForDefaultFramework { get; init; } public bool IsMonoRuntime { get; init; } + public bool IsCoreClrRuntime { get; init; } public bool IsRunningOnCI => EnvironmentVariables.IsRunningOnCI; public static readonly string RelativeTestAssetsPath = @"..\testassets\"; @@ -114,6 +115,7 @@ public BuildEnvironment() UseWebcil = EnvironmentVariables.UseWebcil && EnvironmentVariables.RuntimeFlavor != "CoreCLR"; // TODO-WASM: CoreCLR support for Webcil https://github.com/dotnet/runtime/issues/120248 IsMonoRuntime = EnvironmentVariables.RuntimeFlavor == "Mono"; + IsCoreClrRuntime = EnvironmentVariables.RuntimeFlavor == "CoreCLR"; if (EnvironmentVariables.BuiltNuGetsPath is null || !Directory.Exists(EnvironmentVariables.BuiltNuGetsPath)) throw new Exception($"Cannot find 'BUILT_NUGETS_PATH={EnvironmentVariables.BuiltNuGetsPath}'"); @@ -141,7 +143,7 @@ public BuildEnvironment() EnvVars["WasmFingerprintAssets"] = "false"; } - if (EnvironmentVariables.RuntimeFlavor == "CoreCLR") + if (IsCoreClrRuntime) { EnvVars["WasmTestSupport"] = "true"; EnvVars["WasmTestExitOnUnhandledError"] = "true"; diff --git a/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs b/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs index f6cfb18468a370..2ef6e921a754a2 100644 --- a/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs @@ -40,9 +40,16 @@ public async Task LoadLibraryInitializer() [GeneratedRegex("MONO_WASM: Failed to invoke 'onRuntimeConfigLoaded' on library initializer '../WasmBasicTestApp.[a-z0-9]+.lib.module.js': Error: Error thrown from library initializer")] private static partial Regex AbortStartupOnErrorRegex { get; } + [GeneratedRegex("Aborting startup, reason: Error: Failed to invoke 'onRuntimeReady' on library initializer '(\\.\\./)?WasmBasicTestApp(\\.[a-z0-9]+)?\\.lib\\.module\\.js': Error thrown from library initializer")] + private static partial Regex AbortStartupOnErrorCoreClrRegex { get; } + [Fact, TestCategory("bundler-friendly")] public async Task AbortStartupOnError() { + bool isCoreClr = IsCoreClrRuntime; + string queryParam = isCoreClr ? "throwErrorOnReady" : "throwError"; + Regex expectedRegex = isCoreClr ? AbortStartupOnErrorCoreClrRegex : AbortStartupOnErrorRegex; + Configuration config = Configuration.Debug; ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "LibraryInitializerTests_AbortStartupOnError"); PublishProject(info, config); @@ -50,9 +57,11 @@ public async Task AbortStartupOnError() BrowserRunOptions options = new( config, TestScenario: "LibraryInitializerTest", - BrowserQueryString: new NameValueCollection { {"throwError", "true" } }, + BrowserQueryString: new NameValueCollection { { queryParam, "true" } }, ExpectedExitCode: 1); RunResult result = await RunForPublishWithWebServer(options); - Assert.True(result.ConsoleOutput.Any(m => AbortStartupOnErrorRegex.IsMatch(m)), "The library initializer test didn't emit expected error message"); + Assert.True( + result.ConsoleOutput.Any(m => expectedRegex.IsMatch(m)), + $"The library initializer test didn't emit expected error message.\nConsole output:\n{string.Join("\n", result.ConsoleOutput)}"); } } diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs index ccfd5c57babee1..ee91db28f23c87 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -124,7 +124,7 @@ protected ProjectInfo CopyTestAsset( """; } - if (EnvironmentVariables.RuntimeFlavor == "CoreCLR") + if (s_buildEnv.IsCoreClrRuntime) { // TODO-WASM: https://github.com/dotnet/sdk/issues/51213 string versionSuffix = s_buildEnv.IsRunningOnCI ? "ci" : "dev"; diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js index 200f17f6abaebf..f0a78e2efb3185 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js @@ -12,6 +12,10 @@ export function onRuntimeConfigLoaded(config) { } export async function onRuntimeReady({ getAssemblyExports, getConfig }) { + if (params.get("throwErrorOnReady") === "true") { + throw new Error("Error thrown from library initializer"); + } + const testCase = params.get("test"); if (testCase == "LibraryInitializerTest") { const config = getConfig(); diff --git a/src/native/libs/Common/JavaScript/loader/exit.ts b/src/native/libs/Common/JavaScript/loader/exit.ts index 09c2c9b7ef63b7..84659a277fd528 100644 --- a/src/native/libs/Common/JavaScript/loader/exit.ts +++ b/src/native/libs/Common/JavaScript/loader/exit.ts @@ -130,7 +130,7 @@ export function exit(exitCode: number, reason: any): void { } } } - if (!runtimeState.dotnetReady) { + if (runtimeState.creatingRuntime) { dotnetLogger.info(`Aborting startup, reason: ${reason}`); dotnetLoaderExports.abortStartup(reason); } diff --git a/src/native/libs/Common/JavaScript/loader/run.ts b/src/native/libs/Common/JavaScript/loader/run.ts index 45c0ffb1c5aa1f..83e42b1576236b 100644 --- a/src/native/libs/Common/JavaScript/loader/run.ts +++ b/src/native/libs/Common/JavaScript/loader/run.ts @@ -3,7 +3,7 @@ import type { JsModuleExports, EmscriptenModuleInternal } from "./types"; -import { dotnetAssert, dotnetInternals, dotnetBrowserHostExports, Module } from "./cross-module"; +import { dotnetAssert, dotnetInternals, dotnetBrowserHostExports, dotnetApi, Module } from "./cross-module"; import { exit, runtimeState } from "./exit"; import { createPromiseCompletionSource } from "./promise-completion-source"; import { getIcuResourceName } from "./icu"; @@ -14,6 +14,18 @@ import { validateEngineFeatures } from "./bootstrap"; const runMainPromiseController = createPromiseCompletionSource(); +async function callLibraryInitializers(modules: JsModuleExports[], resources: any[], methodName: string, args: any): Promise { + await Promise.all(modules.map(async (module, i) => { + try { + await (module as any)[methodName]?.(args); + } catch (err) { + const name = (resources[i] as any).name || "unknown"; + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to invoke '${methodName}' on library initializer '${name}': ${message}`, { cause: err }); + } + })); +} + // WASM-TODO: downloadOnly - Blazor render mode auto pre-download. Really no start. // WASM-TODO: debugLevel @@ -31,10 +43,9 @@ export async function createRuntime(downloadOnly: boolean): Promise { } validateLoaderConfig(); - const modulesAfterConfigLoaded = await Promise.all((loaderConfig.resources.modulesAfterConfigLoaded || []).map(loadJSModule)); - for (const afterConfigLoadedModule of modulesAfterConfigLoaded) { - await afterConfigLoadedModule.onRuntimeConfigLoaded?.(loaderConfig); - } + const afterConfigLoadedResources = loaderConfig.resources.modulesAfterConfigLoaded || []; + const modulesAfterConfigLoaded = await Promise.all(afterConfigLoadedResources.map(loadJSModule)); + await callLibraryInitializers(modulesAfterConfigLoaded, afterConfigLoadedResources, "onRuntimeConfigLoaded", loaderConfig); // after onConfigLoaded hooks, polyfills can be initialized await initPolyfills(); @@ -66,7 +77,8 @@ export async function createRuntime(downloadOnly: boolean): Promise { const isDebuggingSupported = loaderConfig.debugLevel != 0; const corePDBsPromise = isDebuggingSupported ? Promise.all((loaderConfig.resources.corePdb || []).map(fetchPdb)) : Promise.resolve([]); const pdbsPromise = isDebuggingSupported ? Promise.all((loaderConfig.resources.pdb || []).map(fetchPdb)) : Promise.resolve([]); - const modulesAfterRuntimeReadyPromise = Promise.all((loaderConfig.resources.modulesAfterRuntimeReady || []).map(loadJSModule)); + const afterRuntimeReadyResources = loaderConfig.resources.modulesAfterRuntimeReady || []; + const modulesAfterRuntimeReadyPromise = Promise.all(afterRuntimeReadyResources.map(loadJSModule)); const nativeModule = await nativeModulePromise; const modulePromise = nativeModule.dotnetInitializeModule(dotnetInternals); @@ -95,12 +107,14 @@ export async function createRuntime(downloadOnly: boolean): Promise { verifyAllAssetsDownloaded(); - if (typeof Module.onDotnetReady === "function") { - await Module.onDotnetReady(); - } - const modulesAfterRuntimeReady = await modulesAfterRuntimeReadyPromise; - for (const afterRuntimeReadyModule of modulesAfterRuntimeReady) { - await afterRuntimeReadyModule.onRuntimeReady?.(loaderConfig); + if (!downloadOnly) { + if (typeof Module.onDotnetReady === "function") { + await Module.onDotnetReady(); + } + const modulesAfterRuntimeReady = await modulesAfterRuntimeReadyPromise; + const allRuntimeReadyModules = [...modulesAfterConfigLoaded, ...modulesAfterRuntimeReady]; + const allRuntimeReadyResources = [...afterConfigLoadedResources, ...afterRuntimeReadyResources]; + await callLibraryInitializers(allRuntimeReadyModules, allRuntimeReadyResources, "onRuntimeReady", dotnetApi); } runtimeState.creatingRuntime = false; } catch (err) {