接续L4-TOOL-DB浏览器Worker边界

结论:浏览器 Tool DB Worker 传输层已接入并明确阻塞缺失的 Python/WASM runtime,未冒充 DB_PROGRAM 执行就绪。
This commit is contained in:
2026-06-19 07:09:25 +08:00
parent d323511f8b
commit 63870fd6d7
10 changed files with 248 additions and 4 deletions

View File

@@ -573,6 +573,11 @@ private module paths:
process port, persisting its transcript and DB flat file through the OPFS tool process port, persisting its transcript and DB flat file through the OPFS tool
DB store, and returning diagnostics. The helper is browser-safe glue; runtime DB store, and returning diagnostics. The helper is browser-safe glue; runtime
semantics still come from the supplied port adapter. semantics still come from the supplied port adapter.
- `createLinuxCncToolDbBrowserWorkerAdapter()` for the browser Worker transport
boundary. The bundled worker reports
`blocked_browser_python_wasm_runtime_missing` until a real Python/WASM runtime
is installed; the Worker shell alone must not mark `DB_PROGRAM` execution
ready.
- `createLinuxCncToolDbNodeRuntimeAdapter()` in - `createLinuxCncToolDbNodeRuntimeAdapter()` in
`runtime/sdk/src/tool-db-node-runtime-adapter.js` for Node/native verification `runtime/sdk/src/tool-db-node-runtime-adapter.js` for Node/native verification
of the real LinuxCNC `DB_PROGRAM` child process. This adapter is intentionally of the real LinuxCNC `DB_PROGRAM` child process. This adapter is intentionally

View File

@@ -99,6 +99,9 @@ export {
createToolDbTransactionPlan, createToolDbTransactionPlan,
validateToolDbTranscript, validateToolDbTranscript,
} from "./tool-db-process-port.js"; } from "./tool-db-process-port.js";
export {
createLinuxCncToolDbBrowserWorkerAdapter,
} from "./tool-db-browser-worker-adapter.js";
export { export {
runToolDbProcessPortPersistenceSession, runToolDbProcessPortPersistenceSession,
} from "./tool-db-runtime-session.js"; } from "./tool-db-runtime-session.js";

View File

@@ -0,0 +1,105 @@
function requireWorkerUrl(value) {
if (typeof value === "string" && value.length > 0) {
return value;
}
if (value instanceof URL) {
return value;
}
throw new Error("workerUrl must be a non-empty string or URL.");
}
function makeRequestId() {
return `tool-db-worker-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
export function createLinuxCncToolDbBrowserWorkerAdapter({
workerUrl,
workerFactory = (url) => new Worker(url, { type: "module" }),
timeoutMs = 5000,
} = {}) {
const normalizedWorkerUrl = requireWorkerUrl(workerUrl);
let worker = null;
const pending = new Map();
let runtimeExecutionReady = false;
let startStatus = "blocked_browser_python_wasm_runtime_missing";
function rejectPending(error) {
for (const { reject, timer } of pending.values()) {
clearTimeout(timer);
reject(error);
}
pending.clear();
}
function request(type, payload = {}) {
if (!worker) {
return Promise.reject(new Error("Tool DB browser worker is not started."));
}
const id = makeRequestId();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`timeout waiting for Tool DB browser worker ${type}.`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer });
worker.postMessage({ id, type, ...payload });
});
}
function handleMessage(event) {
const message = event.data ?? {};
const waiter = pending.get(message.id);
if (!waiter) {
return;
}
pending.delete(message.id);
clearTimeout(waiter.timer);
if (message.ok === false) {
waiter.reject(new Error(message.error ?? `Tool DB browser worker ${message.type ?? "request"} failed.`));
return;
}
waiter.resolve(message);
}
return {
runtimeMode: "browser-python-wasm-worker",
async start() {
worker = workerFactory(normalizedWorkerUrl);
worker.addEventListener("message", handleMessage);
worker.addEventListener("error", (event) => {
rejectPending(new Error(event.message ?? "Tool DB browser worker error."));
});
const result = await request("start");
runtimeExecutionReady = result.runtimeExecutionReady === true;
startStatus = result.status ?? (runtimeExecutionReady
? "browser_python_wasm_runtime_ready"
: "blocked_browser_python_wasm_runtime_missing");
return {
status: startStatus,
runtimeExecutionReady,
};
},
async writeLine(line) {
if (runtimeExecutionReady !== true) {
throw new Error(`${startStatus}: browser Tool DB runtime is not execution-ready.`);
}
await request("writeLine", { line });
},
async readLine() {
if (runtimeExecutionReady !== true) {
throw new Error(`${startStatus}: browser Tool DB runtime is not execution-ready.`);
}
const result = await request("readLine");
return result.line;
},
close() {
rejectPending(new Error("Tool DB browser worker closed."));
worker?.terminate();
worker = null;
},
};
}

View File

@@ -103,6 +103,10 @@ export function createLinuxCncToolDbNodeRuntimeAdapter({
failWaiters(new Error(`DB_PROGRAM exited with code ${code}.`)); failWaiters(new Error(`DB_PROGRAM exited with code ${code}.`));
} }
}); });
return {
status: "node_linuxcnc_db_program_started",
runtimeExecutionReady: true,
};
}, },
async writeLine(line) { async writeLine(line) {

View File

@@ -140,6 +140,8 @@ export function createLinuxCncToolDbProcessPort({
const transcript = []; const transcript = [];
let started = false; let started = false;
let closed = false; let closed = false;
let runtimeExecutionReady = false;
let startStatus = null;
function assertOpen() { function assertOpen() {
if (closed) { if (closed) {
@@ -156,12 +158,17 @@ export function createLinuxCncToolDbProcessPort({
async start() { async start() {
assertOpen(); assertOpen();
started = true; started = true;
let adapterStartResult = {};
if (runtimeAdapter?.start) { if (runtimeAdapter?.start) {
await runtimeAdapter.start(); adapterStartResult = await runtimeAdapter.start();
} }
runtimeExecutionReady = adapterStartResult.runtimeExecutionReady === true ||
(Boolean(runtimeAdapter) && adapterStartResult.runtimeExecutionReady !== false && !adapterStartResult.status);
startStatus = adapterStartResult.status ?? (runtimeExecutionReady ? "runtime_adapter_started" : null);
return { return {
runtimeMode, runtimeMode,
runtimeExecutionReady: Boolean(runtimeAdapter), runtimeExecutionReady,
status: startStatus,
executionEnabled: false, executionEnabled: false,
promotionAllowed: false, promotionAllowed: false,
}; };
@@ -194,9 +201,9 @@ export function createLinuxCncToolDbProcessPort({
async runTransactionPlan(plan = createToolDbTransactionPlan()) { async runTransactionPlan(plan = createToolDbTransactionPlan()) {
assertOpen(); assertOpen();
if (!runtimeAdapter) { if (!runtimeAdapter || runtimeExecutionReady !== true) {
return { return {
status: "blocked_runtime_adapter_required", status: runtimeAdapter ? (startStatus ?? "blocked_runtime_execution_not_ready") : "blocked_runtime_adapter_required",
plan, plan,
runtimeExecutionReady: false, runtimeExecutionReady: false,
executionEnabled: false, executionEnabled: false,

View File

@@ -0,0 +1,39 @@
function postReply(id, type, payload) {
self.postMessage({ id, type, ok: true, ...payload });
}
function postError(id, type, error) {
self.postMessage({
id,
type,
ok: false,
error: error?.message ?? String(error),
});
}
function pythonRuntimeAvailable() {
return typeof self.loadPyodide === "function" || Boolean(self.pyodide?.runPythonAsync);
}
self.addEventListener("message", async (event) => {
const message = event.data ?? {};
const { id, type } = message;
try {
if (type === "start") {
const ready = pythonRuntimeAvailable();
postReply(id, type, {
runtimeMode: "browser-python-wasm-worker",
runtimeExecutionReady: ready,
status: ready
? "browser_python_wasm_runtime_ready"
: "blocked_browser_python_wasm_runtime_missing",
executionEnabled: false,
promotionAllowed: false,
});
return;
}
throw new Error("blocked_browser_python_wasm_runtime_missing");
} catch (error) {
postError(id, type, error);
}
});

View File

@@ -9,6 +9,7 @@
<script type="module"> <script type="module">
import { import {
createLinuxCncToolDbProcessPort, createLinuxCncToolDbProcessPort,
createLinuxCncToolDbBrowserWorkerAdapter,
createToolDbProcessDiagnostics, createToolDbProcessDiagnostics,
createToolDbStorePaths, createToolDbStorePaths,
loadToolDbFile, loadToolDbFile,
@@ -58,6 +59,32 @@
assertEqual(planResult.status, "blocked_runtime_adapter_required", "contract-only plan status"); assertEqual(planResult.status, "blocked_runtime_adapter_required", "contract-only plan status");
assertEqual(planResult.tblFallbackSufficient, false, "tbl fallback guard"); assertEqual(planResult.tblFallbackSufficient, false, "tbl fallback guard");
const workerAdapter = createLinuxCncToolDbBrowserWorkerAdapter({
workerUrl: new URL("../../runtime/workers/tool-db-python-worker.js", import.meta.url),
});
const workerPort = createLinuxCncToolDbProcessPort({
dbProgramPath: "./db_nonran.py",
sourceFiles: [
"configs/sim/axis/db_demo/db_nonran.py",
"configs/sim/axis/db_demo/db.py",
"lib/python/tooldb.py",
],
runtimeMode: workerAdapter.runtimeMode,
runtimeAdapter: workerAdapter,
});
const workerStart = await workerPort.start();
assertEqual(workerStart.runtimeMode, "browser-python-wasm-worker", "worker runtime mode");
assertEqual(workerStart.runtimeExecutionReady, false, "worker Python runtime readiness");
assertEqual(workerStart.status, "blocked_browser_python_wasm_runtime_missing", "worker Python runtime status");
const workerPlanResult = await workerPort.runTransactionPlan();
assertEqual(
workerPlanResult.status,
"blocked_browser_python_wasm_runtime_missing",
"worker plan blocked without Python/WASM runtime",
);
assertEqual(workerPlanResult.promotionAllowed, false, "worker promotion guard");
await workerPort.close();
const transcript = [ const transcript = [
{ direction: "read", line: "v2.1" }, { direction: "read", line: "v2.1" },
{ direction: "write", line: "g" }, { direction: "write", line: "g" },

View File

@@ -90,6 +90,7 @@ import {
createProjectReleaseReadinessArtifactValidationSummaryViewModel, createProjectReleaseReadinessArtifactValidationSummaryViewModel,
createProjectReleaseReadinessReport, createProjectReleaseReadinessReport,
createProjectReleaseReadinessSummaryViewModel, createProjectReleaseReadinessSummaryViewModel,
createLinuxCncToolDbBrowserWorkerAdapter,
createLinuxCncToolDbProcessPort, createLinuxCncToolDbProcessPort,
createToolDbProcessDiagnostics, createToolDbProcessDiagnostics,
createToolDbTransactionPlan, createToolDbTransactionPlan,
@@ -522,6 +523,7 @@ const requiredExports = [
["saveMachineTextFiles", saveMachineTextFiles], ["saveMachineTextFiles", saveMachineTextFiles],
["loadMachineTextFiles", loadMachineTextFiles], ["loadMachineTextFiles", loadMachineTextFiles],
["gcodeFilenameFromProgramPath", gcodeFilenameFromProgramPath], ["gcodeFilenameFromProgramPath", gcodeFilenameFromProgramPath],
["createLinuxCncToolDbBrowserWorkerAdapter", createLinuxCncToolDbBrowserWorkerAdapter],
["createLinuxCncToolDbProcessPort", createLinuxCncToolDbProcessPort], ["createLinuxCncToolDbProcessPort", createLinuxCncToolDbProcessPort],
["createToolDbProcessDiagnostics", createToolDbProcessDiagnostics], ["createToolDbProcessDiagnostics", createToolDbProcessDiagnostics],
["createToolDbTransactionPlan", createToolDbTransactionPlan], ["createToolDbTransactionPlan", createToolDbTransactionPlan],

View File

@@ -83,6 +83,7 @@ const port = createLinuxCncToolDbProcessPort({
const startResult = await port.start(); const startResult = await port.start();
assert.equal(startResult.runtimeMode, "node-linuxcnc-db-program"); assert.equal(startResult.runtimeMode, "node-linuxcnc-db-program");
assert.equal(startResult.runtimeExecutionReady, true); assert.equal(startResult.runtimeExecutionReady, true);
assert.equal(startResult.status, "node_linuxcnc_db_program_started");
assert.equal(startResult.executionEnabled, false); assert.equal(startResult.executionEnabled, false);
assert.equal(startResult.promotionAllowed, false); assert.equal(startResult.promotionAllowed, false);

View File

@@ -1,5 +1,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import {
createLinuxCncToolDbBrowserWorkerAdapter,
} from "../../../runtime/sdk/src/tool-db-browser-worker-adapter.js";
import { import {
TOOL_DB_PROCESS_PORT_CONTRACT_VERSION, TOOL_DB_PROCESS_PORT_CONTRACT_VERSION,
TOOL_DB_TRANSACTION_PLAN, TOOL_DB_TRANSACTION_PLAN,
@@ -37,6 +40,7 @@ const startResult = await port.start();
assert.deepEqual(startResult, { assert.deepEqual(startResult, {
runtimeMode: "contract-only", runtimeMode: "contract-only",
runtimeExecutionReady: false, runtimeExecutionReady: false,
status: null,
executionEnabled: false, executionEnabled: false,
promotionAllowed: false, promotionAllowed: false,
}); });
@@ -48,6 +52,53 @@ assert.equal(blockedPlan.executionEnabled, false);
assert.equal(blockedPlan.promotionAllowed, false); assert.equal(blockedPlan.promotionAllowed, false);
assert.equal(blockedPlan.tblFallbackSufficient, false); assert.equal(blockedPlan.tblFallbackSufficient, false);
class FakeBlockedWorker {
constructor() {
this.listeners = new Map();
}
addEventListener(type, listener) {
this.listeners.set(type, listener);
}
postMessage(message) {
queueMicrotask(() => {
this.listeners.get("message")?.({
data: {
id: message.id,
type: message.type,
ok: true,
runtimeExecutionReady: false,
status: "blocked_browser_python_wasm_runtime_missing",
executionEnabled: false,
promotionAllowed: false,
},
});
});
}
terminate() {}
}
const browserWorkerAdapter = createLinuxCncToolDbBrowserWorkerAdapter({
workerUrl: "./tool-db-python-worker.js",
workerFactory: () => new FakeBlockedWorker(),
});
const browserWorkerPort = createLinuxCncToolDbProcessPort({
dbProgramPath: "./db_nonran.py",
runtimeMode: browserWorkerAdapter.runtimeMode,
runtimeAdapter: browserWorkerAdapter,
});
const browserWorkerStart = await browserWorkerPort.start();
assert.equal(browserWorkerStart.runtimeMode, "browser-python-wasm-worker");
assert.equal(browserWorkerStart.runtimeExecutionReady, false);
assert.equal(browserWorkerStart.status, "blocked_browser_python_wasm_runtime_missing");
const browserWorkerBlockedPlan = await browserWorkerPort.runTransactionPlan();
assert.equal(browserWorkerBlockedPlan.status, "blocked_browser_python_wasm_runtime_missing");
assert.equal(browserWorkerBlockedPlan.runtimeExecutionReady, false);
assert.equal(browserWorkerBlockedPlan.promotionAllowed, false);
await browserWorkerPort.close();
const transcript = [ const transcript = [
{ direction: "read", line: "v2.1" }, { direction: "read", line: "v2.1" },
{ direction: "write", line: "g" }, { direction: "write", line: "g" },