fix(windows): concurrent writes of one file, and free space on a drive root #71

Merged
HumanBean merged 5 commits from HumanBean/LongshotLauncher:fix/windows-file-writes into main 2026-11-14 20:54:14 +00:00
Owner

On Windows, two launcher processes writing the same file at once (a shortcut's --play beside an open launcher, for one) can fail with "access denied". Free space on a drive root was also looked up wrong. This takes over Anarkkie's #64, rebased on main. Anarkkie's branch doesn't allow maintainer edits, so I couldn't push the rebase there.

What changed (Anarkkie's commits, unchanged apart from the rebase)

  • Core/AtomicFile.cs: on Windows, only the final rename over the old file is serialized, by a named mutex per file (Local\, the uppercased full path hashed).
    • Temp files are still written and flushed independently (each writer has its own <name>.<guid>.tmp).
    • An abandoned mutex is taken over.
    • A file that's really locked still fails, keeping its old bytes and removing the temp file.
    • In Anarkkie's repro on Windows (4 processes × 100 writes), the current code had 26 access-denied failures; with the mutex, none.
  • Core/Disk.cs: TrimEndingDirectorySeparator("C:\") keeps its separator, so Within looked for the prefix C:\\ and never matched a drive root. It now adds a separator only when there isn't one.
  • Core/SelfUpdate.cs: a Linux APPIMAGE path is checked with StartsWith('/'). That's the same answer on Linux, and also right when the checks run on Windows.
  • Tests:
    • AtomicChecks runs 4 processes writing the same file, new and existing, on both systems. On Windows it also covers an abandoned owner and a locked file.
    • TestLinks skips only the checks that need to create a symlink, and only on Windows without the symlink privilege. The checks that reject bad links still run everywhere.
    • The Linux shortcut check uses Shortcuts.Quote.
  • Rebased on #54: its StartPath checks and the new AppImage checks are both kept in SelfUpdateChecks.
  • Left out of #64: docs/tested-configurations.md, a record of one test run; the PR has it.

Testing

  • python -I tests/test_launcher_core.py --strict --longshot <Longshot main>: 994 checks passed (Linux, .NET 10.0.404).
  • python -I tests/test_release.py: 28 OK.
  • With #70 (#63's replacement) merged in too: 1018 checks passed.
  • Release build: 0 warnings, 0 errors.
  • The Windows-only checks weren't run here. Anarkkie ran the full core suite on Windows 11 (960 checks) for #64.

Co-authored with Anarkkie (#64).

On Windows, two launcher processes writing the same file at once (a shortcut's `--play` beside an open launcher, for one) can fail with "access denied". Free space on a drive root was also looked up wrong. This takes over Anarkkie's #64, rebased on main. Anarkkie's branch doesn't allow maintainer edits, so I couldn't push the rebase there. ## What changed (Anarkkie's commits, unchanged apart from the rebase) - `Core/AtomicFile.cs`: on Windows, only the final rename over the old file is serialized, by a named mutex per file (`Local\`, the uppercased full path hashed). - Temp files are still written and flushed independently (each writer has its own `<name>.<guid>.tmp`). - An abandoned mutex is taken over. - A file that's really locked still fails, keeping its old bytes and removing the temp file. - In Anarkkie's repro on Windows (4 processes × 100 writes), the current code had 26 access-denied failures; with the mutex, none. - `Core/Disk.cs`: `TrimEndingDirectorySeparator("C:\")` keeps its separator, so `Within` looked for the prefix `C:\\` and never matched a drive root. It now adds a separator only when there isn't one. - `Core/SelfUpdate.cs`: a Linux `APPIMAGE` path is checked with `StartsWith('/')`. That's the same answer on Linux, and also right when the checks run on Windows. - Tests: - `AtomicChecks` runs 4 processes writing the same file, new and existing, on both systems. On Windows it also covers an abandoned owner and a locked file. - `TestLinks` skips only the checks that need to create a symlink, and only on Windows without the symlink privilege. The checks that reject bad links still run everywhere. - The Linux shortcut check uses `Shortcuts.Quote`. - Rebased on #54: its `StartPath` checks and the new AppImage checks are both kept in `SelfUpdateChecks`. - Left out of #64: `docs/tested-configurations.md`, a record of one test run; the PR has it. ## Testing - `python -I tests/test_launcher_core.py --strict --longshot <Longshot main>`: 994 checks passed (Linux, .NET 10.0.404). - `python -I tests/test_release.py`: 28 OK. - With #70 (#63's replacement) merged in too: 1018 checks passed. - Release build: 0 warnings, 0 errors. - The Windows-only checks weren't run here. Anarkkie ran the full core suite on Windows 11 (960 checks) for #64. Co-authored with Anarkkie (#64).
HumanBean force-pushed fix/windows-file-writes from 136c7edd74
All checks were successful
CI / Compile and test (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
to 775a84ba82
All checks were successful
CI / Compile and test (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 5s
2026-11-14 20:54:00 +00:00
Compare
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!71
No description provided.