Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2444,21 +2444,79 @@ could otherwise be picked up silently.)

Three properties worth preserving if you touch this:

- **Staged, then swapped.** The download is verified, extracted to
`<deps>.new`, checked with `executabilityProblem()` and stamped with its
`version.json` — and only then swapped in via two renames, with the old tree
moved to `<deps>.old` and deleted afterwards. It used to delete the live
install and extract over the top, which left the user with *nothing* if that
window was interrupted. If the second rename fails the first is undone.
- **`version.json` is written last**, inside the staged tree. It is the commit
marker: a tree that never completed can never look valid.
- **Staged, then swapped — but only when there is an install to protect.** An
*upgrade* is verified, extracted to `<deps>.new`, stamped with its
`version.json` and only then swapped in via two renames, with the old tree
moved to `<deps>.old` and deleted afterwards; if the second rename fails the
first is undone. It used to delete the live install and extract over the top,
which left the user with *nothing* if that window was interrupted. A **first
install** extracts straight into `<deps>` and performs no rename at all —
there is nothing to protect, and the rename is the fragile step (see below).
- **`version.json` is written last**, inside whichever tree was written. It is
the commit marker: a tree that never completed can never look valid — which
is also what makes extracting a first install in place safe.
- **Direction is checked, not just equality.** Installed *newer* than expected
is `newerThanExpected` — kept, with a one-time warning at startup — not
`outdated`. Treating it as outdated downgraded a deliberately newer bundle,
and since installing wipes and replaces, that was destructive. Use
`compareVersions`, not `!=` or a string compare: `"1.10.0"` sorts before
`"1.9.0"` lexically.

> **A Windows directory rename is refused while anything holds a handle inside
> it, and that is not a permissions problem (issue #87).** Measured: an open
> read handle on one descendant file, or a child process whose working directory
> is inside the tree, is enough — both surface as
> `PathAccessException … Access is denied, errno = 5`. A *running* `.exe` inside
> the tree is **not** enough, and a destination that already exists gives
> **errno 183** instead, so the two can be told apart. Straight after writing a
> ~200 MB bundle there is routinely something holding a handle for a few hundred
> milliseconds — a scanner, the search indexer, Explorer building a thumbnail —
> and the reporter's install failed on that every time, throwing away the whole
> download at the very last step.
>
> Both renames therefore go through `retryTransientFsOperation` (~7.5s over 12
> attempts), as does the `.new`/`.old` cleanup at the start — a leftover `.new`
> can still be held by whatever blocked the swap, and an unguarded delete there
> failed the *next* attempt with a second, different error.
> `PathExistsException`/`PathNotFoundException` are rethrown immediately: those
> will not clear, and burning the budget on them delays a fault the user can act
> on. `dependency_install_retry_test.dart` reproduces the real errno 5 on
> Windows and skips elsewhere, because POSIX renames a directory happily with
> its files open.
>
> **`executabilityProblem()` runs after the swap on Windows, before it
> everywhere else.** The quarantine case it guards (issue #50) is macOS-only, so
> on Windows all it can report is a generic "would not run" — while executing a
> freshly written, unsigned 100 MB binary is exactly what makes a scanner open
> the tree we are about to rename. A Windows failure rolls the previous bundle
> back out of `<deps>.old`, so it still cannot leave the user worse off.

**The zip survives a failed install, and the dialog says what is happening.**
Three things about the feedback, all of which #87 exposed:

- **The download is cached, not thrown away.** It lands in
`<temp>/vapourbox-deps-cache/<filename>` and is deleted **only after the
install succeeds**, so Retry skips a ~200 MB re-download of bytes that were
never the problem. It is reused only when the sidecar supplied a sha256 to
check it against — without one, a truncated download is indistinguishable
from a complete one and would surface as a corrupt bundle. Anything in that
directory under another name is another version's leftovers and is pruned,
which is what bounds the cache.
- **The install phase emits progress.** It used to emit nothing between the last
extraction tick and `Complete`, so the swap happened under a bar reading
"Extracting… 100%" — and the retry budget added to that would have been
indistinguishable from a hang. `_reportInstallStep` sends 0/0 events (an
indeterminate bar, deliberately: the swap has no fraction to report) and
`retryTransientFsOperation`'s `onRetry` names the wait.
- **The remedy is chosen from the failure.** `DependencyManager.remedyFor` maps
the error to advice — held handles, a full disk, a permissions fault, or the
connection line as the fallback — and `DependencyInstallException` carries its
own for messages that already say what to do (macOS quarantine ships its
`xattr` command). The dialog used one fixed line of *connection* advice under
every failure, which is why the reporter went looking at folder ACLs. The
headings were wrong for the same reason and now say **Installation Failed**,
not "Download Failed" — most of what can fail here happens after the download.

The critical-file list also includes a file that exists **only** in the R78
layout (`libvapoursynthfilters`, plus `vapoursynth/__init__.py` on Unix).
`vspipe`, `plugins/` and `ffmpeg` all exist in both layouts, so without an
Expand Down
Loading
Loading