接入L4-TOOL-DB节点运行时

结论:ToolDbProcessPort 已可通过 Node/native adapter 真实执行 LinuxCNC DB_PROGRAM 协议,浏览器端仍保持锁定直到 Python/WASM Worker 接入。
This commit is contained in:
2026-06-19 06:55:56 +08:00
parent c3e5e336a0
commit 823bab5fa4
8 changed files with 288 additions and 5 deletions

View File

@@ -569,6 +569,12 @@ private module paths:
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
promotion disabled.
- `createLinuxCncToolDbNodeRuntimeAdapter()` in
`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
not exported from the browser-facing SDK index because it depends on Node
process APIs; browser runtime remains locked until a Python/WASM Worker runs
the DB program.
- `saveToolDbFile()`, `loadToolDbFile()`, `saveToolDbTranscript()`, and
`loadToolDbTranscript()` for OPFS persistence of the DB flat file and protocol
transcript artifacts.

View File

@@ -0,0 +1,160 @@
import { createWriteStream } from "node:fs";
import { mkdir, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
function requireString(value, label) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`${label} must be a non-empty string.`);
}
return value;
}
function defaultRepoRoot() {
return fileURLToPath(new URL("../../../..", import.meta.url));
}
function appendPathEnv(current, next) {
return current ? `${next}:${current}` : next;
}
export function createLinuxCncToolDbNodeRuntimeAdapter({
repoRoot = defaultRepoRoot(),
linuxCncRoot = join(repoRoot, "linuxcnc"),
dbProgramPath = join(linuxCncRoot, "configs/sim/axis/db_demo/db_nonran.py"),
dbProgramCwd = dirname(dbProgramPath),
dbSavefile = join(repoRoot, "wasm-port/build/native/tool-db-runtime/sdk-node-db-nonran"),
stdoutLog = join(repoRoot, "wasm-port/build/native/tool-db-runtime/sdk-node-db-program.stdout.log"),
stderrLog = join(repoRoot, "wasm-port/build/native/tool-db-runtime/sdk-node-db-program.stderr.log"),
periodMinutes = 999,
} = {}) {
requireString(repoRoot, "repoRoot");
requireString(linuxCncRoot, "linuxCncRoot");
requireString(dbProgramPath, "dbProgramPath");
requireString(dbProgramCwd, "dbProgramCwd");
requireString(dbSavefile, "dbSavefile");
let proc = null;
let stdoutFile = null;
let stderrFile = null;
let stdoutBuffer = "";
const lineQueue = [];
const waiters = [];
let closed = false;
function flushWaiters() {
while (lineQueue.length > 0 && waiters.length > 0) {
const waiter = waiters.shift();
waiter.resolve(lineQueue.shift());
}
}
function failWaiters(error) {
while (waiters.length > 0) {
const waiter = waiters.shift();
waiter.reject(error);
}
}
function pushStdout(chunk) {
stdoutBuffer += chunk.toString("utf8");
let newlineIndex = stdoutBuffer.indexOf("\n");
while (newlineIndex !== -1) {
const line = stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, "");
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
lineQueue.push(line);
newlineIndex = stdoutBuffer.indexOf("\n");
}
flushWaiters();
}
return {
runtimeMode: "node-linuxcnc-db-program",
dbSavefile,
stdoutLog,
stderrLog,
async start() {
await mkdir(dirname(dbSavefile), { recursive: true });
await rm(dbSavefile, { force: true });
stdoutFile = createWriteStream(stdoutLog, { encoding: "utf8" });
stderrFile = createWriteStream(stderrLog, { encoding: "utf8" });
const env = {
...process.env,
PYTHONPATH: appendPathEnv(process.env.PYTHONPATH, join(linuxCncRoot, "lib/python")),
LD_LIBRARY_PATH: appendPathEnv(process.env.LD_LIBRARY_PATH, join(linuxCncRoot, "lib")),
};
proc = spawn(dbProgramPath, [`--period_minutes=${periodMinutes}`, dbSavefile], {
cwd: dbProgramCwd,
env,
stdio: ["pipe", "pipe", "pipe"],
});
proc.stdout.on("data", (chunk) => {
stdoutFile.write(chunk);
pushStdout(chunk);
});
proc.stderr.on("data", (chunk) => stderrFile.write(chunk));
proc.on("error", (error) => failWaiters(error));
proc.on("exit", (code) => {
if (!closed && code !== 0) {
failWaiters(new Error(`DB_PROGRAM exited with code ${code}.`));
}
});
},
async writeLine(line) {
if (!proc?.stdin || proc.stdin.destroyed) {
throw new Error("DB_PROGRAM stdin is not writable.");
}
proc.stdin.write(`${line}\n`);
},
async readLine({ timeoutMs = 5000 } = {}) {
if (lineQueue.length > 0) {
return lineQueue.shift();
}
return new Promise((resolve, reject) => {
let waiter;
const timer = setTimeout(() => {
const index = waiters.indexOf(waiter);
if (index !== -1) {
waiters.splice(index, 1);
}
reject(new Error("timeout waiting for DB_PROGRAM output."));
}, timeoutMs);
waiter = {
resolve: (line) => {
clearTimeout(timer);
resolve(line);
},
reject: (error) => {
clearTimeout(timer);
reject(error);
},
};
waiters.push(waiter);
});
},
async close() {
closed = true;
try {
proc?.stdin?.end();
} catch {
// ignore close races
}
if (proc && proc.exitCode === null && proc.signalCode === null) {
await new Promise((resolve) => {
proc.once("exit", resolve);
proc.kill("SIGTERM");
setTimeout(resolve, 1000);
});
}
stdoutFile?.end();
stderrFile?.end();
},
};
}

View File

@@ -207,8 +207,18 @@ export function createLinuxCncToolDbProcessPort({
for (const step of plan) {
if (step.direction === "write") {
await this.writeLine(step.line);
if (step.requiresFini) {
let finiSeen = false;
while (!finiSeen) {
const reply = await this.readLine();
finiSeen = reply.startsWith("FINI");
}
}
} else {
await this.readLine();
const line = await this.readLine();
if (step.expect && line !== step.expect) {
throw new Error(`ToolDbProcessPort ${step.phase} expected ${step.expect}, got ${line}.`);
}
}
}
return {
@@ -226,9 +236,9 @@ export function createLinuxCncToolDbProcessPort({
return transcript.map((entry) => ({ ...entry }));
},
close() {
async close() {
if (runtimeAdapter?.close) {
runtimeAdapter.close();
await runtimeAdapter.close();
}
closed = true;
return {