mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Fall back to the page's thread when new Worker throws in a sandboxed iframe (#1586)
This commit is contained in:
@@ -315,7 +315,15 @@ function spawn(base) {
|
||||
/// path, bytes)` receives "reply" (json), "file" (path, bytes: a rendered file to download),
|
||||
/// "store" (path, bytes: a file to keep in browser storage) and "error" (json = message).
|
||||
export function workerRun(id, json, files, base, onMessage) {
|
||||
const w = idle.pop() ?? spawn(base);
|
||||
let w;
|
||||
try {
|
||||
w = idle.pop() ?? spawn(base);
|
||||
} catch (e) {
|
||||
// `new Worker` throws at once when the page's origin is opaque (a sandboxed iframe): the job
|
||||
// fails like one whose worker didn't start.
|
||||
setTimeout(() => onMessage("error", String((e && e.message) || e), null, null));
|
||||
return;
|
||||
}
|
||||
running.set(id, w);
|
||||
w.ready.then(() => {
|
||||
w.worker.onmessage = (e) => {
|
||||
@@ -380,7 +388,17 @@ const frameWorkers = [];
|
||||
/// GPU effects on it.
|
||||
export function frameWorkerStart(base, onMessage, gpu) {
|
||||
const index = frameWorkers.length;
|
||||
const worker = new Worker(new URL("worker.js", base), { type: "module", name: `effectcraft-frames-${index}` });
|
||||
let worker;
|
||||
try {
|
||||
worker = new Worker(new URL("worker.js", base), { type: "module", name: `effectcraft-frames-${index}` });
|
||||
} catch (e) {
|
||||
// The constructor throws at once when the page's origin is opaque (a sandboxed iframe). The
|
||||
// slot stays, and the failure is reported like a worker that died, after the caller has
|
||||
// recorded the index: frames then render on the page's thread (as with `?noworkers`).
|
||||
frameWorkers.push(null);
|
||||
setTimeout(() => onMessage(index, undefined, undefined, String((e && e.message) || e || "frame worker failed")));
|
||||
return index;
|
||||
}
|
||||
worker.onmessage = (e) => {
|
||||
const m = e.data;
|
||||
if (m && m.type === "frameReply") onMessage(index, m.json, m.bytes ? new Uint8Array(m.bytes) : null, undefined);
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// (#218), and not when it is unchanged.
|
||||
// - A worker whose start fails fails its job and is terminated, instead of the job waiting for
|
||||
// "ready" forever (#216): host.js on the worker's error, worker.js re-raising the failed start.
|
||||
// - A `new Worker` that throws (a sandboxed iframe's opaque origin, #1586) fails the job, and a
|
||||
// frame worker is reported dead, instead of the exception escaping into startup.
|
||||
// Exits non-zero on failure.
|
||||
import assert from "node:assert/strict";
|
||||
import { copyFileSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
@@ -67,6 +69,35 @@ assert.equal(failed.kind, "error");
|
||||
assert.ok(workers[1].terminated, "the failed worker is terminated");
|
||||
assert.equal(host.workerStats().running, 0);
|
||||
|
||||
// A `new Worker` that throws (a sandboxed iframe's opaque origin): the job fails, and a frame
|
||||
// worker is reported dead, instead of the exception escaping into startup.
|
||||
failStart = false;
|
||||
const RealWorker = globalThis.Worker;
|
||||
globalThis.Worker = class {
|
||||
constructor() {
|
||||
throw new Error("SecurityError: cannot be accessed from origin 'null'");
|
||||
}
|
||||
};
|
||||
// (Take the idle workers out of play first, so that a new one has to be built.)
|
||||
for (let n = host.workerStats().idle, id = 100; n > 0; n--, id++) {
|
||||
host.workerRun(id, "{}", [], "http://localhost/", () => {});
|
||||
host.workerCancel(id);
|
||||
}
|
||||
assert.equal(host.workerStats().idle, 0);
|
||||
const threw = await run(6, []);
|
||||
assert.equal(threw.kind, "error");
|
||||
assert.match(threw.json, /SecurityError/);
|
||||
assert.equal(host.workerStats().running, 0);
|
||||
let frameCall = null;
|
||||
const frameDone = new Promise((resolve) => {
|
||||
frameCall = host.frameWorkerStart("http://localhost/", (index, json, bytes, error) => resolve({ index, error }), false);
|
||||
});
|
||||
const dead = await frameDone;
|
||||
assert.equal(dead.index, frameCall, "the dead worker keeps its index");
|
||||
assert.match(dead.error, /SecurityError/);
|
||||
host.frameWorkerPost(frameCall, { type: "frame" }); // no worker behind it: ignored
|
||||
globalThis.Worker = RealWorker;
|
||||
|
||||
// #216, worker side: a rejected start is re-raised where the page's `onerror` sees it.
|
||||
const dir = mkdtempSync(join(tmpdir(), "ec-worker-"));
|
||||
copyFileSync(join(here, "../web/worker.js"), join(dir, "worker.js"));
|
||||
|
||||
Reference in New Issue
Block a user