Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Dynamic import must consult the same scope as a static import from this
// file: the host names the referrer by script origin, and that origin must
// canonicalize to the registry key the scope prefixes match against.
export function load() {
return import("ns-scoped-leaf");
}
23 changes: 23 additions & 0 deletions test-app/app/src/main/assets/app/tests/testEsmHttpLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,29 @@ describe("HTTP ESM Loader", function () {
reportRejection(error, done);
});
});

it("applies the referrer's scope to dynamic import()", function (done) {
// The host names a dynamic import's referrer by script origin
// (a file:// URL), not by registry key; the origin must land on
// the same canonical key the scope prefixes match, or scoped
// lookups silently fall through to top-level imports.
var insideScope = appRoot + "/esm/scoped/inside/";
var scopes = {};
scopes[insideScope] = { "ns-scoped-leaf": origin + "/esm/graph-leaf.mjs?k=in" };
setMap({
imports: { "ns-scoped-leaf": origin + "/esm/graph-leaf.mjs?k=top" },
scopes: scopes,
});

import("~/esm/scoped/inside/dynamic.mjs").then(function (mod) {
return mod.load();
}).then(function (leafMod) {
expect(leafMod.name).toBe("in");
done();
}).catch(function (error) {
reportRejection(error, done);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});

// An imperative API rejects bad input loudly, the way WebIDL does on the
Expand Down
14 changes: 14 additions & 0 deletions test-app/app/src/main/assets/app/tests/testWorkerEsmEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ describe("worker ES module entries", function () {
worker.postMessage("ping");
});

// An http(s) worker specifier bypasses the filesystem check entirely: the
// entry is fetched, compiled and registered under its canonical URL key on
// the worker's own thread, which is also the key the settle gate probes.
it("runs a worker whose entry is an http URL", function (done) {
var origin = "http://127.0.0.1:" + com.tns.tests.ModuleTestServer.ensureStarted();
var worker = new Worker(origin + "/esm/worker-entry.mjs");
worker.onmessage = function (msg) {
expect(msg.data).toBe("http-worker-entry:ping");
worker.terminate();
done();
};
worker.postMessage("ping");
});

// Extension resolution tries `.js` before `.mjs`, and no `.js` sibling
// exists, so the ES module entry is what answers. Its top-level await also
// parks past the yield window, so the message posted here proves the
Expand Down
17 changes: 17 additions & 0 deletions test-app/app/src/main/java/com/tns/tests/ModuleTestServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ private static void route(Socket socket, String path, String query) throws IOExc
return;
}

if ("/esm/worker-entry.mjs".equals(path)) {
// A worker entry served over HTTP, importing one relative dependency
// so the entry exercises the graph walk and not just the root fetch.
String body = "import { WORKER_TAG } from \"./worker-entry-dep.mjs\";\n"
+ "globalThis.onmessage = function (msg) {\n"
+ " postMessage(WORKER_TAG + \":\" + msg.data);\n"
+ "};\n";
respond(socket, "200 OK", JS_MIME, body.getBytes(UTF8));
return;
}

if ("/esm/worker-entry-dep.mjs".equals(path)) {
String body = "export const WORKER_TAG = \"http-worker-entry\";\n";
respond(socket, "200 OK", JS_MIME, body.getBytes(UTF8));
return;
}

if ("/esm/syntax-error.mjs".equals(path)) {
// Deliberately unparseable: pins that the loader surfaces V8's real
// compile error instead of a generic failure.
Expand Down
52 changes: 35 additions & 17 deletions test-app/runtime/src/main/cpp/CallbackHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#include <fstream>
#include <cstdio>
#include <chrono>
#include "HttpLoader.h"
#include "MethodCache.h"
#include "ModuleInternal.h"
#include "SimpleProfiler.h"
#include "Runtime.h"
#include "WorkerMessage.h"
Expand Down Expand Up @@ -1215,11 +1217,17 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo<v8::Valu

int priority = GetWorkerThreadPriority(isolate, context, args);

// TODO: Validate worker path and call worker.onerror if the script does not exist
// An http(s) entry has no filesystem form to validate or to resolve
// against the caller's directory: it is already absolute, and the
// module loader's HTTP branch fetches it on the worker's own thread
// under the same security gate every other remote load passes. The
// URL is what the worker registers its entry under, so it is also what
// the settle gate probes — it must reach the wrapper unrewritten.
const bool isHttpEntry = ModuleInternal::IsHttpModulePath(workerPath);

// Resolve tilde paths before creating the worker
std::string resolvedPath = workerPath;
if (!workerPath.empty() && workerPath[0] == '~') {
if (!isHttpEntry && !workerPath.empty() && workerPath[0] == '~') {
// Convert ~/path to ApplicationPath/path
std::string tail = workerPath.size() >= 2 && workerPath[1] == '/' ? workerPath.substr(2) : workerPath.substr(1);
resolvedPath = Constants::APP_ROOT_FOLDER_PATH + tail;
Expand All @@ -1232,7 +1240,9 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo<v8::Valu
* app-root-relative resolution, mirroring the iOS runtime.
*/
std::string currentDir = Constants::APP_ROOT_FOLDER_PATH;
auto stack = StackTrace::CurrentStackTrace(isolate, 1, StackTrace::kScriptName);
auto stack = isHttpEntry
? Local<StackTrace>()
: StackTrace::CurrentStackTrace(isolate, 1, StackTrace::kScriptName);
if (!stack.IsEmpty() && stack->GetFrameCount() > 0) {
auto currentExecutingScriptName = stack->GetFrame(isolate, 0)->GetScriptName();
auto currentExecutingScriptNameStr = ArgConverter::ConvertToString(
Expand All @@ -1248,22 +1258,30 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo<v8::Valu
}
}

// Will throw if the path is invalid or the file doesn't exist. The
// worker runs on its own thread, with its own working directory and
// module registry, so it gets the canonical path resolved here rather
// than the spec: nothing on the other side can redo this resolution,
// and the entry's registry key must be the file that was validated.
// The worker runs on its own thread, with its own working directory
// and module registry, so it gets the entry resolved here rather than
// the spec: nothing on the other side can redo this resolution, and
// the entry's registry key must be what was resolved here.
std::string entryPath;
try {
entryPath = ModuleInternal::CheckFileExists(isolate, resolvedPath, currentDir);
} catch (NativeScriptException& e) {
if (currentDir == Constants::APP_ROOT_FOLDER_PATH) {
throw;
if (isHttpEntry) {
// Repaired, not canonicalized: the canonical key depends on the
// worker's own canonicalization vocabulary, which is installed on
// its isolate, and both the loader and the settle gate derive it
// there from this URL.
entryPath = NormalizeHttpModuleUrl(resolvedPath);
} else {
// Throws if the path is invalid or the file doesn't exist.
try {
entryPath = ModuleInternal::CheckFileExists(isolate, resolvedPath, currentDir);
} catch (NativeScriptException& e) {
if (currentDir == Constants::APP_ROOT_FOLDER_PATH) {
throw;
}
// not found next to the caller - retry against the app root
entryPath = ModuleInternal::CheckFileExists(isolate, resolvedPath,
Constants::APP_ROOT_FOLDER_PATH);
currentDir = Constants::APP_ROOT_FOLDER_PATH;
}
// not found next to the caller - retry against the app root
entryPath = ModuleInternal::CheckFileExists(isolate, resolvedPath,
Constants::APP_ROOT_FOLDER_PATH);
currentDir = Constants::APP_ROOT_FOLDER_PATH;
}

auto workerId = WorkerWrapper::NextWorkerId();
Expand Down
6 changes: 5 additions & 1 deletion test-app/runtime/src/main/cpp/EventLoop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,11 @@ EventLoop::PumpResult EventLoop::PumpUntil(double deadlineSeconds,
if (settled()) {
return PumpResult::kSettled;
}
if (isolate_->IsExecutionTerminating()) {
// Both probes: IsExecutionTerminating is true only while JS frames
// unwind with the termination exception active, so a pump parked with
// nothing queued would never observe TerminateExecution through it.
if (terminationRequested_.load(std::memory_order_acquire) ||
isolate_->IsExecutionTerminating()) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return PumpResult::kTerminated;
}
if (IsStopped()) {
Expand Down
14 changes: 14 additions & 0 deletions test-app/runtime/src/main/cpp/EventLoop.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ class EventLoop {
*/
static bool IsInLooperCallback();

/**
* Marks this loop's isolate as termination-requested. Callable from any
* thread. Pumps consult it alongside Isolate::IsExecutionTerminating,
* which per its contract is true only while JS frames are unwinding with
* the termination exception active - a pump parked with nothing queued
* never runs JS, so TerminateExecution alone cannot end it before the
* deadline.
*/
void NoteTerminationRequested() {
terminationRequested_.store(true, std::memory_order_release);
}

/**
* Blocks the calling thread until this loop's internal lane has work (the
* eventfd or timerfd is readable) or `timeoutMs` elapses, whichever comes
Expand Down Expand Up @@ -400,6 +412,8 @@ class EventLoop {
Lane internal_;
Lane ordered_;
std::atomic<uint64_t> claimCells_[kClaimCells] = {};
// Set by NoteTerminationRequested (any thread), read by PumpUntil.
std::atomic_bool terminationRequested_{false};
// ordered-lane source with its own bookkeeping (Timers); home-thread only
OrderedTaskSource* timerSource_ = nullptr;
// bare ordered tokens posted before the bind; flushed by Bind
Expand Down
Loading