fix(code): Workers 實部署修正 —— wasmfile+Module 載入 + tick-budget timeout(CF 實測)
leo21c 實部署發現兩個 CF 限制並修正:
1. CF 禁 runtime 從 bytes 編譯 wasm(WebAssembly.instantiate(bytes) 被 embedder 擋)
→ singlefile(base64) 內嵌不可用。改 wasmfile variant + `import wasm from './vendor/quickjs.wasm'`
(wrangler CompiledWasm rule 綁成預編 WebAssembly.Module),newVariant({wasmModule}) 注入。
sandbox 改 variant 注入制(setVariant):Worker 注 wrangler Module、Node 由 bytes 建 Module,
同一 production 路徑受測。vendor/quickjs.wasm 由 postinstall 自 node_modules 複製(gitignored)。
2. CF 凍結同步執行期 Date.now → wall-clock deadline 對純同步迴圈失效(撞 CF CPU 回 1102)。
改指令計數 interrupt(max_ticks,CF-safe),wall-clock 留 Node 保護。校準:cadence≈5000
指令/tick、真實 card 解析≈4 ticks、CF 門檻≈1000+ ticks → 預設 max_ticks=500。
有 body 的迴圈(含 100M 迴圈)皆乾淨回 TimeoutError;空體 while(true){} 仍由 CF CPU guard 容納。
Worker live: arcrun-code.leo21c.workers.dev(cypher-executor 以此 workers.dev 慣例位址呼叫)。
Node 單測 12/12 綠(wasmfile variant + 注入 Module,同 production 路徑)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o
This commit is contained in:
@@ -5,8 +5,11 @@
|
||||
* → QuickJS-wasm 沙箱(./sandbox.mjs 的 runCode)
|
||||
* → { success:true, data } | { success:false, error, error_type }
|
||||
*
|
||||
* 封裝=A(quickjs-emscripten singlefile variant):wasm 內嵌為 base64、同步載入,
|
||||
* bundler 友善、無需 [[wasm_modules]] 綁定、無需 nodejs_compat(sandbox 用 TextEncoder 計 bytes)。
|
||||
* 封裝=A(quickjs-emscripten wasmfile variant)。關鍵:CF Workers 禁止 runtime 從 bytes
|
||||
* 編譯 wasm(WebAssembly.instantiate(bytes) 被 embedder 擋),故不能用 singlefile(base64) 內嵌。
|
||||
* 改為 `import wasm from '.../wasm'` 讓 wrangler 在 build 時把 .wasm 綁成一個「已編好的
|
||||
* WebAssembly.Module」,再以 newVariant({ wasmModule }) 注入沙箱 → 執行期只 instantiate 既有
|
||||
* Module、不編譯,合 Workers 規則。無需 nodejs_compat(sandbox 用 TextEncoder 計 bytes)。
|
||||
*
|
||||
* 與其他 logic 零件不同:`code` 是自足 Worker(自帶 index.ts + sandbox.mjs + quickjs variant),
|
||||
* 不走 component-worker-template 的 TinyGo-wasm bundling 流程。部署見 DEPLOY.md。
|
||||
@@ -14,8 +17,16 @@
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { newVariant } from 'quickjs-emscripten-core';
|
||||
import baseVariant from '@jitl/quickjs-wasmfile-release-sync';
|
||||
// wrangler 把相對路徑 .wasm import 綁成 WebAssembly.Module(build 時編好,執行期不重編)。
|
||||
// wasm 從 quickjs-wasmfile-release-sync vendored 進 vendor/(見 DEPLOY.md「vendor 步驟」)。
|
||||
import wasmModule from './vendor/quickjs.wasm';
|
||||
// @ts-expect-error —— sandbox.mjs 為 runtime-agnostic JS 核心(Node 測試與 Worker 共用同一份)
|
||||
import { runCode } from './sandbox.mjs';
|
||||
import { runCode, setVariant } from './sandbox.mjs';
|
||||
|
||||
// 注入預編 Module(模組載入時一次)。之後 runCode 只 instantiate、不編譯。
|
||||
setVariant(newVariant(baseVariant, { wasmModule: wasmModule as WebAssembly.Module }));
|
||||
|
||||
const app = new Hono();
|
||||
app.use('*', cors());
|
||||
|
||||
+992
-56
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,16 @@
|
||||
"description": "arcrun code 零件 —— sandbox inline JS(QuickJS-wasm, Workers-ready)",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest run --config vitest.config.mjs"
|
||||
"postinstall": "node scripts/vendor-wasm.mjs",
|
||||
"vendor": "node scripts/vendor-wasm.mjs",
|
||||
"test": "vitest run --config vitest.config.mjs",
|
||||
"predeploy": "node scripts/vendor-wasm.mjs",
|
||||
"deploy": "wrangler deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-wasmfile-release-sync": "^0.32.0",
|
||||
"hono": "^4.7.0",
|
||||
"quickjs-emscripten-core": "^0.31.0",
|
||||
"@jitl/quickjs-singlefile-mjs-release-sync": "^0.32.0"
|
||||
"quickjs-emscripten-core": "^0.31.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.1.0",
|
||||
|
||||
@@ -10,16 +10,30 @@
|
||||
// 要給的能力,只能由 host 明確、逐一注入 —— 目前唯一 curated builtin = 純函式 `sha256`,
|
||||
// 且以「純 JS 演算法字串 prelude」注入(不呼叫 host、不用 async Web Crypto,Node/Worker 皆決定性)。
|
||||
//
|
||||
// 封裝方式:quickjs-emscripten「singlefile」variant(wasm 內嵌為 base64、同步載入),
|
||||
// 這是 Cloudflare Workers 相容的 loading 路徑(不靠 fetch/fs 取 .wasm)。
|
||||
// 封裝方式:quickjs-emscripten「wasmfile」variant + 預編 WebAssembly.Module 注入(見下方 setVariant)。
|
||||
// CF Workers 禁 runtime 從 bytes 編譯 wasm,故不用 singlefile(base64);改由 build 時編好 Module。
|
||||
//
|
||||
// 對齊 arcrun 契約:io_model=stdin_stdout_json、no_network_syscall、no_filesystem_syscall。
|
||||
|
||||
import variant from '@jitl/quickjs-singlefile-mjs-release-sync';
|
||||
// wasm 載入以「variant 注入」制:CF Workers 禁止 runtime 從 bytes 編譯 wasm
|
||||
// (WebAssembly.instantiate(bytes) 被 embedder 擋),故必須用「已編好的 WebAssembly.Module」。
|
||||
// - Worker(index.ts):import 的 .wasm 由 wrangler 綁成 WebAssembly.Module → newVariant 注入。
|
||||
// - Node/vitest:由 .wasm bytes 建 new WebAssembly.Module(...) → 同一 newVariant 路徑注入。
|
||||
// 呼叫方必須在 runCode 前 setVariant()。sandbox 本身不綁定任何 variant(不 bundle 錯的 loader)。
|
||||
import { newQuickJSWASMModuleFromVariant } from 'quickjs-emscripten-core';
|
||||
|
||||
let _variant = null;
|
||||
/** 注入 quickjs variant(已含預編 WebAssembly.Module)。Worker 與 Node 各自注入自己的。 */
|
||||
export function setVariant(v) { _variant = v; _modulePromise = null; }
|
||||
|
||||
export const DEFAULT_LIMITS = {
|
||||
timeout_ms: 1000, // 執行牆鐘上限(interrupt handler 逐指令檢查 deadline)
|
||||
timeout_ms: 1000, // 牆鐘上限(Node/本機保護;CF 同步執行會凍結 Date.now,故非主保護)
|
||||
max_ticks: 500, // ★ 指令計數上限(CF 主保護):interrupt 回呼被叫超過此數即中止。
|
||||
// CF Workers 凍結同步 Date.now → 純同步無窮迴圈只能靠此計數中止。
|
||||
// 校準(leo21c 實測):interrupt cadence ≈ 5000 指令/tick;
|
||||
// 真實 card 解析 ≈ 4 ticks;CF CPU 1102 門檻 ≈ 1000+ ticks。
|
||||
// 500 ticks(≈ 2.5M 指令)= card 的 125× 餘裕、且穩在 CF 門檻下。
|
||||
// 需更多算力的 user code 可提高 limits.max_ticks(但別逼近 ~1000)。
|
||||
memory_bytes: 16 * 1024 * 1024, // QuickJS runtime 記憶體硬上限
|
||||
max_stack_bytes: 512 * 1024, // 遞迴/深堆疊上限
|
||||
max_output_bytes: 1024 * 1024, // stdout JSON 大小上限(防跑飛)
|
||||
@@ -103,7 +117,8 @@ function sha256(ascii) {
|
||||
|
||||
let _modulePromise = null;
|
||||
function getModule() {
|
||||
if (!_modulePromise) _modulePromise = newQuickJSWASMModuleFromVariant(variant);
|
||||
if (!_variant) throw new Error('sandbox variant not set — 呼叫 setVariant() 注入預編 WebAssembly.Module');
|
||||
if (!_modulePromise) _modulePromise = newQuickJSWASMModuleFromVariant(_variant);
|
||||
return _modulePromise;
|
||||
}
|
||||
|
||||
@@ -129,7 +144,11 @@ export async function runCode(code, input, opts = {}) {
|
||||
|
||||
const deadline = Date.now() + limits.timeout_ms;
|
||||
let interrupted = false;
|
||||
let ticks = 0;
|
||||
runtime.setInterruptHandler(() => {
|
||||
// 主保護(CF-safe):指令計數。CF 凍結同步 Date.now,純同步無窮迴圈只能靠此中止。
|
||||
if (++ticks > limits.max_ticks) { interrupted = true; return true; }
|
||||
// 次保護(Node/本機):牆鐘 deadline(CF 同步期間不會推進,故僅在有 I/O 或非 CF 生效)。
|
||||
if (Date.now() > deadline) { interrupted = true; return true; }
|
||||
return false;
|
||||
});
|
||||
@@ -154,7 +173,7 @@ export async function runCode(code, input, opts = {}) {
|
||||
if (evalResult.error) {
|
||||
const detail = ctx.dump(evalResult.error);
|
||||
evalResult.error.dispose();
|
||||
if (interrupted) return err(`execution timed out after ${limits.timeout_ms}ms`, 'TimeoutError');
|
||||
if (interrupted) return err(`execution aborted: exceeded time (${limits.timeout_ms}ms) or instruction budget (${limits.max_ticks} ticks)`, 'TimeoutError');
|
||||
const msg = typeof detail === 'object' && detail
|
||||
? `${detail.name || 'Error'}: ${detail.message || ''}`.trim()
|
||||
: String(detail);
|
||||
@@ -169,7 +188,7 @@ export async function runCode(code, input, opts = {}) {
|
||||
}
|
||||
return { success: true, data: JSON.parse(outJson) };
|
||||
} catch (e) {
|
||||
if (interrupted) return err(`execution timed out after ${limits.timeout_ms}ms`, 'TimeoutError');
|
||||
if (interrupted) return err(`execution aborted: exceeded time (${limits.timeout_ms}ms) or instruction budget (${limits.max_ticks} ticks)`, 'TimeoutError');
|
||||
const m = e instanceof Error ? e.message : String(e);
|
||||
if (/out of memory|memory/i.test(m)) return err('out of memory', 'ResourceError');
|
||||
return err(m, 'SandboxError');
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// 把 quickjs-wasmfile variant 的 .wasm 複製進 vendor/,供 index.ts 以相對路徑 import
|
||||
// (wrangler 只可靠處理相對路徑 .wasm → CompiledWasm;node_modules 子路徑 .wasm 解析不穩)。
|
||||
// 由 postinstall 自動執行;vendor/*.wasm 為 build 產物、gitignored。
|
||||
import { copyFileSync, mkdirSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const src = require.resolve('@jitl/quickjs-wasmfile-release-sync/wasm');
|
||||
mkdirSync(new URL('../vendor/', import.meta.url), { recursive: true });
|
||||
copyFileSync(src, new URL('../vendor/quickjs.wasm', import.meta.url));
|
||||
console.log('vendored quickjs.wasm from', src);
|
||||
@@ -2,12 +2,19 @@ import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { runCode } from '../sandbox.mjs';
|
||||
import { runCode, setVariant } from '../sandbox.mjs';
|
||||
import { planCard } from './fixtures/card-to-envelope.oracle.mjs';
|
||||
import baseVariant from '@jitl/quickjs-wasmfile-release-sync';
|
||||
import { newVariant } from 'quickjs-emscripten-core';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const FIX = join(HERE, 'fixtures');
|
||||
|
||||
// 注入與 production(Worker)同一條路徑:wasmfile variant + 預編 WebAssembly.Module。
|
||||
// Worker 由 wrangler 把 import 的 .wasm 綁成 Module;此處在 Node 由 bytes 建 Module。
|
||||
const wasmBytes = readFileSync(join(HERE, '..', 'node_modules', '@jitl', 'quickjs-wasmfile-release-sync', 'dist', 'emscripten-module.wasm'));
|
||||
setVariant(newVariant(baseVariant, { wasmModule: new WebAssembly.Module(wasmBytes) }));
|
||||
|
||||
describe('① 基本 snippet 跑通', () => {
|
||||
it('sum: {return {sum: input.a + input.b}}', async () => {
|
||||
const r = await runCode('return {sum: input.a + input.b};', { a: 2, b: 40 });
|
||||
|
||||
Reference in New Issue
Block a user