fix: write write_file and disabled_extensions.json atomically#170
Merged
jmaxdev merged 2 commits intoTrixtyAI:mainfrom Apr 21, 2026
Merged
fix: write write_file and disabled_extensions.json atomically#170jmaxdev merged 2 commits intoTrixtyAI:mainfrom
jmaxdev merged 2 commits intoTrixtyAI:mainfrom
Conversation
|
Thanks for the contribution! I'll review it as soon as possible. If you have still changes, please mark this PR as draft and all reviews will be cancelled. Tests reviews will be re-run only when the PR is marked as ready for review. |
048d969 to
b7ef0db
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces an atomic write helper for the Tauri backend to prevent file truncation/corruption on crash or power loss, and wires it into the write_file command and the extensions registry (disabled_extensions.json) write path.
Changes:
- Add
fs_atomic::write_atomic(path, bytes)helper that writes to a sibling temp file, syncs it, then renames over the destination. - Update
write_fileto usewrite_atomicinstead ofstd::fs::write. - Update
toggle_extension_stateto writedisabled_extensions.jsonviawrite_atomic.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| apps/desktop/src-tauri/src/lib.rs | Registers the new module and routes write_file through atomic writes. |
| apps/desktop/src-tauri/src/fs_atomic.rs | Implements atomic write helper and adds unit tests. |
| apps/desktop/src-tauri/src/extensions.rs | Routes disabled_extensions.json updates through atomic writes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hi @matiaspalmac, the quality checks have failed. ❌ Quality Checks Failed
Full Log (Format) |
matiaspalmac
added a commit
to matiaspalmac/ide
that referenced
this pull request
Apr 21, 2026
…afe test dirs Addresses all three review threads on TrixtyAI#170. - Temp uniqueness. The previous PID+nanos suffix relied on SystemTime resolution being finer than a save cycle, which isn't always true (15.6 ms on older Windows) and `.unwrap_or(0)` collapsed clock failures to the same value. A `File::create` on a duplicate suffix would also truncate an existing temp that another caller owned. Introduced `create_unique_temp` that opens the temp with `OpenOptions::create_new(true)` (fails fast with AlreadyExists), adds a process-wide `AtomicU64` counter alongside PID + nanos so two racing calls inside the same tick never clash, and retries up to 32 times before surfacing an error. - Permission preservation. When the destination file already exists, carry its mode bits to the temp via `set_permissions` before the rename. On Unix this preserves the executable / group / other bits that the review called out (previously the rename landed a default 0644/0666-style mask even if the original file was 0755). On Windows `Permissions` only exposes the read-only bit and ACLs inherit from the parent directory, so the carry-over is a no-op in practice — documented that in the doc-comment so a future editor doesn't try to plug a hole that doesn't exist. - Test-parallelism flake. `tmp_dir()` in the test module now incorporates the current thread id plus a process-wide atomic counter in addition to PID and nanos, so two Rust-test threads racing into the helper at the same clock tick always pick disjoint directories. - Added a Unix-only regression test (`preserves_unix_mode_bits_on_replace`) that chmod 0o755s the destination, overwrites, and asserts the mode survives. - Mentioned as polish: the `io_other_error` clippy lint pushed us to use `std::io::Error::other` instead of `Error::new(Other, …)` in the retry-exhausted branch.
jmaxdev
pushed a commit
to matiaspalmac/ide
that referenced
this pull request
Apr 21, 2026
…afe test dirs Addresses all three review threads on TrixtyAI#170. - Temp uniqueness. The previous PID+nanos suffix relied on SystemTime resolution being finer than a save cycle, which isn't always true (15.6 ms on older Windows) and `.unwrap_or(0)` collapsed clock failures to the same value. A `File::create` on a duplicate suffix would also truncate an existing temp that another caller owned. Introduced `create_unique_temp` that opens the temp with `OpenOptions::create_new(true)` (fails fast with AlreadyExists), adds a process-wide `AtomicU64` counter alongside PID + nanos so two racing calls inside the same tick never clash, and retries up to 32 times before surfacing an error. - Permission preservation. When the destination file already exists, carry its mode bits to the temp via `set_permissions` before the rename. On Unix this preserves the executable / group / other bits that the review called out (previously the rename landed a default 0644/0666-style mask even if the original file was 0755). On Windows `Permissions` only exposes the read-only bit and ACLs inherit from the parent directory, so the carry-over is a no-op in practice — documented that in the doc-comment so a future editor doesn't try to plug a hole that doesn't exist. - Test-parallelism flake. `tmp_dir()` in the test module now incorporates the current thread id plus a process-wide atomic counter in addition to PID and nanos, so two Rust-test threads racing into the helper at the same clock tick always pick disjoint directories. - Added a Unix-only regression test (`preserves_unix_mode_bits_on_replace`) that chmod 0o755s the destination, overwrites, and asserts the mode survives. - Mentioned as polish: the `io_other_error` clippy lint pushed us to use `std::io::Error::other` instead of `Error::new(Other, …)` in the retry-exhausted branch.
2759207 to
4830e11
Compare
…ive mid-write crashes write_file in lib.rs and the disabled_extensions.json write in extensions.rs both called fs::write directly. A crash or power loss midway through that write left the target truncated, which corrupted whatever the user had been saving (including the extensions registry itself — a crash during an enable/disable left the JSON in an invalid state and broke extension loading on next launch). Added a new `fs_atomic::write_atomic` helper that: - Writes the payload to a hidden sibling temp file (`.<name>.<pid>.<nanos>.tmp`) so the eventual rename stays on the same volume (the atomicity guarantee is per-volume). - Calls `sync_data` before renaming, so the data has actually reached disk rather than merely the OS page cache. - Renames over the destination. On POSIX `rename` is atomic at the filesystem level; on Windows `std::fs::rename` maps to `MoveFileEx` with `MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH`, which is atomic on NTFS for same-volume targets. - Cleans up the temp file on any error so we don't leak `.tmp` files if the rename itself fails (read-only target, cross-volume path, etc.). Both call sites now go through the helper. Added three unit tests covering new-file write, existing-file replacement, and the no-leftover `.tmp` invariant. Closes the non-atomic-write path without touching the disk format or any public command surface.
…afe test dirs Addresses all three review threads on TrixtyAI#170. - Temp uniqueness. The previous PID+nanos suffix relied on SystemTime resolution being finer than a save cycle, which isn't always true (15.6 ms on older Windows) and `.unwrap_or(0)` collapsed clock failures to the same value. A `File::create` on a duplicate suffix would also truncate an existing temp that another caller owned. Introduced `create_unique_temp` that opens the temp with `OpenOptions::create_new(true)` (fails fast with AlreadyExists), adds a process-wide `AtomicU64` counter alongside PID + nanos so two racing calls inside the same tick never clash, and retries up to 32 times before surfacing an error. - Permission preservation. When the destination file already exists, carry its mode bits to the temp via `set_permissions` before the rename. On Unix this preserves the executable / group / other bits that the review called out (previously the rename landed a default 0644/0666-style mask even if the original file was 0755). On Windows `Permissions` only exposes the read-only bit and ACLs inherit from the parent directory, so the carry-over is a no-op in practice — documented that in the doc-comment so a future editor doesn't try to plug a hole that doesn't exist. - Test-parallelism flake. `tmp_dir()` in the test module now incorporates the current thread id plus a process-wide atomic counter in addition to PID and nanos, so two Rust-test threads racing into the helper at the same clock tick always pick disjoint directories. - Added a Unix-only regression test (`preserves_unix_mode_bits_on_replace`) that chmod 0o755s the destination, overwrites, and asserts the mode survives. - Mentioned as polish: the `io_other_error` clippy lint pushed us to use `std::io::Error::other` instead of `Error::new(Other, …)` in the retry-exhausted branch.
4830e11 to
b191da3
Compare
jmaxdev
approved these changes
Apr 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[Fix]: Write
write_fileanddisabled_extensions.jsonatomicallyDescription
Two call sites used
fs::write(equivalentlystd::fs::write)directly:
apps/desktop/src-tauri/src/lib.rs— thewrite_fileTauri commandused by the frontend editor/save path.
apps/desktop/src-tauri/src/extensions.rs— thedisabled_extensions.jsonwrite insidetoggle_extension_state.A crash or power loss partway through either call left the target
truncated — enough to corrupt whatever the user had been saving
and, in the extensions case, to break extension loading on the next
launch because the registry file was no longer valid JSON.
Change
Added a small new module
apps/desktop/src-tauri/src/fs_atomic.rsexposing
write_atomic(path, bytes). Both call sites now go throughit.
Behavior of
write_atomic.<target_name>.<pid>.<nanos>.tmp. Placing the temp next to thetarget keeps the rename on the same volume (the atomicity
guarantee is per-volume), and the leading
.+ PID/timestampkeeps editors and file-watchers from surfacing it.
sync_data()the temp file before the rename, so the data hasactually reached disk rather than merely the OS page cache.
std::fs::rename(tmp, target):renameis atomic at the filesystem level.MoveFileExwithMOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, whichperforms the replacement atomically on NTFS for same-volume
targets.
the temp file is cleaned up best-effort so the target directory
doesn't accumulate
.tmpleftovers.Tests
cargo test fs_atomiccovers three cases:different bytes.
.tmpleftovers — sibling directory is clean aftera successful write.
All three pass on Windows; the module has no platform-specific code
beyond what
std::fs::renamealready abstracts.Trade-offs
avoid collisions between concurrent saves of the same file without
pulling in
tempfile. If two processes ever share an IDE statedirectory (they shouldn't), the nanosecond timestamp still avoids
clashes.
than the temp (e.g., a user's file on a network drive), the rename
will fail and return an error; the original file is preserved. A
cross-volume fallback (copy + delete) would not be atomic anyway,
so I deliberately did not add one — a loud error is preferable to
a silently non-atomic path.
write_atomicdoes not currentlycarry over mode bits / ACLs from the previous file. Both call
sites write JSON or plain text that's owned by the IDE process,
so this hasn't caused behavior differences in practice. If a
future caller needs preservation we can extend the helper rather
than every call site.
Verification
cargo check→ clean.cargo clippy --tests -- -D warnings→ clean.cargo test fs_atomic→ 3/3 pass.write_file("C:\\…\\note.txt", "hello")→ temp next to target,rename succeeds, target now contains
hello, no.tmpfilesremain.
toggle_extension_state(id, false)then_state(id, true)→disabled_extensions.jsonvalid JSON after both operations, nointermediate truncation observed even when hitting the process
with
taskkill /Fmid-toggle.Related Issue
Fixes #77
Checklist