fix(home): on Windows, Delete leaves out game files a folder with no row still holds #101

Merged
HumanBean merged 1 commit from madebyjordan/LongshotLauncher:fix/frees-links-outside into main 2026-11-15 17:22:28 +00:00
Contributor

Why

On Windows, Delete can say it frees the ~26 GB of game files a version shares with a folder that has no row. Until #57 lands, every update leaves such a folder: it builds into a new folder, links the game files from the old one (#58), and the old folder stays on the disk. Disk.Measure gets file IDs on Windows from the folder listing, which has no link count, so a file counts as freed when no other listed version holds it. Linux already uses the link count.

What changed

  • Core/Disk.cs Measure: on Windows, a file whose links seen so far all sit in one folder (the only files that folder's delete could free) is asked for its link count, on up to 8 threads. A file two listed versions share is never freed by deleting either, so it isn't asked. Nothing changes on Linux, where statx already gives the count. Walk now passes each file's path.
  • Core/FileLinks.cs WindowsLinks(path, id): one open of the file with FILE_READ_ATTRIBUTES (never its data) and GetFileInformationByHandle. It returns the link count only if the file is still the one the listing named (volume serial and file index), else 0, and the count then goes by the links seen, as before.
  • tests/launcher_core MeasureChecks: the two Frees checks expect the same on Windows as on Linux. A link outside every listed build (elsewhere/outside) is not freed, and a file linked twice inside one build is.

Cost

Measured on this PC (Ryzen 5 5600, NVMe, Windows 11) on the installed game folder (nightly-189, ~187,000 files, no version sharing its files), through the PC's job queue, main and this branch alternated, 2 runs of 3 rounds each:

Disk.Measure
main @ fa3a444 0.56, 0.52, 0.53 s; 0.67, 0.38, 0.36 s
this PR 3.65, 3.95, 3.35 s; 3.16, 2.98, 2.99 s

About 3 s more per version whose files no other listed version shares (fewer files are asked when versions share them). Measure runs in the background, only when a version was built, updated or deleted, and its figures are kept in builds.json. A first try during a game update on the same PC was noise for both (33 to 60 s) and is left out.

Testing

  • python -I tests/test_launcher_core.py on Windows (.NET 10.0.404), output to a file: 1137 checks pass. The symbolic-link fixtures are skipped (no Developer Mode). Two local workarounds were needed for the run, and both are needed on main too:
    • The release fixtures were checked out with LF line endings (core.autocrlf=false). A fresh Windows checkout with core.autocrlf=true gets them with CRLF, and ReleaseChecks fails on main as on this branch.
    • root = Path(folder).resolve() in the runner. Without it, InstallChecks fails on main as on this branch, because the temp folder's short 8.3 name (C:\Users\LONGNA~1\...) doesn't match the full paths the checks expect.
  • With main's Disk.cs and FileLinks.cs and this branch's check, the run fails at "Deleting a build frees only its files with no link outside its folder.", so the check covers the fix.
  • A small program against main's code, before this change: a version whose game file is also held by an unlisted folder said frees 1010 where deleting frees 10.
  • dotnet build of the app: no warnings.
  • Not run: Linux (CI runs the checks there; the Linux path is unchanged).

Based on main @ fa3a444.

🤖 Generated with Claude Code

## Why On Windows, Delete can say it frees the ~26 GB of game files a version shares with a folder that has no row. Until https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/57 lands, every update leaves such a folder: it builds into a new folder, links the game files from the old one (#58), and the old folder stays on the disk. `Disk.Measure` gets file IDs on Windows from the folder listing, which has no link count, so a file counts as freed when no other *listed* version holds it. Linux already uses the link count. ## What changed - `Core/Disk.cs` `Measure`: on Windows, a file whose links seen so far all sit in one folder (the only files that folder's delete could free) is asked for its link count, on up to 8 threads. A file two listed versions share is never freed by deleting either, so it isn't asked. Nothing changes on Linux, where statx already gives the count. `Walk` now passes each file's path. - `Core/FileLinks.cs` `WindowsLinks(path, id)`: one open of the file with `FILE_READ_ATTRIBUTES` (never its data) and `GetFileInformationByHandle`. It returns the link count only if the file is still the one the listing named (volume serial and file index), else 0, and the count then goes by the links seen, as before. - `tests/launcher_core` `MeasureChecks`: the two `Frees` checks expect the same on Windows as on Linux. A link outside every listed build (`elsewhere/outside`) is not freed, and a file linked twice inside one build is. ## Cost Measured on this PC (Ryzen 5 5600, NVMe, Windows 11) on the installed game folder (`nightly-189`, ~187,000 files, no version sharing its files), through the PC's job queue, main and this branch alternated, 2 runs of 3 rounds each: | | `Disk.Measure` | |---|---| | main @ fa3a444 | 0.56, 0.52, 0.53 s; 0.67, 0.38, 0.36 s | | this PR | 3.65, 3.95, 3.35 s; 3.16, 2.98, 2.99 s | About 3 s more per version whose files no other listed version shares (fewer files are asked when versions share them). Measure runs in the background, only when a version was built, updated or deleted, and its figures are kept in builds.json. A first try during a game update on the same PC was noise for both (33 to 60 s) and is left out. ## Testing - `python -I tests/test_launcher_core.py` on Windows (.NET 10.0.404), output to a file: 1137 checks pass. The symbolic-link fixtures are skipped (no Developer Mode). Two local workarounds were needed for the run, and both are needed on main too: - The release fixtures were checked out with LF line endings (`core.autocrlf=false`). A fresh Windows checkout with `core.autocrlf=true` gets them with CRLF, and `ReleaseChecks` fails on main as on this branch. - `root = Path(folder).resolve()` in the runner. Without it, `InstallChecks` fails on main as on this branch, because the temp folder's short 8.3 name (`C:\Users\LONGNA~1\...`) doesn't match the full paths the checks expect. - With main's `Disk.cs` and `FileLinks.cs` and this branch's check, the run fails at "Deleting a build frees only its files with no link outside its folder.", so the check covers the fix. - A small program against main's code, before this change: a version whose game file is also held by an unlisted folder said `frees 1010` where deleting frees 10. - `dotnet build` of the app: no warnings. - Not run: Linux (CI runs the checks there; the Linux path is unchanged). Based on main @ fa3a444. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(home): on Windows, Delete leaves out game files a folder with no row still holds
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Compile and test (pull_request) Blocked by required conditions
e9e8811f51
On Windows, Disk.Measure takes file ids from the folder listing, which says no link count, so a file counted as freed when
no other listed build held it. An update built into a new folder leaves the old one on the disk with no row, holding links
to the game files it gave the new one (#58): Delete then said it freed those ~26 GB too. Linux already used the link count.

- Disk.Measure: on Windows, a file whose links seen all sit in one folder (the only files a delete could free) is asked for
  its link count, on up to 8 threads; one two listed builds share is not asked. ~187,000 game files: 3.0 to 4.0 s, where
  the listing alone took 0.4 to 0.7 s (Ryzen 5 5600, NVMe). Walk passes each file's path.
- FileLinks.WindowsLinks: one open for the file's attributes (GetFileInformationByHandle), its count only while it is
  still the file the listing named (volume serial and file index).
- tests/launcher_core MeasureChecks: Frees is the same on Windows as on Linux (a link outside every build is not freed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
HumanBean deleted branch fix/frees-links-outside 2026-11-15 17:22:29 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Compile and test (pull_request) Blocked by required conditions
Some workflows are waiting to be reviewed.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/LongshotLauncher!101
No description provided.