fix(linux): a saves move that fails part way brings back what it moved #557
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!557
Loading…
Reference in a new issue
No description provided.
Delete branch "zeldaootr/Longshot:saves-move-partial-failure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: low · Tested on: Linux x86-64 (game-free); Steam Deck OLED, SteamOS 3.9.5 (a tester's run) · Needs: none
Why
On the first start of a new version, the game moves its saves out of the old
user/folder. If one entry could not be moved (a full disk, say), the entries moved before it stayed in the new place, and the next start kept using the old folder without them: saves and settings could seem to vanish. The tracker has no issue for it (searched).How to reproduce
Seen on: Steam Deck OLED, SteamOS 3.9.5, main
7ff5256e(a tester's run; Linux only: Windows keepsuser/beside the game)Settings and switches:
XDG_DATA_HOMEon another file system than the game folder (here tmpfs against ext4), so the move has to copySteps: in a copy of a test game folder, put a test save in
user/with an entry after it that cannot be read (chmod 000), then start the gameExpected: the log says the saves stay in
user/, anduser/keeps every entryActual: on main the log says the saves stay in
user/, but the move had already taken the save:user/was left with onlylocked/,temp/and an emptysave/account folder, and 10 save files were in the new data folder, where the game does not lookWhat changed
A saves move that fails part way now puts back what it had already moved, so the old
user/is whole again and the next start uses it as before. A copied entry's old place is deleted only once every entry has arrived.saves_dir()inhost/hos_main.cremembers each entry it moves (renamed or copied). When one fails, the renamed ones are moved back and the copies in the new place deleted; a rename back that fails is logged (saves: <to> could not come back to <from>).tests/test_linux_saves_move.pygets a case with an entry that cannot be copied (an unreadable file stands in for a full disk), with renamed and copied entries listed before it, on one drive and across two: at both starts the old place must keep every entry and the new place stay empty, and once the entry can go, the next start moves them all.Measurements
Verdict: pass, the bug is gone. On a tester's Steam Deck OLED, with an unreadable entry in
user/and the data folder on another file system, main lost 10 save files out ofuser/into a folder the game does not read; with this PRuser/was unchanged in both starts and the new place held no files, and once the entry was readable the next start moved everything. The game-free test fails on main and passes here.Hardware: Steam Deck OLED, AMD Custom APU 0932 (RADV VANGOGH), 16 GB, Mesa 26.2.0-devel git-5c65d4fc31, SteamOS 3.9.5 (build 20261023.100), kernel 7.2.7-valve2 (dukenheimer's run, test request T-WYKE); game-free: Linux x86-64 server, no GPU or game
Scene: the first start in a copy of a test game folder on ext4 (/home),
XDG_DATA_HOMEan empty test folder on tmpfs (/tmp);user/held a test save (one account, four slots), an emptytemp/andlocked/unreadable.binwith no read permission, in that order (save,temp,locked), so the save went first; started headless (HOS_HEADLESS=1) with stderr on the journal, closed through the control bridgeMethod: the
saves:lines of each start'slogs/longshot_log.txt, and the entries, modes and sizes ofuser/and the data folder after it. This PR twice with the entry unreadable and once with it readable again (chmod 644), main once with it unreadable; one more start of this PR with the data folder on ext4 (a rename). A behaviour check, not a timing: the outcome of each start is all or nothing. Game-free:python3 -I -B tests/test_linux_saves_move.pyat692a7948and at6464bfde(rebased onto mainaa95d35f), with gcc 13.3 and with clang 23.1.5; before that, at3e562a68and4f15e996on main53f96d78, and before the first rebase atccccaa8bandf7f5578a, twice each, alternatingHow to reproduce: build main
7ff5256eand the combined build (main7ff5256ewith #550 to #558 merged in order), both x86-64-v3 with the release play profile and LLVM 23.1.5; then the steps under How to reproduce, never in a real game folder; for the game-free check, run the test command at each commit7ff5256ef7f5578a(in the combined build)user/after themlongshot/folder the start made)user/(the 10 save files,temp/,locked/);saves-moved.txtbeside the gameEach start ran: in start 3 the frame count went from 190 to 503 in 5 s; starts 1 and 2 were closed while still preparing shaders, after the saves step, which comes first. Within one file system the move is a rename, which never reads the file, so it does not fail there. The tester's report: #557 (comment)
aa95d35f(with the test,692a7948)6464bfdeuser/whole and new place empty after a failed move (gcc and clang the same)Flags and settings
None.
Testing
Tested: Linux x86-64, game-free. The new test fails on the test-only commit
692a7948and passes at6464bfde, this branch rebased onto mainaa95d35f, with gcc 13.3 and with clang 23.1.5 (before, it did the same at3e562a68and4f15e996on main53f96d78, and atccccaa8bandf7f5578a, twice each, alternating) (python3 -I -B tests/test_linux_saves_move.py). Onland/integration27553c83 (main7ff5256ewith this PR and twelve other fixes merged), the thirteen fixes' own tests and the checks their fix commits edit pass, 16 of 16; the fullpython3 -I -B tests/run_all.pyran on that branch too (see the checklist).In the game: dukenheimer ran test request T-WYKE on a Steam Deck OLED (SteamOS 3.9.5) and reported a pass on 2026-11-15 at 03:06 UTC (#557 (comment)). The builds were the combined build of main
7ff5256ewith #550 to #558 merged in order, this PR at its head then,f7f5578a(6464bfdeafter the rebases, the same change bygit patch-id), and main7ff5256efor the before; each in a copy of a test game folder, started headless with stderr on the journal, as a launcher gives it. With an unreadable entry inuser/and the data folder on tmpfs, this PR leftuser/unchanged in both starts and the new place empty, and the third start, with the entry readable again, moved everything; main, once, logged the same "the saves stay" line while 10 save files had already gone to the new place. With the data folder on ext4 the move is a rename and succeeded (this PR, one start).The branch has been rebased since the tester's run, onto main
aa95d35f, with its commits re-authored under the forge's noreply address; each commit's change is identical to before bygit patch-id, so the commits named above are the ones tested and the result stands for this head (5c1b3834).Not tested: A real full disk (an unreadable file stands in for it, in the test and on the Deck). An exFAT SD card as the data folder. A start through Steam in Game Mode with a window (the Deck's starts were headless). A rename back that fails, and a crash during the deferred deletes (see Risks). Windows is not affected:
saves_diris built on Linux only (#ifndef _WIN32; Windows keepsuser/beside the game).Must be tested: None
Risks
What could break: The rollback has limits. Copied entries' old places are deleted after all entries arrived: a crash or power loss during those deferred deletes leaves some entries in both places, and the next start then sees entries in the new place and keeps the old one, which still has everything (no save lost, the move just does not happen). A rename back that fails (rare: same drive, the name was just free) leaves that entry in the new place and logs it. If memory runs out while listing, the move is called off as if an entry failed. The Host code builds on Windows box is left out: the change is inside
#ifndef _WIN32.Turning it off: revert the commit
Compatibility: Nothing changes for a move that succeeds; one that fails now leaves the saves in
user/, where the game keeps using themChecklist
Everyone
type(scope): description) of the status line's type.main. Onto main94abdd57(head8b6de684) with no conflict, with the commit subjects made Conventional Commits, which CI's lint job now checks for every commit, on a base that has CI's config (#417); each commit's change is the same as before bygit patch-id. The commits named in this description are from before this rebase:692a7948is now881284a5,6464bfdeis now90efaf70,5c1b3834is now8b6de684. Re-run on94abdd57:test_linux_saves_movefails at the test-only commit881284a5and passes at the head, with g++ 13.3 and clang 23.1.5; of the 35 checks that are this PR's or read its files (g++ 13.3), 32 pass and the 3 that fail fail on main94abdd57too (test_crash_dialog,test_lift_cache,test_pipeline_tour).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). Run on this branch atab8fcc68on main53f96d78, before this rebase (5c1b3834now, the same patches bygit patch-id; the Vulkan and SPIR-V headers from the CPM cache, 8 at once, on a loaded Linux x86-64 server): 530 passed, 15 failed, 48 skipped. The 15 are the ones that fail on main53f96d78in the same run:test_appimage_player_build,test_bake_portability,test_cpm_discovery,test_crash_dialog,test_env_on,test_lift_cache,test_lift_cleanup_error,test_make_game_cache,test_pipeline_tour,test_readback_late,test_vk_pgo,test_vram_budget_row,test_walk_key,nx2/test_mesh_first_types.pyandnx2/test_texture_init_barrier.py. Main's run also timed outtest_fpx_sticky(it passes alone there). After the rebase onto mainaa95d35f, at5c1b3834: 35 checks, this PR's test and those that read its changed files, with g++ 13.3 and with clang 23.1.5: 32 pass; the 3 that fail fail on mainaa95d35ftoo (run alone there, both compilers):tests/test_crash_dialog.py(#626),tests/test_lift_cache.py(#628),tests/test_pipeline_tour.py(#627).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md).docs/tested/2026-11-15-saves-move-dukenheimer-t-wyke.md, the tester's run;scripts/tested_configurations.pyandtests/test_tested_configurations.pypass with it.mainand passing here.saves_dir()is inside#ifndef _WIN32). Linux: the game linked in the tester's combined build (x86-64-v3, LLVM 23.1.5) and ran. After the rebase onto mainaa95d35f,host/hos_main.cat5c1b3834checked whole (-fsyntax-only -Wall -Wextrawith the host's include paths) for Linux (clang 23.1.5) and Windows (llvm-mingw 20261104): the same warnings as main aa95d35f's, none new.user/ccccaa8b0csaves_dir)Filled in: the full game-free suite on the combined branch, the Windows and Linux builds, and the test request T-WYKE. Still work in progress until a Steam Deck run is in.
Test request T-WYKE: pass
Built in a combined build of main
7ff5256ewith #550 to #558 merged in order, this PR at its headf7f5578a; main7ff5256efor thebefore. Setup below.
Result
saves:linesaves: 1 of <game>/user's entries could not go to <data>/longshot: the saves stay in <game>/userlongshot/folder, no fileslongshot/foldersaves: moved from <game>/user to <data>/longshotsaves-moved.txtbeside the gametemp/andlocked/entries, with the same modes and sizes. The test data folder held no files, only the empty
longshot/folder the start made. Withthe file readable again, the third start moved the saves and user/ went.
with only
locked/,temp/and an emptysave/<account>/.account.sav,option.savand all four slots (progress.savandcaption.sav) were in the test data folder, where the line says the game does not look.reads the file, so it does not fail there:
saves: moved from <game>/user to <data>/longshot, user/ gone (this PR, one start).shaders; the saves step comes first, before that.
File systems: game folder ext4 (/home); test data folder tmpfs (/tmp), so the move had to copy and could fail on the file.
Setup
kernel 7.2.7-valve2.
7ff5256e, and the combined build: main + #550 to #558, merged in order, all clean. Both for x86-64-v3 with therelease play profile and LLVM 23.1.5.
empty
temp/, andlocked/unreadable.binwith no read permission. The game takes user/'s entries in the folder's own order: heresave,temp,locked, so the save went first.HOS_HEADLESS=1, no window) with stderr on the journal, as a launcher gives it, so the game wrote its ownlogs/longshot_log.txt. Thesaves:lines above are from it. Each start was closed through the control bridge.f7f5578ad4ab8fcc6823Thank you, dukenheimer, for T-WYKE on the Steam Deck (#557 (comment)), main's lost saves included.
Rebased onto main
53f96d78with no conflict: the test and fix commits are the same as before (git range-diff:ccccaa8bis now3e562a68,f7f5578ais now4f15e996), and the run is added asdocs/tested/2026-11-15-saves-move-dukenheimer-t-wyke.mdin a third commit,ab8fcc68. The description now ticks the rebase anddocs/tested/boxes, and has the host code box (the change is Linux-only, inside#ifndef _WIN32).Tests run again after the rebase:
tests/test_linux_saves_move.pyfails at3e562a68(an entry that cannot go:user/lost entries at start 1) and passes at the tip, with gcc 13.3 and with clang 23.1.5. The fullpython3 -I -B tests/run_all.pyon this branch: 530 passed, 15 failed, 48 skipped, the same 15 that fail on main53f96d78in the same run (named in the checklist).host/hos_main.cchecked whole for Linux and Windows with the build's host flags: the same warnings as main's, none new.Why the Deck result still holds: main changed nothing in
saves_dir()or the lines that call it. Since the tested base7ff5256e, main changedhost/hos_main.cin three commits, all elsewhere in the file: the crash window's helper and watcher (a0dac82e; the watcher is off headless, as the tester's starts were),HOS_HLE_STATS(1c9958c1, off unless set) and the removal of the Windows write watch andLONGSHOT_OCCLUSION_OFF(414f56e2). None of them reads or movesuser/or the data folder.WIP: fix(linux): a saves move that fails part way brings back what it movedto fix(linux): a saves move that fails part way brings back what it movedab8fcc68235c1b3834beRebased onto
mainaa95d35f(head5c1b3834); the commits are re-authored with the forge's noreply address: no code change, the same patches bygit patch-id.Tests re-run on the new base, gcc 13.3 and clang 23.1.5:
test_linux_saves_movefails at the test-only commit692a7948and passes at6464bfde; of the 35 checks that are this PR's or read its files, 32 pass and the 3 that fail fail on main too. The Deck run (T-WYKE) stands; the description names the new commits.Code review only (no tests run). Read the
saves_dir()change inhost/hos_main.c.wentis updated whenever realloc succeeds, a failedstrdupcounts as kept), no leak or use after free on the paths I followed. The(move && kept)skip stops further moves after the first failure.#ifndef _WIN32), so no Windows concern. The tester's Steam Deck run and the test fit the description.Approving; nothing needs to change.
5c1b3834be8b6de684a4New commits pushed, approval review dismissed automatically according to repository settings
Rebased onto
main94abdd57(head8b6de684), with no conflict; the commit subjects are now Conventional Commits, which CircleCI's lint job checks for every commit of a pull request (#417), and the base has the CI's config, which a fork's pull request needs at its own head. No other change: each commit's change is the same as before bygit patch-id.Re-run on
94abdd57:test_linux_saves_movefails at the test-only commit881284a5and passes at the head, with g++ 13.3 and clang 23.1.5; of the 35 checks that are this PR's or read its files (g++ 13.3), 32 pass and the 3 that fail fail on main94abdd57too (test_crash_dialog,test_lift_cache,test_pipeline_tour). The description names the new commits.