From 84bdc819b46061ca3ad6f17ce9b6f1ce8f38c390 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 8 Jun 2026 03:26:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E5=BB=BA=E8=AE=AE=EF=BC=8C=E7=BB=A7?= =?UTF-8?q?=E7=BB=AD=E5=AE=8C=E6=88=90=E5=90=8E=E7=BB=AD=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:已新增 OPFS host-side 路径模型,覆盖 INI、tool table、parameter、G-code、preview cache 与 session snapshot 存储目标,并让 file-service 和浏览器 smoke 复用该模型;聚合 smoke 验证通过。 --- wasm-port/docs/compatibility-validation.md | 6 +- wasm-port/docs/drift-report.md | 7 +- wasm-port/docs/source-reuse-map.md | 9 +- wasm-port/runtime/opfs/file-service.js | 21 +--- wasm-port/runtime/opfs/path-model.js | 101 ++++++++++++++++++ wasm-port/tests/browser/ini_panel_smoke.html | 3 +- .../tests/opfs/node/verify_file_service.mjs | 27 +++++ 7 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 wasm-port/runtime/opfs/path-model.js diff --git a/wasm-port/docs/compatibility-validation.md b/wasm-port/docs/compatibility-validation.md index 3864499..0d93435 100644 --- a/wasm-port/docs/compatibility-validation.md +++ b/wasm-port/docs/compatibility-validation.md @@ -67,7 +67,9 @@ queries against a file written to the Emscripten filesystem. 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 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 headless against a test page that imports the JS SDK, loads the INI WASM @@ -124,7 +126,7 @@ The validation fails if: | 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/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/host/verify_host_smokes.sh` | Runs the current host-side Node, WASM, OPFS, and browser smoke validation with a single INI WASM build. | diff --git a/wasm-port/docs/drift-report.md b/wasm-port/docs/drift-report.md index d74dc90..eed116c 100644 --- a/wasm-port/docs/drift-report.md +++ b/wasm-port/docs/drift-report.md @@ -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. | | 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. | -| 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 @@ -60,8 +60,9 @@ semantic rewrites: Chromium smoke harnesses against vendored LinuxCNC `inifile.cc`. - JS SDK validation is currently limited to the INI WASM wrapper around vendored LinuxCNC `inifile.cc`. -- OPFS validation covers a Node mock of the file-service adapter and a - Chromium localhost round trip for INI text-file persistence. +- OPFS validation covers a Node mock of the file-service adapter, the + host-side path model, and a Chromium localhost round trip for INI text-file + persistence. - Host-side smoke validation is aggregated by `tests/host/verify_host_smokes.sh` so Node, WASM, OPFS, and browser checks run from one command. diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index 987a0f2..5c2ed60 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -51,7 +51,7 @@ Current validation is intentionally mechanical: | 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` | | 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/` | @@ -72,7 +72,8 @@ Current validation is intentionally mechanical: interpreter/planner core. The INI parser has a minimal JS SDK wrapper and Node/browser WASM smoke harnesses. - OPFS persistence is connected to the INI panel through the host-side - `runtime/opfs/file-service.js` adapter; tool table, parameter file, G-code - program loading, and broader browser-level OPFS validation remain future - work. + `runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now + defines paths for INI, tool table, parameter file, G-code program, + 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. diff --git a/wasm-port/runtime/opfs/file-service.js b/wasm-port/runtime/opfs/file-service.js index 0d200a5..4dc68b7 100644 --- a/wasm-port/runtime/opfs/file-service.js +++ b/wasm-port/runtime/opfs/file-service.js @@ -1,17 +1,4 @@ -function splitPath(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; -} +import { splitOpfsPath } from "./path-model.js"; export async function getOpfsRoot(storage = globalThis.navigator?.storage) { if (!storage?.getDirectory) { @@ -21,7 +8,7 @@ export async function getOpfsRoot(storage = globalThis.navigator?.storage) { } export async function ensureParentDir(root, path) { - const parts = splitPath(path); + const parts = splitOpfsPath(path); let current = root; for (const part of parts.slice(0, -1)) { current = await current.getDirectoryHandle(part, { create: true }); @@ -32,7 +19,7 @@ export async function ensureParentDir(root, path) { export async function saveTextFile(path, text, storage) { const root = await getOpfsRoot(storage); 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 writable = await fileHandle.createWritable(); await writable.write(text); @@ -41,7 +28,7 @@ export async function saveTextFile(path, text, storage) { export async function loadTextFile(path, storage) { const root = await getOpfsRoot(storage); - const parts = splitPath(path); + const parts = splitOpfsPath(path); let current = root; for (const part of parts.slice(0, -1)) { current = await current.getDirectoryHandle(part); diff --git a/wasm-port/runtime/opfs/path-model.js b/wasm-port/runtime/opfs/path-model.js new file mode 100644 index 0000000..fe6ac86 --- /dev/null +++ b/wasm-port/runtime/opfs/path-model.js @@ -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), + }; +} diff --git a/wasm-port/tests/browser/ini_panel_smoke.html b/wasm-port/tests/browser/ini_panel_smoke.html index f1cd728..192b8ab 100644 --- a/wasm-port/tests/browser/ini_panel_smoke.html +++ b/wasm-port/tests/browser/ini_panel_smoke.html @@ -9,6 +9,7 @@