| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(api): stream raw request bodies to the watch directory (#1014) * feat(api): stream raw request bodies to the watch directory `PUT /api/watch/file` only accepted a JSON body, so the entire file had to arrive as a JSON string and be held in memory: body-parser buffers the request, parses it into a JavaScript string, and `monitor.writeFile()` then serialises that string back to disk. Peak RSS grows to several times the file size — around 600 MB for a 107 MiB program. This turned up while writing a native macOS/iOS client that uses cncjs as its backend and uploads large programs to the watch directory. Accept a raw streamed body as an alternative. When the request has no parsed body (any content type body-parser does not handle, such as `application/octet-stream`), the file name is read from the query string and the request is piped straight to disk by a new `monitor.writeStream()`. The JSON path is untouched: a parsed body still goes through `monitor.writeFile()` with the same arguments, the same response shape and the same "No file specified" error, so existing clients are unaffected. `writeStream()` writes to a temporary dotfile and renames it into place on completion, so a half-written program never appears under its final name in the watched directory, and removes the temporary file if the upload errors or is aborted. Directory components are stripped from the file name, as `writeFile()` already does. Measured on a real 112,678,627 byte Estlcam program, three runs each, with a fresh server process per run: workstation JSON +537..659 MB raw +40..46 MB LattePanda JSON +621..643 MB raw +29..69 MB The LattePanda figures are over the network, on the kind of single-board computer cncjs is typically deployed to. Both produce byte-identical files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(api): select the raw upload path by content type Review feedback from @cheton on #1014. Inferring the upload kind from whether body-parser produced anything was wrong: an empty JSON body parses to an empty object, which is indistinguishable from "not parsed". `PUT /api/watch/file?file=x.nc` with `Content-Type: application/json` and a body of `{}` therefore took the streaming path and wrote a zero-byte file, where before this feature it answered 400 "No file specified" — a change to the JSON path that this series claimed not to make. An unsupported content type was silently treated as a raw upload for the same reason. Take the kind from the content type instead, and answer 415 for anything that is neither `application/octet-stream` nor `application/json`, so the contract is explicit rather than a consequence of what body-parser happened to do. Adds `src/server/api/__tests__/api.watch.test.js` covering both paths, the empty-JSON-body case, 415 for an unsupported and for an absent content type, and the missing-name case on each path. `415` gets a named constant alongside the other statuses in `constants/index.js`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(monitor): stream through pipeline() and bound the upload size Review feedback from @cheton on #1014. The streamed path had no size limit. A JSON body is bounded by body-parser's configured 256mb, but `application/octet-stream` went straight to disk, so an authenticated caller could write until the filesystem filled — on a machine that is often a single-board computer with a small card, and whose whole job is to keep feeding a running machine. Adds `middleware.upload.maxFileSize` (256MB, matching the JSON limit), passed through an options object rather than another positional argument, and mapped to 413 by the API layer. The limit counts bytes as they pass rather than trusting Content-Length, so a body that understates its length or sends none is bounded too. Rewrites `writeStream()` on `stream.pipeline()`. The hand-rolled version coordinated `error`, `aborted`, `close`, `finish`, `unpipe()`, `destroy()` and a `settled` flag to get right what pipeline already does: propagate errors in both directions, apply backpressure, destroy every participating stream on failure, and report a client that disconnects mid-upload as a premature close. Verified against a running server: a 300MB body answers 413 with no file and no temporary file left behind, an aborted upload still leaves nothing, and the JSON path, the 415 rejection and the empty-JSON-body case are unchanged. Re-measured the 112,678,627 byte program through the new implementation over three runs — peak RSS +43.4/+43.6/+44.0 MB against +40..46 MB before, so the figures quoted in the PR still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 1 个月前 | |
feat(api): stream raw request bodies to the watch directory (#1014) * feat(api): stream raw request bodies to the watch directory `PUT /api/watch/file` only accepted a JSON body, so the entire file had to arrive as a JSON string and be held in memory: body-parser buffers the request, parses it into a JavaScript string, and `monitor.writeFile()` then serialises that string back to disk. Peak RSS grows to several times the file size — around 600 MB for a 107 MiB program. This turned up while writing a native macOS/iOS client that uses cncjs as its backend and uploads large programs to the watch directory. Accept a raw streamed body as an alternative. When the request has no parsed body (any content type body-parser does not handle, such as `application/octet-stream`), the file name is read from the query string and the request is piped straight to disk by a new `monitor.writeStream()`. The JSON path is untouched: a parsed body still goes through `monitor.writeFile()` with the same arguments, the same response shape and the same "No file specified" error, so existing clients are unaffected. `writeStream()` writes to a temporary dotfile and renames it into place on completion, so a half-written program never appears under its final name in the watched directory, and removes the temporary file if the upload errors or is aborted. Directory components are stripped from the file name, as `writeFile()` already does. Measured on a real 112,678,627 byte Estlcam program, three runs each, with a fresh server process per run: workstation JSON +537..659 MB raw +40..46 MB LattePanda JSON +621..643 MB raw +29..69 MB The LattePanda figures are over the network, on the kind of single-board computer cncjs is typically deployed to. Both produce byte-identical files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(api): select the raw upload path by content type Review feedback from @cheton on #1014. Inferring the upload kind from whether body-parser produced anything was wrong: an empty JSON body parses to an empty object, which is indistinguishable from "not parsed". `PUT /api/watch/file?file=x.nc` with `Content-Type: application/json` and a body of `{}` therefore took the streaming path and wrote a zero-byte file, where before this feature it answered 400 "No file specified" — a change to the JSON path that this series claimed not to make. An unsupported content type was silently treated as a raw upload for the same reason. Take the kind from the content type instead, and answer 415 for anything that is neither `application/octet-stream` nor `application/json`, so the contract is explicit rather than a consequence of what body-parser happened to do. Adds `src/server/api/__tests__/api.watch.test.js` covering both paths, the empty-JSON-body case, 415 for an unsupported and for an absent content type, and the missing-name case on each path. `415` gets a named constant alongside the other statuses in `constants/index.js`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(monitor): stream through pipeline() and bound the upload size Review feedback from @cheton on #1014. The streamed path had no size limit. A JSON body is bounded by body-parser's configured 256mb, but `application/octet-stream` went straight to disk, so an authenticated caller could write until the filesystem filled — on a machine that is often a single-board computer with a small card, and whose whole job is to keep feeding a running machine. Adds `middleware.upload.maxFileSize` (256MB, matching the JSON limit), passed through an options object rather than another positional argument, and mapped to 413 by the API layer. The limit counts bytes as they pass rather than trusting Content-Length, so a body that understates its length or sends none is bounded too. Rewrites `writeStream()` on `stream.pipeline()`. The hand-rolled version coordinated `error`, `aborted`, `close`, `finish`, `unpipe()`, `destroy()` and a `settled` flag to get right what pipeline already does: propagate errors in both directions, apply backpressure, destroy every participating stream on failure, and report a client that disconnects mid-upload as a premature close. Verified against a running server: a 300MB body answers 413 with no file and no temporary file left behind, an aborted upload still leaves nothing, and the JSON path, the 415 rejection and the empty-JSON-body case are unchanged. Re-measured the 112,678,627 byte program through the new implementation over three runs — peak RSS +43.4/+43.6/+44.0 MB against +40..46 MB before, so the figures quoted in the PR still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 1 个月前 | |
feat(api): stream raw request bodies to the watch directory (#1014) * feat(api): stream raw request bodies to the watch directory `PUT /api/watch/file` only accepted a JSON body, so the entire file had to arrive as a JSON string and be held in memory: body-parser buffers the request, parses it into a JavaScript string, and `monitor.writeFile()` then serialises that string back to disk. Peak RSS grows to several times the file size — around 600 MB for a 107 MiB program. This turned up while writing a native macOS/iOS client that uses cncjs as its backend and uploads large programs to the watch directory. Accept a raw streamed body as an alternative. When the request has no parsed body (any content type body-parser does not handle, such as `application/octet-stream`), the file name is read from the query string and the request is piped straight to disk by a new `monitor.writeStream()`. The JSON path is untouched: a parsed body still goes through `monitor.writeFile()` with the same arguments, the same response shape and the same "No file specified" error, so existing clients are unaffected. `writeStream()` writes to a temporary dotfile and renames it into place on completion, so a half-written program never appears under its final name in the watched directory, and removes the temporary file if the upload errors or is aborted. Directory components are stripped from the file name, as `writeFile()` already does. Measured on a real 112,678,627 byte Estlcam program, three runs each, with a fresh server process per run: workstation JSON +537..659 MB raw +40..46 MB LattePanda JSON +621..643 MB raw +29..69 MB The LattePanda figures are over the network, on the kind of single-board computer cncjs is typically deployed to. Both produce byte-identical files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(api): select the raw upload path by content type Review feedback from @cheton on #1014. Inferring the upload kind from whether body-parser produced anything was wrong: an empty JSON body parses to an empty object, which is indistinguishable from "not parsed". `PUT /api/watch/file?file=x.nc` with `Content-Type: application/json` and a body of `{}` therefore took the streaming path and wrote a zero-byte file, where before this feature it answered 400 "No file specified" — a change to the JSON path that this series claimed not to make. An unsupported content type was silently treated as a raw upload for the same reason. Take the kind from the content type instead, and answer 415 for anything that is neither `application/octet-stream` nor `application/json`, so the contract is explicit rather than a consequence of what body-parser happened to do. Adds `src/server/api/__tests__/api.watch.test.js` covering both paths, the empty-JSON-body case, 415 for an unsupported and for an absent content type, and the missing-name case on each path. `415` gets a named constant alongside the other statuses in `constants/index.js`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(monitor): stream through pipeline() and bound the upload size Review feedback from @cheton on #1014. The streamed path had no size limit. A JSON body is bounded by body-parser's configured 256mb, but `application/octet-stream` went straight to disk, so an authenticated caller could write until the filesystem filled — on a machine that is often a single-board computer with a small card, and whose whole job is to keep feeding a running machine. Adds `middleware.upload.maxFileSize` (256MB, matching the JSON limit), passed through an options object rather than another positional argument, and mapped to 413 by the API layer. The limit counts bytes as they pass rather than trusting Content-Length, so a body that understates its length or sends none is bounded too. Rewrites `writeStream()` on `stream.pipeline()`. The hand-rolled version coordinated `error`, `aborted`, `close`, `finish`, `unpipe()`, `destroy()` and a `settled` flag to get right what pipeline already does: propagate errors in both directions, apply backpressure, destroy every participating stream on failure, and report a client that disconnects mid-upload as a premature close. Verified against a running server: a 300MB body answers 413 with no file and no temporary file left behind, an aborted upload still leaves nothing, and the JSON path, the 415 rejection and the empty-JSON-body case are unchanged. Re-measured the 112,678,627 byte program through the new implementation over three runs — peak RSS +43.4/+43.6/+44.0 MB against +40..46 MB before, so the figures quoted in the PR still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 1 个月前 | |
fix(grbl): parse and maintain `Pn:` and `A:` status fields (#1013) * fix(grbl): populate and clear the Pn: input pin state `pinState` was never populated, so probe and limit pin state never reached clients. There are two defects. 1. Parsing. The status tokenizer is /[a-zA-Z]+(:[0-9\.\-]+(,[0-9\.\-]+){0,5})?/g whose value class accepts only digits, dot and minus. Grbl v1.1 reports input pins as letters (`Pn:XYZPDHRS`), so `Pn:X` never matched as a name/value pair: the regex captured a bare `Pn`, the `^(.+):(.+)` split below found no colon, and `result.Pn` was never set. That made the existing if (_.has(result, 'Pn')) { payload.pinState = ...; } branch unreachable. The accessory field `A:SFM` was dropped for the same reason. Fixed by allowing letters in the first value after `:` only; comma-continued values stay numeric so v0.9 comma-separated field boundaries (`MPos:..,WPos:..`) still split exactly as before. 2. Clearing. Grbl omits `Pn:` entirely once nothing is triggered, so `{ ...this.state.status, ...payload }` inherited the previous value and a pin stayed latched forever after one trigger — a limit switch brushed once read as permanently closed. Absence of the field is itself the all-clear, so it is now set explicitly before the merge. Verified against FluidNC hardware: probe, touch plate, toolsetter and the X/Y/Z limits all report and clear correctly. Adds a regression test covering a single pin, multiple pins and the clear, and updates three existing status tests that predate `pinState` always being present. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grbl): preserve accessory state between refreshes * docs(changeset): describe Grbl Pn and A state handling * chore: rename fix-grbl-pinstate.md to pr-1013-grbl-pinstate.md --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: cheton <cheton@gmail.com> Co-authored-by: Cheton Wu <447801+cheton@users.noreply.github.com> | 1 个月前 | |
fix(i18n): rename `pt-pt` to `pt` for European Portuguese (Portugal) (#886) * i18n: rename `pt-pt` to `pt` for Portuguese (Portugal) * chore: update translations * chore: change indent from 4 space to 2 space for i18n translation files | 1 年前 | |
fix(autolevel): store probe measurements at the intended grid node's XY (#1001) * fix(autolevel): store probe measurements at the intended grid node's XY The four controllers recorded the XY the machine reported back with the probe result. That XY is quantised by the motor steps -- a commanded Y60 reads back as 59.999 -- so the same commanded coordinate lands on different values across a run. Compensation detects the grid from the probed data, so one grid line splits into two 0.001mm apart: the detected spacing collapses to 0.001mm and the lattice fills with holes. Every move is then subdivided every 0.0005mm and interpolated over a broken surface via the plane-fit fallback. Only Z is a measurement; the XY is the node the machine was told to go to and was known before probing. Grbl, Marlin, Smoothie and TinyG now overwrite the reported XY with `probePoints[probedPositions.length]` before pushing the point, so the stored position, the `autolevel:update` payload the widget accumulates, and the per-point progress log all carry the exact node. Each controller's existing debug line runs before the overwrite and still shows the raw reported position, which is where the quantisation stays visible. The probe points carry the units the probe G-code was emitted in, while probe data is stored in mm, so the node XY is converted under G20. Marlin, Smoothie and TinyG reuse the `isImperial` they already derive from the modal state; Grbl reads the modal state separately, since its existing `$13` flag governs the PRB report only. Adds two tests to src/server/lib/__tests__/autolevel.test.js that feed applyProbeCompensation a 3x3 grid whose middle row is split at Y10/Y9.999 and characterise both consequences: the output grows by three orders of magnitude, and the compensation at a cell centre is lost on a saddle surface. They characterise the library's sensitivity to quantised input rather than guard the controllers, which have no test harness in this repo. Adds .changeset/autolevel-grid-node-xy.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(autolevel): add probe-storage regression tests for all controllers The controller fix (7e96f6aa) had no regression coverage, and the stale Marlin probe-capture test from #1006 still pinned the old behaviour (store the machine-reported XY), failing the suite on this branch's base. - Marlin: rewrite the probe-capture test — the measurement is stored at the intended grid node's XY with the measured Z; a two-node quantised sequence covers the index advance and minZ/maxZ. - Grbl / Smoothie: the same two-node test over the [PRB:...] parameter report (runner 'parameters' event). - TinyG: the same test over the {"r":{"prb":{...}}} receive report (runner 'r' event). All four share the title 'the probe measurement is stored at the intended grid node with the measured Z'; reported values carry a 0.001 quantisation offset, so reverting the controller fix or shifting the node index fails them. Full suite: 515/515. ESLint clean. * chore: update autolevel-grid-node-xy.md Record every autolevel probe measurement at the intended grid node's XY instead of the machine-reported XY, which is quantised by the motor steps and splits grid lines into near-duplicates. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: cheton <cheton@gmail.com> Co-authored-by: Cheton Wu <447801+cheton@users.noreply.github.com> | 1 个月前 | |
feat(api): stream raw request bodies to the watch directory (#1014) * feat(api): stream raw request bodies to the watch directory `PUT /api/watch/file` only accepted a JSON body, so the entire file had to arrive as a JSON string and be held in memory: body-parser buffers the request, parses it into a JavaScript string, and `monitor.writeFile()` then serialises that string back to disk. Peak RSS grows to several times the file size — around 600 MB for a 107 MiB program. This turned up while writing a native macOS/iOS client that uses cncjs as its backend and uploads large programs to the watch directory. Accept a raw streamed body as an alternative. When the request has no parsed body (any content type body-parser does not handle, such as `application/octet-stream`), the file name is read from the query string and the request is piped straight to disk by a new `monitor.writeStream()`. The JSON path is untouched: a parsed body still goes through `monitor.writeFile()` with the same arguments, the same response shape and the same "No file specified" error, so existing clients are unaffected. `writeStream()` writes to a temporary dotfile and renames it into place on completion, so a half-written program never appears under its final name in the watched directory, and removes the temporary file if the upload errors or is aborted. Directory components are stripped from the file name, as `writeFile()` already does. Measured on a real 112,678,627 byte Estlcam program, three runs each, with a fresh server process per run: workstation JSON +537..659 MB raw +40..46 MB LattePanda JSON +621..643 MB raw +29..69 MB The LattePanda figures are over the network, on the kind of single-board computer cncjs is typically deployed to. Both produce byte-identical files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(api): select the raw upload path by content type Review feedback from @cheton on #1014. Inferring the upload kind from whether body-parser produced anything was wrong: an empty JSON body parses to an empty object, which is indistinguishable from "not parsed". `PUT /api/watch/file?file=x.nc` with `Content-Type: application/json` and a body of `{}` therefore took the streaming path and wrote a zero-byte file, where before this feature it answered 400 "No file specified" — a change to the JSON path that this series claimed not to make. An unsupported content type was silently treated as a raw upload for the same reason. Take the kind from the content type instead, and answer 415 for anything that is neither `application/octet-stream` nor `application/json`, so the contract is explicit rather than a consequence of what body-parser happened to do. Adds `src/server/api/__tests__/api.watch.test.js` covering both paths, the empty-JSON-body case, 415 for an unsupported and for an absent content type, and the missing-name case on each path. `415` gets a named constant alongside the other statuses in `constants/index.js`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(monitor): stream through pipeline() and bound the upload size Review feedback from @cheton on #1014. The streamed path had no size limit. A JSON body is bounded by body-parser's configured 256mb, but `application/octet-stream` went straight to disk, so an authenticated caller could write until the filesystem filled — on a machine that is often a single-board computer with a small card, and whose whole job is to keep feeding a running machine. Adds `middleware.upload.maxFileSize` (256MB, matching the JSON limit), passed through an options object rather than another positional argument, and mapped to 413 by the API layer. The limit counts bytes as they pass rather than trusting Content-Length, so a body that understates its length or sends none is bounded too. Rewrites `writeStream()` on `stream.pipeline()`. The hand-rolled version coordinated `error`, `aborted`, `close`, `finish`, `unpipe()`, `destroy()` and a `settled` flag to get right what pipeline already does: propagate errors in both directions, apply backpressure, destroy every participating stream on failure, and report a client that disconnects mid-upload as a premature close. Verified against a running server: a 300MB body answers 413 with no file and no temporary file left behind, an aborted upload still leaves nothing, and the JSON path, the 415 rejection and the empty-JSON-body case are unchanged. Re-measured the 112,678,627 byte program through the new implementation over three runs — peak RSS +43.4/+43.6/+44.0 MB against +40..46 MB before, so the figures quoted in the PR still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 1 个月前 | |
Enhance CI/CD workflow with Yarn 3, Node.js 14, and GitHub Actions (#799) * Publish package-lock file to Appveyor artifacts * chore: bump to Node.js 14 and Yarn 3 * ci: add GitHub workflow for CI build * ci: refactor build scripts for CI pipeline * chore: revert to use npm to run electron builder script * ci: omit Linux i386 support due to lack of Electron binary support * ci: add "export ARCHFLAGS="-arch x86_64" to overcome the issue of incompatible architecture with macOS 10.15 * ci: remove Windows x86 build * ci: bump electron dependencies * chore: update build scripts in package.json * ci: implement CI/CD with GitHub action workflow * ci: deprecate AppVeyor and use GitHub Actions * chore: target the next release version to 1.10.0 * ci: remove GITHUB_TOKEN environment variable from build.yml * chore: sync dependencies in src/package.json * chore: update Node.js engine version in package.json * chore: provide access to env.GITHUB_TOKEN when building packages * ci: update build-latest.sh * fix: upgrade serialport to v10 * docs: update README.md for Node.js support * fix: use named import for the SerialPort module * feat: upgrade Electron application * feat: change indentation from 4 spaces to 2 spaces * chore: rename `read-user-data` and `write-user-data` to `read-user-config` and `write-user-config` * chore: babel polyfill is no longer needed Co-authored-by: cheton <cheton@gmail.com> | 3 年前 | |
Rename files and restructure folder layout (#440) | 7 年前 | |
feat: add support for M6 tool change policy (#895) * feat: Tool Change Policy (M6) * refactor: replace M0, M1, and M6 command handling with dedicated functions * feat: add tool widget in progress * chore(UserStoreService): replace `fs.watch` with `fs.watchFile` for better file change detection * chore: bump gcode-parser, gcode-interpreter, and gcode-toolpath * feat: add common controller utilities for parsing gcode * chore: revise log message * chore: bump `gcode-parser` to 2.2.0 * feat: implement tool change command in progress * chore: import `ensureArray` from `ensure-type` * feat: tool change command in progress * feat: update tool widget * Update TinyGController.js * remove deprecated `spe` and `spd` for spindle enable/direction * set `si` to 200ms (the minimum status internal) * fix incorrect `tofz` command format * chore: add tool length offset * feat: utilize `MessageSlot` to retain a single message (`%msg <message>`) in the G-code files * feat: custom probing in progress * chore: change `mapWCSToP` to `mapWCSToPValue` * feat: support tool probe custom commands * feat: tool probe overrides * feat: add `toolProbeOverrides` to the allowed keys * feat: tool probe overrides * feat: custom tool probe commands * chore: update i18n resource files * feat: add translations for tool probe commands | 1 年前 | |
feat(api): rename and delete files in the watch directory (#1015) * feat(api): rename and delete files in the watch directory The watch directory API can list, read and write, but a file put there can only ever be replaced, never renamed or removed, so tidying it up means shell access to the machine running cncjs. That is awkward when cncjs is the only thing reachable — a headless single-board computer driving a machine, or a client that uploads programs over the network. Add `DELETE /api/watch/file` and `POST /api/watch/file/rename`, with the file name taken from the body or the query string as the sibling endpoints already do. Both refuse a name containing directory components rather than stripping it. `writeFile()` strips with `path.basename()`, which is right when creating a file, but the same silent correction applied to a destructive operation would act on a *different* existing file than the caller named: deleting `../../program.nc` would delete `program.nc`. Refusing is the only safe reading. Directories are refused too. Renaming does not overwrite. `fs.rename()` would silently replace an existing target and destroy a program the caller never mentioned, so the new name is claimed with `fs.link()`, which fails with EEXIST atomically — a stat-then-rename check cannot, since the target can appear in between. The original is unlinked only once the new name is in place, and taken back if that unlink fails, so the rename is all-or-nothing. The path handling lives in the monitor service alongside `writeFile()`, rather than in the route handler, so the API layer stays a thin mapping from error codes to statuses: 400 for a bad name or a directory, 404 for a missing file, 409 for a taken target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(api): address review on rename and delete Review feedback from @cheton on #1015. `lstat()` instead of `stat()`. `stat()` follows symbolic links, so a link to a regular file satisfied `isFile()` and was accepted — including a link whose target lives outside the watched directory, which is exactly what the rest of this code is careful to prevent. Adds a test covering a link to a file inside the directory and one pointing out of it. Rename no longer requires hard-link support. `fs.link()` is still preferred, because it refuses an existing target atomically in a way a stat-then-rename check cannot, but filesystems that support ordinary reads, writes and deletes while rejecting `link()` — FAT32 and exFAT removable media, some network mounts — would have failed every rename. A watch directory on a USB stick is an ordinary setup, so those fall back to `copyFile()` with `COPYFILE_EXCL`, which still refuses to overwrite, though the check and the write are then no longer one operation. Tested by forcing `link()` to fail with EPERM. The rollback is reported rather than swallowed. If the source cannot be removed the new name is given back, but that second removal can fail too, and the previous code discarded its error and returned the first one — so a caller could be told the rename failed while both names existed. That case now returns its own error saying so, and the commit message for this series no longer claims the operation is strictly all-or-nothing. `__delete` is renamed to `deleteFile`, for consistency with `readFile`, `writeFile` and `renameFile`. The `__delete` convention elsewhere in this API exists because `delete` is a reserved word; `deleteFile` is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: rename changeset file for clarity --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cheton Wu <447801+cheton@users.noreply.github.com> | 1 个月前 | |
feat: add support for M6 tool change policy (#895) * feat: Tool Change Policy (M6) * refactor: replace M0, M1, and M6 command handling with dedicated functions * feat: add tool widget in progress * chore(UserStoreService): replace `fs.watch` with `fs.watchFile` for better file change detection * chore: bump gcode-parser, gcode-interpreter, and gcode-toolpath * feat: add common controller utilities for parsing gcode * chore: revise log message * chore: bump `gcode-parser` to 2.2.0 * feat: implement tool change command in progress * chore: import `ensureArray` from `ensure-type` * feat: tool change command in progress * feat: update tool widget * Update TinyGController.js * remove deprecated `spe` and `spd` for spindle enable/direction * set `si` to 200ms (the minimum status internal) * fix incorrect `tofz` command format * chore: add tool length offset * feat: utilize `MessageSlot` to retain a single message (`%msg <message>`) in the G-code files * feat: custom probing in progress * chore: change `mapWCSToP` to `mapWCSToPValue` * feat: support tool probe custom commands * feat: tool probe overrides * feat: add `toolProbeOverrides` to the allowed keys * feat: tool probe overrides * feat: custom tool probe commands * chore: update i18n resource files * feat: add translations for tool probe commands | 1 年前 |