From 5c2d5cfa057dce64bf5a4bcc9624b5996712295d Mon Sep 17 00:00:00 2001 From: cnc Date: Wed, 3 Jun 2026 11:22:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=89=80=E6=9C=89=E5=88=86?= =?UTF-8?q?=E7=BB=84=EF=BC=9A=E4=BF=9D=E6=8C=81OPFS=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=9D=9E=E8=87=B4=E5=91=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:按 LinuxCNC rs274ngc_pre.cc save_parameters() 中 unlink/link 失败不阻止 rename 主参数文件的语义,OPFS 参数 .bak 写入失败时不让解析保存失败,并避免把目录当作可 unlink 的备份文件删除;未扩展 smoke 解析行为。 验证:./test-native.sh;./test-linuxcnc-source-link.sh;./test-web-wasm-browser-smoke.sh。 --- web/src/wasm-core.js | 34 +++++++++++++++-- ...wser-wasm-smoke-opfs-parameter-sections.js | 37 ++++++++----------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/web/src/wasm-core.js b/web/src/wasm-core.js index 612e3a6..f4828ad 100644 --- a/web/src/wasm-core.js +++ b/web/src/wasm-core.js @@ -141,6 +141,21 @@ async function removeOpfsEntry(workspacePath, relativePath, recursive) { } } +async function removeOpfsFileEntry(workspacePath, relativePath) { + assertRelativePath(relativePath); + const parts = relativePath.split("/"); + const entryName = parts.pop(); + try { + const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); + await directory.getFileHandle(entryName, { create: false }); + await directory.removeEntry(entryName, { recursive: false }); + } catch (error) { + if (error?.name !== "NotFoundError" && error?.name !== "TypeMismatchError") { + throw error; + } + } +} + async function opfsEntryExists(workspacePath, relativePath) { assertRelativePath(relativePath); const parts = relativePath.split("/"); @@ -498,7 +513,7 @@ function installOpfsWorkspace(module, options) { if (backupData !== null) { await writeOpfsFile(workspacePath, `${path}.bak`, backupData); } else { - await removeOpfsEntry(workspacePath, `${path}.bak`, false); + await removeOpfsFileEntry(workspacePath, `${path}.bak`); } return data; }, @@ -712,9 +727,22 @@ export async function createWasmSimulator(moduleOptions = {}) { // replacing filename. Browser filesystems may not expose that link // through the WASM mirror, so preserve the loaded OPFS file here. if (previousParameterFile === null) { - await opfs.removeFile(`${parameterPath}.bak`); + try { + const backupStat = await opfs.stat(`${parameterPath}.bak`); + if (backupStat.kind === "file") { + await opfs.removeFile(`${parameterPath}.bak`); + } + } catch (error) { + if (error?.name !== "NotFoundError") { + throw error; + } + } } else { - await opfs.writeFile(`${parameterPath}.bak`, previousParameterFile); + try { + await opfs.writeFile(`${parameterPath}.bak`, previousParameterFile); + } catch (_backupError) { + // LinuxCNC treats link(filename, filename + ".bak") failure as non-fatal. + } } return events; } finally { diff --git a/web/test-browser-wasm-smoke-opfs-parameter-sections.js b/web/test-browser-wasm-smoke-opfs-parameter-sections.js index e486809..6c71ac5 100644 --- a/web/test-browser-wasm-smoke-opfs-parameter-sections.js +++ b/web/test-browser-wasm-smoke-opfs-parameter-sections.js @@ -6,6 +6,7 @@ export async function runBrowserOpfsParameterSections(context, simulator) { expectMissingOpfsFile, expectMissingWasmPath, expectNoEvent, + expectOpfsDirectoryStat, expectText, expectWasmFilesMissing, near, @@ -15,14 +16,11 @@ export async function runBrowserOpfsParameterSections(context, simulator) { withSmokeSimulator, } = context; const backend = { backend: "linuxcnc-rs274" }; + const parseWithParameters = (program, path) => + simulator.parseWithParameterFile(program, path, "linuxcnc", backend); await runSection("opfs parameter save", async () => { - await simulator.parseWithParameterFile( - "G21 G90\nG0 X1\nG28.1\nM30\n", - "parameters/rs274ngc.var", - "linuxcnc", - backend, - ); + await simulator.parseWithParameterFile("G21 G90\nG0 X1\nG28.1\nM30\n", "parameters/rs274ngc.var", "linuxcnc", backend); const opfsParameterFile = await readOpfsText(simulator, "parameters/rs274ngc.var"); expectIncludes(opfsParameterFile, "5161\t1.000000", "LinuxCNC parameter file was not persisted into OPFS"); }); @@ -45,12 +43,7 @@ export async function runBrowserOpfsParameterSections(context, simulator) { await runSection("opfs default parameter backup", async () => { // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() // preserves the previous default parameter file as filename + ".bak". - await simulator.parseWithParameterFile( - "G21 G90\nM30\n", - "parameters/rs274ngc.var", - "linuxcnc", - backend, - ); + await parseWithParameters("G21 G90\nM30\n", "parameters/rs274ngc.var"); const defaultParameterBackup = await readOpfsText(simulator, "parameters/rs274ngc.var.bak"); expectIncludes(defaultParameterBackup, "5161\t1.000000", "LinuxCNC default OPFS parameter backup did not preserve the previous file"); }); @@ -60,11 +53,9 @@ export async function runBrowserOpfsParameterSections(context, simulator) { // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes // filename + ".new", then renames it over the parameter file. simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary parameter file\n"); - const restoredParameterEvents = await simulator.parseWithParameterFile( + const restoredParameterEvents = await parseWithParameters( "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n", "parameters/rs274ngc.var", - "linuxcnc", - backend, ); expectEvent(restoredParameterEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), "LinuxCNC parameter file was not restored from OPFS"); @@ -97,12 +88,7 @@ export async function runBrowserOpfsParameterSections(context, simulator) { // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() unlinks // filename + ".bak", links the current parameter file to it, // then renames filename + ".new" over filename. - await simulator.parseWithParameterFile( - "G21 G90\nG0 X4\nG28.1\nM30\n", - "parameters/existing-save.var", - "linuxcnc", - backend, - ); + await parseWithParameters("G21 G90\nG0 X4\nG28.1\nM30\n", "parameters/existing-save.var"); const existingSaveParameterFile = await readOpfsText(simulator, "parameters/existing-save.var"); expectIncludes(existingSaveParameterFile, "5161\t4.000000", "LinuxCNC existing OPFS parameter file was not replaced after save"); const existingSaveParameterBackup = await readOpfsText(simulator, "parameters/existing-save.var.bak"); @@ -111,6 +97,15 @@ export async function runBrowserOpfsParameterSections(context, simulator) { () => simulator.fs.opfs.readFile("parameters/existing-save.var.new"), "LinuxCNC existing OPFS parameter temporary file remained after save", ); + await simulator.fs.opfs.writeFile("parameters/backup-directory.var", "5161\t6.000000\n"); + const backupDirectory = simulator.fs.opfs.resolvePath("parameters/backup-directory.var.bak"); + simulator.fs.module.FS.mkdir(backupDirectory); + await simulator.fs.opfs.persistDirectory("parameters/backup-directory.var.bak"); + await parseWithParameters("G21 G90\nG0 X7\nG28.1\nM30\n", "parameters/backup-directory.var"); + expectIncludes(await readOpfsText(simulator, "parameters/backup-directory.var"), "5161\t7.000000", + "LinuxCNC OPFS parameter file was not saved when backup link failed"); + await expectOpfsDirectoryStat(simulator, "parameters/backup-directory.var.bak", + "LinuxCNC non-fatal OPFS backup failure did not leave the existing backup path intact"); }); await runSection("opfs parameter stale backup cleanup", async () => {