Repository navigation
fix(studio-server): undo and redo on Windows restore a file something holds open - #6628
Conversation
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 1 important · 0 nits · 0c8c4b23
- blocker:
Commentsis red at this head. The comment ratchet flagscore/src/renameWithRetry.ts(6.1% → 7.0%),studio-server/src/history/blobStore.ts(4.6% → 5.5%), and the newstudio-server/src/helpers/servedFiles.ts, which starts at 12.0% against the package's 6.6%. Cut those comment lines or move what they say into names or types.
Scope
Read every changed file. The fails without the fix: tests check out locally: removing the in-place write fails the first two, removing the hash check fails the mismatch test, and removing stopServing fails the served-read test. All 6 pass at head. CI and Windows render verification were still running when I posted this.
| async function writeInPlace(temp: string, to: string, hash: string): Promise<void> { | ||
| let outcome: InPlaceOutcome = "failed"; | ||
| try { | ||
| await copyFile(temp, to); |
There was a problem hiding this comment.
important: this relies on fs.copyFile (libuv CopyFileW) opening a destination another process holds. CopyFileW asks for its own access and share mode on the destination, which may differ from a plain write through a handle. The tests mock copyFile, so CI never runs it against a real hold. Did the Windows probe run copyFile itself? If it didn't, writing through open(to, "r+") + truncate + write is the in-place write the probe showed works. When this fails, the restore errors as it did before, so nothing is lost. The point is whether the fallback ever succeeds.
There was a problem hiding this comment.
Yes. The Windows probe ran fs.copyFileSync (the same libuv uv_fs_copyfile → CopyFileW as the async copyFile here) against each holder. It worked against a Node read handle in the same process or another, a paused Node read stream, and PowerShell handles sharing ReadWrite or ReadWrite+Delete. It failed with EBUSY only when the holder shared read alone, and then the restore errors as before. So the fallback succeeds against every holder that allows a write through a handle.
There was a problem hiding this comment.
✅ Answered: the Windows probe ran copyFileSync (CopyFileW) against each holder.
| beforeReplace?.(); | ||
| renameWithRetry(temp, to); | ||
| } catch (error) { | ||
| stopServing(to); |
There was a problem hiding this comment.
Not blocking: destroy() closes the fd asynchronously. If a read is in flight, the close waits for that read's callback on the main thread, and the synchronous renameWithRetry below blocks the main thread for its whole ~1.3 s retry. So that rename can't succeed, and the restore falls through to the in-place path. The test checks read.destroyed, not whether the fd is closed. awaiting the streams' close events here (or using renameWithRetryAsync) would make the fix hold.
There was a problem hiding this comment.
Right, fixed at 1468e5c. stopServingAndClose destroys each open read and resolves on its close event, and the restore awaits it before the synchronous rename. The test now checks read.closed at the rename, not read.destroyed. A break check that calls it without awaiting fails that test.
Edit accuracy: accurate 271 (base branch 271), smooth 143 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (2)
|
…before replacing the file
|
Blocker fixed at 1468e5c: the new comments are down to one line per file, and the doc comment in |
| beforeReplace?.(); | ||
| renameWithRetry(temp, to); | ||
| } catch (error) { | ||
| await stopServingAndClose(to); |
There was a problem hiding this comment.
important: beforeReplace is assertWritable(), the check that the project folder is still this project. It ran right before the synchronous rename. This await now sits between them, so if the preview holds reads of the file, the folder can be swapped while they close and the rename still lands. Close the reads first:
| beforeReplace?.(); | |
| renameWithRetry(temp, to); | |
| } catch (error) { | |
| await stopServingAndClose(to); | |
| await stopServingAndClose(to); | |
| beforeReplace?.(); |
There was a problem hiding this comment.
Taken at fce3bb3: the reads close first, then beforeReplace runs right before the synchronous rename, as before. The in-place fallback starts with no yield after that rename, so nothing runs between the check and the write. blobStore.test.ts passes three runs in a row; tsc, oxfmt and the comment ratchet are clean.
…view's reads close
What changed
On Windows, undo and redo could fail with
EPERMwhen they put a file back. The history writes the earlier copy beside the file and renames it over the file. Windows refuses that rename while anything has the file open, even a read handle that allows deleting. #6559 added a short retry, but some holders keep the file open for longer than that.Two holders, two fixes:
<video>keeps its read open. The preview now registers each stream it serves (helpers/servedFiles.ts), and a history restore ends the open reads of a file and waits for them to close before it replaces it. The player asks again and gets the new bytes.onRestoredInPlacelets an app count how often this happens and how it ends.Every other platform and every other error still fails at once, as before.
isWindowsHoldin core is now the single check for "Windows is holding this file", shared by the rename retry and the restore.What I verified
src/history/blobStore.test.ts:blobStore.test.tspasses three runs in a row (6 tests).tscis clean for core and studio-server.What I did NOT exercise