按建议,继续完成后续工作
结论:已新增 OPFS host-side 路径模型,覆盖 INI、tool table、parameter、G-code、preview cache 与 session snapshot 存储目标,并让 file-service 和浏览器 smoke 复用该模型;聚合 smoke 验证通过。
This commit is contained in:
@@ -67,7 +67,9 @@ queries against a file written to the Emscripten filesystem.
|
|||||||
The OPFS host-boundary script validates the JavaScript file-service adapter
|
The OPFS host-boundary script validates the JavaScript file-service adapter
|
||||||
with a Node mock of the browser File System Access handles. It covers nested
|
with a Node mock of the browser File System Access handles. It covers nested
|
||||||
directory creation, text save/load, missing file behavior, invalid relative
|
directory creation, text save/load, missing file behavior, invalid relative
|
||||||
paths, and unavailable OPFS storage.
|
paths, unavailable OPFS storage, and the host-side OPFS path model for INI,
|
||||||
|
tool table, parameter, G-code, preview-cache, and session-snapshot storage
|
||||||
|
targets.
|
||||||
|
|
||||||
The browser smoke script serves `wasm-port/` over localhost and runs Chromium
|
The browser smoke script serves `wasm-port/` over localhost and runs Chromium
|
||||||
headless against a test page that imports the JS SDK, loads the INI WASM
|
headless against a test page that imports the JS SDK, loads the INI WASM
|
||||||
@@ -124,7 +126,7 @@ The validation fails if:
|
|||||||
| Harness | Purpose |
|
| Harness | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. |
|
| `tests/wasm/node/verify_ini_wasm.sh` | Validates the browser-facing INI WASM module can be built from vendored LinuxCNC `inifile.cc`, loaded through the JS SDK in Node, and queried through the exported C ABI. |
|
||||||
| `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter used by the browser INI panel without moving file persistence into the WASM core. |
|
| `tests/opfs/node/verify_file_service.sh` | Validates the host-owned OPFS text-file adapter and path model used by the browser INI panel without moving file persistence into the WASM core. |
|
||||||
| `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, and OPFS text-file round trip in a real browser runtime. |
|
| `tests/browser/verify_ini_panel_browser.sh` | Validates the INI SDK, WASM module loading, and OPFS text-file round trip in a real browser runtime. |
|
||||||
| `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM, OPFS, and browser smoke validation with a single INI WASM build. |
|
| `tests/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM, OPFS, and browser smoke validation with a single INI WASM build. |
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ semantic rewrites:
|
|||||||
| Kinematics component lifecycle | Kinematics modules are initialized through LinuxCNC module entry points where native runtime probes exist, while HAL component init/ready/exit, HAL pin allocation, and RTAPI module metadata are handled by standalone shims. |
|
| Kinematics component lifecycle | Kinematics modules are initialized through LinuxCNC module entry points where native runtime probes exist, while HAL component init/ready/exit, HAL pin allocation, and RTAPI module metadata are handled by standalone shims. |
|
||||||
| Go math C/C++ linkage | `genserkins` runtime probing compiles vendored `gomath.c` through a narrow C++ wrapper so LinuxCNC `genserfuncs.c` can link to the upstream Go math symbols without editing vendored source. |
|
| Go math C/C++ linkage | `genserkins` runtime probing compiles vendored `gomath.c` through a narrow C++ wrapper so LinuxCNC `genserfuncs.c` can link to the upstream Go math symbols without editing vendored source. |
|
||||||
| Switchkins iterative forward | `genhexkins` runtime probing follows LinuxCNC switchkins iterative-forward behavior, including the first-call warmup path before asserting roundtrip convergence. |
|
| Switchkins iterative forward | `genhexkins` runtime probing follows LinuxCNC switchkins iterative-forward behavior, including the first-call warmup path before asserting roundtrip convergence. |
|
||||||
| Browser storage | OPFS remains outside the native core; `runtime/opfs/file-service.js` owns browser text-file persistence for host-managed INI content. |
|
| Browser storage | OPFS remains outside the native core; `runtime/opfs/file-service.js` owns browser text-file persistence and `runtime/opfs/path-model.js` owns host-side storage paths for INI, tool table, parameter, G-code, preview-cache, and session-snapshot content. |
|
||||||
|
|
||||||
## Enforced Non-Drift Rules
|
## Enforced Non-Drift Rules
|
||||||
|
|
||||||
@@ -60,8 +60,9 @@ semantic rewrites:
|
|||||||
Chromium smoke harnesses against vendored LinuxCNC `inifile.cc`.
|
Chromium smoke harnesses against vendored LinuxCNC `inifile.cc`.
|
||||||
- JS SDK validation is currently limited to the INI WASM wrapper around
|
- JS SDK validation is currently limited to the INI WASM wrapper around
|
||||||
vendored LinuxCNC `inifile.cc`.
|
vendored LinuxCNC `inifile.cc`.
|
||||||
- OPFS validation covers a Node mock of the file-service adapter and a
|
- OPFS validation covers a Node mock of the file-service adapter, the
|
||||||
Chromium localhost round trip for INI text-file persistence.
|
host-side path model, and a Chromium localhost round trip for INI text-file
|
||||||
|
persistence.
|
||||||
- Host-side smoke validation is aggregated by
|
- Host-side smoke validation is aggregated by
|
||||||
`tests/host/verify_host_smokes.sh` so Node, WASM, OPFS, and browser checks
|
`tests/host/verify_host_smokes.sh` so Node, WASM, OPFS, and browser checks
|
||||||
run from one command.
|
run from one command.
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Current validation is intentionally mechanical:
|
|||||||
|
|
||||||
| Dependency | LinuxCNC files that expose it | Standalone treatment |
|
| Dependency | LinuxCNC files that expose it | Standalone treatment |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; browser OPFS remains a host-side adapter under `runtime/opfs/` |
|
| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, parameter file paths | Allowed in native probes; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js` |
|
||||||
| RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` |
|
| RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` |
|
||||||
| NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes |
|
| NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes |
|
||||||
| HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` |
|
| HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` |
|
||||||
@@ -72,7 +72,8 @@ Current validation is intentionally mechanical:
|
|||||||
interpreter/planner core. The INI parser has a minimal JS SDK wrapper and
|
interpreter/planner core. The INI parser has a minimal JS SDK wrapper and
|
||||||
Node/browser WASM smoke harnesses.
|
Node/browser WASM smoke harnesses.
|
||||||
- OPFS persistence is connected to the INI panel through the host-side
|
- OPFS persistence is connected to the INI panel through the host-side
|
||||||
`runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code
|
`runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now
|
||||||
program loading, and broader browser-level OPFS validation remain future
|
defines paths for INI, tool table, parameter file, G-code program,
|
||||||
work.
|
preview-cache, and session-snapshot targets; semantic loading for tool
|
||||||
|
tables, parameter files, and G-code programs remains future work.
|
||||||
- Native LinuxCNC GUI code remains out of scope for implementation.
|
- Native LinuxCNC GUI code remains out of scope for implementation.
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
function splitPath(path) {
|
import { splitOpfsPath } from "./path-model.js";
|
||||||
if (typeof path !== "string" || path.length === 0) {
|
|
||||||
throw new Error("OPFS path must be a non-empty relative path.");
|
|
||||||
}
|
|
||||||
if (path.startsWith("/") || path.includes("//")) {
|
|
||||||
throw new Error(`Invalid OPFS path: ${path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = path.split("/");
|
|
||||||
if (parts.some((part) => part === "." || part === ".." || part === "")) {
|
|
||||||
throw new Error(`Invalid OPFS path: ${path}`);
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getOpfsRoot(storage = globalThis.navigator?.storage) {
|
export async function getOpfsRoot(storage = globalThis.navigator?.storage) {
|
||||||
if (!storage?.getDirectory) {
|
if (!storage?.getDirectory) {
|
||||||
@@ -21,7 +8,7 @@ export async function getOpfsRoot(storage = globalThis.navigator?.storage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureParentDir(root, path) {
|
export async function ensureParentDir(root, path) {
|
||||||
const parts = splitPath(path);
|
const parts = splitOpfsPath(path);
|
||||||
let current = root;
|
let current = root;
|
||||||
for (const part of parts.slice(0, -1)) {
|
for (const part of parts.slice(0, -1)) {
|
||||||
current = await current.getDirectoryHandle(part, { create: true });
|
current = await current.getDirectoryHandle(part, { create: true });
|
||||||
@@ -32,7 +19,7 @@ export async function ensureParentDir(root, path) {
|
|||||||
export async function saveTextFile(path, text, storage) {
|
export async function saveTextFile(path, text, storage) {
|
||||||
const root = await getOpfsRoot(storage);
|
const root = await getOpfsRoot(storage);
|
||||||
const dir = await ensureParentDir(root, path);
|
const dir = await ensureParentDir(root, path);
|
||||||
const filename = splitPath(path).at(-1);
|
const filename = splitOpfsPath(path).at(-1);
|
||||||
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
const fileHandle = await dir.getFileHandle(filename, { create: true });
|
||||||
const writable = await fileHandle.createWritable();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(text);
|
await writable.write(text);
|
||||||
@@ -41,7 +28,7 @@ export async function saveTextFile(path, text, storage) {
|
|||||||
|
|
||||||
export async function loadTextFile(path, storage) {
|
export async function loadTextFile(path, storage) {
|
||||||
const root = await getOpfsRoot(storage);
|
const root = await getOpfsRoot(storage);
|
||||||
const parts = splitPath(path);
|
const parts = splitOpfsPath(path);
|
||||||
let current = root;
|
let current = root;
|
||||||
for (const part of parts.slice(0, -1)) {
|
for (const part of parts.slice(0, -1)) {
|
||||||
current = await current.getDirectoryHandle(part);
|
current = await current.getDirectoryHandle(part);
|
||||||
|
|||||||
101
wasm-port/runtime/opfs/path-model.js
Normal file
101
wasm-port/runtime/opfs/path-model.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const ROOT = "linuxcnc";
|
||||||
|
|
||||||
|
function assertSegment(segment, label = "path segment") {
|
||||||
|
if (typeof segment !== "string" || segment.length === 0) {
|
||||||
|
throw new Error(`${label} must be a non-empty string.`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
segment === "." ||
|
||||||
|
segment === ".." ||
|
||||||
|
segment.includes("/") ||
|
||||||
|
segment.includes("\\") ||
|
||||||
|
segment.includes("\0")
|
||||||
|
) {
|
||||||
|
throw new Error(`Invalid ${label}: ${segment}`);
|
||||||
|
}
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitOpfsPath(path) {
|
||||||
|
if (typeof path !== "string" || path.length === 0) {
|
||||||
|
throw new Error("OPFS path must be a non-empty relative path.");
|
||||||
|
}
|
||||||
|
if (path.startsWith("/") || path.includes("//")) {
|
||||||
|
throw new Error(`Invalid OPFS path: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = path.split("/");
|
||||||
|
if (parts.some((part) => part === "." || part === ".." || part === "")) {
|
||||||
|
throw new Error(`Invalid OPFS path: ${path}`);
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOpfsPath(path) {
|
||||||
|
return splitOpfsPath(path).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function opfsPath(...segments) {
|
||||||
|
return segments.map((segment) => assertSegment(segment)).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function machineRoot(machineId) {
|
||||||
|
return opfsPath(ROOT, "machines", assertSegment(machineId, "machine id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function machineIniPath(machineId, filename = "machine.ini") {
|
||||||
|
return opfsPath(
|
||||||
|
ROOT,
|
||||||
|
"machines",
|
||||||
|
assertSegment(machineId, "machine id"),
|
||||||
|
assertSegment(filename, "INI filename"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toolTablePath(machineId, filename = "tool.tbl") {
|
||||||
|
return opfsPath(
|
||||||
|
ROOT,
|
||||||
|
"machines",
|
||||||
|
assertSegment(machineId, "machine id"),
|
||||||
|
assertSegment(filename, "tool table filename"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parameterFilePath(machineId, filename = "linuxcnc.var") {
|
||||||
|
return opfsPath(
|
||||||
|
ROOT,
|
||||||
|
"machines",
|
||||||
|
assertSegment(machineId, "machine id"),
|
||||||
|
assertSegment(filename, "parameter filename"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gcodeProgramPath(filename) {
|
||||||
|
return opfsPath(ROOT, "gcode", assertSegment(filename, "G-code filename"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewCachePath(cacheKey, filename = "preview.json") {
|
||||||
|
return opfsPath(
|
||||||
|
ROOT,
|
||||||
|
"preview-cache",
|
||||||
|
assertSegment(cacheKey, "preview cache key"),
|
||||||
|
assertSegment(filename, "preview cache filename"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sessionSnapshotPath(sessionId, filename = "snapshot.json") {
|
||||||
|
return opfsPath(
|
||||||
|
ROOT,
|
||||||
|
"sessions",
|
||||||
|
assertSegment(sessionId, "session id"),
|
||||||
|
assertSegment(filename, "session snapshot filename"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultMachinePaths(machineId) {
|
||||||
|
return {
|
||||||
|
ini: machineIniPath(machineId),
|
||||||
|
toolTable: toolTablePath(machineId),
|
||||||
|
parameters: parameterFilePath(machineId),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/linuxcnc-ini.js";
|
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/linuxcnc-ini.js";
|
||||||
import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
|
import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
|
||||||
|
import { machineIniPath } from "../../runtime/opfs/path-model.js";
|
||||||
|
|
||||||
const status = document.getElementById("status");
|
const status = document.getElementById("status");
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ JOINTS = 3
|
|||||||
assertEqual(fields.machine, "browser-smoke", "field machine");
|
assertEqual(fields.machine, "browser-smoke", "field machine");
|
||||||
assertEqual(fields.joints, "3", "field joints");
|
assertEqual(fields.joints, "3", "field joints");
|
||||||
|
|
||||||
const opfsPath = "linuxcnc/browser-smoke.ini";
|
const opfsPath = machineIniPath("browser-smoke");
|
||||||
await saveTextFile(opfsPath, iniText);
|
await saveTextFile(opfsPath, iniText);
|
||||||
assertEqual(await loadTextFile(opfsPath), iniText, "opfs roundtrip");
|
assertEqual(await loadTextFile(opfsPath), iniText, "opfs roundtrip");
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ import {
|
|||||||
loadTextFile,
|
loadTextFile,
|
||||||
saveTextFile,
|
saveTextFile,
|
||||||
} from "../../../runtime/opfs/file-service.js";
|
} from "../../../runtime/opfs/file-service.js";
|
||||||
|
import {
|
||||||
|
defaultMachinePaths,
|
||||||
|
gcodeProgramPath,
|
||||||
|
machineIniPath,
|
||||||
|
normalizeOpfsPath,
|
||||||
|
parameterFilePath,
|
||||||
|
previewCachePath,
|
||||||
|
sessionSnapshotPath,
|
||||||
|
toolTablePath,
|
||||||
|
} from "../../../runtime/opfs/path-model.js";
|
||||||
|
|
||||||
class MockFileHandle {
|
class MockFileHandle {
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
@@ -63,6 +73,19 @@ const storage = {
|
|||||||
|
|
||||||
assert.equal(await getOpfsRoot(storage), root);
|
assert.equal(await getOpfsRoot(storage), root);
|
||||||
|
|
||||||
|
assert.equal(normalizeOpfsPath("linuxcnc/machines/xyzab.ini"), "linuxcnc/machines/xyzab.ini");
|
||||||
|
assert.equal(machineIniPath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/machine.ini");
|
||||||
|
assert.equal(toolTablePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/tool.tbl");
|
||||||
|
assert.equal(parameterFilePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/linuxcnc.var");
|
||||||
|
assert.equal(gcodeProgramPath("fixture.ngc"), "linuxcnc/gcode/fixture.ngc");
|
||||||
|
assert.equal(previewCachePath("fixture"), "linuxcnc/preview-cache/fixture/preview.json");
|
||||||
|
assert.equal(sessionSnapshotPath("session-1"), "linuxcnc/sessions/session-1/snapshot.json");
|
||||||
|
assert.deepEqual(defaultMachinePaths("xyzab-tdr"), {
|
||||||
|
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
|
||||||
|
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
|
||||||
|
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
|
||||||
|
});
|
||||||
|
|
||||||
await saveTextFile(
|
await saveTextFile(
|
||||||
"linuxcnc/machines/xyzab.ini",
|
"linuxcnc/machines/xyzab.ini",
|
||||||
"[EMC]\nMACHINE = opfs-smoke\n",
|
"[EMC]\nMACHINE = opfs-smoke\n",
|
||||||
@@ -81,6 +104,10 @@ await assert.rejects(
|
|||||||
() => saveTextFile("../escape.ini", "", storage),
|
() => saveTextFile("../escape.ini", "", storage),
|
||||||
/Invalid OPFS path/,
|
/Invalid OPFS path/,
|
||||||
);
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => machineIniPath("../escape"),
|
||||||
|
/Invalid machine id/,
|
||||||
|
);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => getOpfsRoot({}),
|
() => getOpfsRoot({}),
|
||||||
/OPFS is not available/,
|
/OPFS is not available/,
|
||||||
|
|||||||
Reference in New Issue
Block a user