结论:ToolDbProcessPort 已可通过 Node/native adapter 真实执行 LinuxCNC DB_PROGRAM 协议,浏览器端仍保持锁定直到 Python/WASM Worker 接入。
161 lines
4.7 KiB
JavaScript
161 lines
4.7 KiB
JavaScript
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();
|
|
},
|
|
};
|
|
}
|