接续L4-TOOL-DB持久化闭环

结论:真实 Node/native DB_PROGRAM transcript 与 flat-file 已接入 Tool DB OPFS store 证据链,浏览器执行仍保持锁定等待 Python/WASM Worker。
This commit is contained in:
2026-06-19 07:01:48 +08:00
parent 823bab5fa4
commit d323511f8b
6 changed files with 181 additions and 18 deletions

View File

@@ -569,6 +569,10 @@ private module paths:
LinuxCNC-owned tool DB process port contract. Contract-only mode does not run LinuxCNC-owned tool DB process port contract. Contract-only mode does not run
`DB_PROGRAM`, does not parse `.tbl` as a fallback, and keeps execution and `DB_PROGRAM`, does not parse `.tbl` as a fallback, and keeps execution and
promotion disabled. promotion disabled.
- `runToolDbProcessPortPersistenceSession()` for running a supplied tool DB
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
semantics still come from the supplied port adapter.
- `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 {
runToolDbProcessPortPersistenceSession,
} from "./tool-db-runtime-session.js";
export { export {
defaultMachinePaths, defaultMachinePaths,

View File

@@ -1,5 +1,5 @@
import { createWriteStream } from "node:fs"; import { createWriteStream } from "node:fs";
import { mkdir, rm } from "node:fs/promises"; import { mkdir, readFile, rm } from "node:fs/promises";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
@@ -156,5 +156,9 @@ export function createLinuxCncToolDbNodeRuntimeAdapter({
stdoutFile?.end(); stdoutFile?.end();
stderrFile?.end(); stderrFile?.end();
}, },
async readDbFileText() {
return readFile(dbSavefile, "utf8");
},
}; };
} }

View File

@@ -0,0 +1,78 @@
import {
loadToolDbFile,
loadToolDbTranscript,
saveToolDbFile,
saveToolDbTranscript,
} from "../../opfs/tool-db-store.js";
import {
createToolDbProcessDiagnostics,
createToolDbTransactionPlan,
validateToolDbTranscript,
} from "./tool-db-process-port.js";
function requireString(value, label) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`${label} must be a non-empty string.`);
}
return value;
}
export async function runToolDbProcessPortPersistenceSession({
port,
machineId,
runId = "run-1",
dbProgramPath = "./db_nonran.py",
dbFileText,
storage,
plan = createToolDbTransactionPlan(),
startupToolCount = 10,
mutationCount = 3,
readDbFileText = null,
} = {}) {
if (!port?.start || !port?.runTransactionPlan || !port?.exportTranscript || !port?.close) {
throw new Error("port must be a ToolDbProcessPort-like object.");
}
requireString(machineId, "machineId");
requireString(runId, "runId");
requireString(dbProgramPath, "dbProgramPath");
const startResult = await port.start();
const planResult = await port.runTransactionPlan(plan);
const transcript = port.exportTranscript();
const validation = validateToolDbTranscript(transcript);
const resolvedDbFileText = readDbFileText ? await readDbFileText() : (dbFileText ?? "");
await port.close();
const savedDb = await saveToolDbFile(machineId, resolvedDbFileText, { storage });
const savedTranscript = await saveToolDbTranscript(machineId, runId, transcript, { storage });
const loadedDb = await loadToolDbFile(machineId, { storage });
const loadedTranscript = await loadToolDbTranscript(machineId, runId, { storage });
const diagnostics = createToolDbProcessDiagnostics({
dbProgramPath,
opfsDbPath: savedDb.displayPath,
transcript,
dbFileText: loadedDb.text,
startupToolCount,
mutationCount,
runtimeMode: startResult.runtimeMode,
runtimeExecutionReady: startResult.runtimeExecutionReady === true && planResult.runtimeExecutionReady === true,
});
return {
apiName: "linuxcnc-tool-db-process-runtime-persistence-session",
ready: validation.ready &&
diagnostics.runtimeExecutionReady === true &&
diagnostics.opfsPersistenceReady === true &&
diagnostics.protocolTranscriptReady === true,
startResult,
planResult,
validation,
savedDb,
savedTranscript,
loadedDb,
loadedTranscript,
diagnostics,
executionEnabled: false,
promotionAllowed: false,
};
}

View File

@@ -93,6 +93,7 @@ import {
createLinuxCncToolDbProcessPort, createLinuxCncToolDbProcessPort,
createToolDbProcessDiagnostics, createToolDbProcessDiagnostics,
createToolDbTransactionPlan, createToolDbTransactionPlan,
runToolDbProcessPortPersistenceSession,
createVirtualHalBridgeActionPlan, createVirtualHalBridgeActionPlan,
createVirtualHalBridgeReadiness, createVirtualHalBridgeReadiness,
createVirtualHalCommandScriptFixtureReport, createVirtualHalCommandScriptFixtureReport,
@@ -524,6 +525,7 @@ const requiredExports = [
["createLinuxCncToolDbProcessPort", createLinuxCncToolDbProcessPort], ["createLinuxCncToolDbProcessPort", createLinuxCncToolDbProcessPort],
["createToolDbProcessDiagnostics", createToolDbProcessDiagnostics], ["createToolDbProcessDiagnostics", createToolDbProcessDiagnostics],
["createToolDbTransactionPlan", createToolDbTransactionPlan], ["createToolDbTransactionPlan", createToolDbTransactionPlan],
["runToolDbProcessPortPersistenceSession", runToolDbProcessPortPersistenceSession],
["validateToolDbTranscript", validateToolDbTranscript], ["validateToolDbTranscript", validateToolDbTranscript],
["saveToolDbFile", saveToolDbFile], ["saveToolDbFile", saveToolDbFile],
["loadToolDbFile", loadToolDbFile], ["loadToolDbFile", loadToolDbFile],

View File

@@ -4,12 +4,64 @@ import { resolve } from "node:path";
import { import {
createLinuxCncToolDbProcessPort, createLinuxCncToolDbProcessPort,
createToolDbProcessDiagnostics,
validateToolDbTranscript, validateToolDbTranscript,
} from "../../../runtime/sdk/src/tool-db-process-port.js"; } from "../../../runtime/sdk/src/tool-db-process-port.js";
import { import {
createLinuxCncToolDbNodeRuntimeAdapter, createLinuxCncToolDbNodeRuntimeAdapter,
} from "../../../runtime/sdk/src/tool-db-node-runtime-adapter.js"; } from "../../../runtime/sdk/src/tool-db-node-runtime-adapter.js";
import {
runToolDbProcessPortPersistenceSession,
} from "../../../runtime/sdk/src/tool-db-runtime-session.js";
class MockFileHandle {
constructor(name) {
this.name = name;
this.textValue = "";
}
async createWritable() {
return {
write: async (text) => {
this.textValue = String(text);
},
close: async () => {},
};
}
async getFile() {
return {
text: async () => this.textValue,
};
}
}
class MockDirectoryHandle {
constructor(name = "") {
this.name = name;
this.dirs = new Map();
this.files = new Map();
}
async getDirectoryHandle(name, options = {}) {
if (!this.dirs.has(name)) {
if (!options.create) {
throw new Error(`missing directory: ${name}`);
}
this.dirs.set(name, new MockDirectoryHandle(name));
}
return this.dirs.get(name);
}
async getFileHandle(name, options = {}) {
if (!this.files.has(name)) {
if (!options.create) {
throw new Error(`missing file: ${name}`);
}
this.files.set(name, new MockFileHandle(name));
}
return this.files.get(name);
}
}
const adapter = createLinuxCncToolDbNodeRuntimeAdapter({ const adapter = createLinuxCncToolDbNodeRuntimeAdapter({
dbSavefile: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-adapter-db-nonran"), dbSavefile: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-adapter-db-nonran"),
@@ -59,22 +111,42 @@ const dbFileText = readFileSync(adapter.dbSavefile, "utf8");
assert.match(dbFileText, /T11 P111 D0.33 Z0.11/); assert.match(dbFileText, /T11 P111 D0.33 Z0.11/);
assert.match(dbFileText, /T14 P114\b/); assert.match(dbFileText, /T14 P114\b/);
const diagnostics = createToolDbProcessDiagnostics({ const storageRoot = new MockDirectoryHandle();
dbProgramPath: "./db_nonran.py", const storage = { getDirectory: async () => storageRoot };
opfsDbPath: "/machines/db-demo/tool-db/db_nonran_file", const sessionAdapter = createLinuxCncToolDbNodeRuntimeAdapter({
transcript, dbSavefile: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-persistence-db-nonran"),
dbFileText, stdoutLog: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-persistence.stdout.log"),
startupToolCount: 10, stderrLog: resolve("wasm-port/build/native/tool-db-runtime/sdk-node-runtime-persistence.stderr.log"),
mutationCount: 3,
runtimeMode: adapter.runtimeMode,
runtimeExecutionReady: true,
}); });
assert.equal(diagnostics.runtimeMode, "node-linuxcnc-db-program"); const session = await runToolDbProcessPortPersistenceSession({
assert.equal(diagnostics.runtimeExecutionReady, true); port: createLinuxCncToolDbProcessPort({
assert.equal(diagnostics.protocolTranscriptReady, true); dbProgramPath: "./db_nonran.py",
assert.equal(diagnostics.opfsPersistenceReady, true); sourceFiles: [
assert.equal(diagnostics.tblFallbackSufficient, false); "configs/sim/axis/db_demo/db_nonran.py",
assert.equal(diagnostics.executionEnabled, false); "configs/sim/axis/db_demo/db.py",
assert.equal(diagnostics.promotionAllowed, false); "lib/python/tooldb.py",
],
runtimeMode: sessionAdapter.runtimeMode,
runtimeAdapter: sessionAdapter,
}),
machineId: "db-demo",
runId: "node-runtime-session",
dbProgramPath: "./db_nonran.py",
readDbFileText: () => sessionAdapter.readDbFileText(),
storage,
});
assert.equal(session.apiName, "linuxcnc-tool-db-process-runtime-persistence-session");
assert.equal(session.ready, true);
assert.equal(session.diagnostics.runtimeMode, "node-linuxcnc-db-program");
assert.equal(session.diagnostics.runtimeExecutionReady, true);
assert.equal(session.diagnostics.protocolTranscriptReady, true);
assert.equal(session.diagnostics.opfsPersistenceReady, true);
assert.equal(session.diagnostics.tblFallbackSufficient, false);
assert.equal(session.diagnostics.executionEnabled, false);
assert.equal(session.diagnostics.promotionAllowed, false);
assert.equal(session.savedDb.displayPath, "/machines/db-demo/tool-db/db_nonran_file");
assert.equal(session.savedTranscript.displayPath, "/machines/db-demo/tool-db/transcripts/node-runtime-session/transcript.json");
assert.match(session.loadedDb.text, /T11 P111 D0.33 Z0.11/);
assert.match(session.loadedTranscript.payload.transcript[0].line, /^v2\.1$/);
console.log("tool_db_node_runtime_adapter=ok"); console.log("tool_db_node_runtime_adapter=ok");