Repository navigation
fix(builder): Cancel stops a Linux update while it downloads - #2160
Conversation
Joey-Magnus
left a comment
There was a problem hiding this comment.
Nice fix, and well contained. Without a stop event, fetch_bytes, _read_at_most and install behave exactly as before. With one, a cancel is seen between 1 MB pieces, in the wait between tries, between parts, and once more right before the new folder replaces the old one, and the except Exception in install removes the staged folder. Cancelled is re-raised ahead of the generic handlers in both fetch_bytes paths, so a cancel never gets retried or falls through to the API download. On the builder side, Cancel only exists on the progress page, and a normal build sets self.proc in the same callback that shows the page, so the new elif self.proc is None branch can only run while an update's thread is working. I ran tests/test_linux_update_cancel.py (13 tests), tests/test_linux_update.py and tests/test_update_tracks.py at the PR's head, and all pass. CI is green, and it merges cleanly with main.
Two small things:
- should fix
builder/linux/hookshot_builder.py:979-981with:1140-1141: the thread checksstop.is_set()and then queuesdo-build, butpumponly drains the queue every 100 ms. If Cancel lands after the check and beforepumphandlesdo-build(most likely on the already-fetched path, where the thread does no download and finishes at once),start_make_updatestill runs.make_game.pythen starts, and "Stopping the update" is overwritten by "Compiling what changed". The fix is a guard in the handler:elif kind == "do-build": if self.stop_update.is_set(): self.lines.put(("update-error", str(linux_update.Cancelled()))) else: self.start_make_update(v)
- minor
builder/linux/linux_update.py:256-263: a cancel is noticed between reads, so on a connection that has stalled mid-read it only takes effect when the socket times out (60 s for a part, 120 s for the API path). The page says "Stopping the update" during that time, so it isn't wrong, just possibly slow. No change needed if you're fine with that; it might be worth a word in the PR description's "Not tested".
Joey-Magnus
left a comment
There was a problem hiding this comment.
It needs a rebase: it conflicts with #2406 in builder/linux/hookshot_builder.py around startup_manifest. Keep main's manifest.tag != target check alongside the new get=...stop argument.
The earlier should-fix is still open: a Cancel between the thread's stop.is_set() check and pump() handling do-build still starts make_game.py. A guard in the do-build handler (~line 1140) closes it. Everything else checks out.
The Linux builder's Cancel only stopped make_game.py. An update first downloads the new version's parts on a thread of its own, with no make_game.py yet, and through those minutes the button did nothing: the download went on, the new builder started and the update ran. The Windows builder cancels its download. Cancel now sets an event the download is given. linux_update reads a part a megabyte at a time and ends with Cancelled at the next one, or in the wait between two tries, and a cancelled download gets none of the tries a failed one does. install looks at the event between parts and once more before the new folder takes the old one's place; it removes what it staged, as for any error, so the installed version stays as it was. The page then says "The update was cancelled. Your game folder is unchanged." with Back, the way a failed update does. Without an event, fetch_bytes and install read and wait as before.
24c12fe to
f62186e
Compare
|
Thanks. Rebased on main (6f4f28a) and done in f62186e:
Tested on Linux: |
hckrman101
left a comment
There was a problem hiding this comment.
Looks good to me. Checked at f62186e.
The open point from Joey-Magnus's review is fixed: the do-build handler in pump now looks at the stop event, so a Cancel that lands after the thread's last check ends as cancelled and does not start make_game.py. The test for that order is there too. The rebase keeps main's manifest.tag != target check right after the startup_manifest(..., get=...) call, and the get override is only used when HOOKSHOT_LINUX_LIST is unset, so it does not change the override path.
I also checked that elif self.proc is None can only be reached during an update's download: self.proc is reset after a build ends, and the Cancel button only exists on the progress page. Cancelled is re-raised ahead of the generic handlers in both fetch_bytes paths, so no retries and no fall-through to the API download, and install removes the staged folder through its existing except Exception. Without an event, the reads and waits are as before.
Still open, and not worth blocking on: Joey-Magnus's minor note that a stalled socket only ends at its timeout (60 s / 120 s), so "Stopping the update" can stay up for that long. A word in the description would cover it.
CI is green (Linux and Windows Python checks, AppImage, Windows Vulkan build); the Linux-only test is skipped on Windows through RUN_ALL_SKIP.
Conflicts with NotRexed#2141, NotRexed#2160, NotRexed#2367 and NotRexed#2447, resolved for the launcher: - NotRexed#2367 (portable homes): Storage.Base is BuilderHome.Base. The launcher's hidden --engine setup gets the home (BuilderHome.PassTo), and the setup's engine mode opens it before it installs (it ran before init_home and would have stopped on every install). In engine mode a reused part is linked, in a portable home too, so the version in use stays whole; otherwise a part is moved, or copied in a portable home, as NotRexed#2367 has it. The post-build clean-up is Storage's, so it already uses the portable home. A portable home makes no shortcuts by default, as NotRexed#2367's portable install. - NotRexed#2447 (Save my pipeline list): its button was on the launch page the launcher replaces. It is a row of the menu's Settings now, on Windows and Linux, with the short of what it did under it and the whole in its tooltip. - NotRexed#2141 (NotRexed#2129, back from an update that stopped): the launcher's one way back is BackFromBuild, for Back and for a cancel alike: to the launcher of the game, which lets the update go and offers it again, else to the build's choices. test_builder_update_back.py's Windows half now checks that. - NotRexed#2160 (Cancel during a Linux update download): its checks are in the launcher's message_in, which pump hands each line to; test_linux_update_cancel.py's stand-in compiles both. The hard-linked parts (link_tree): nothing writes a part's file in place (compilers, Python, headers and libraries are read; Python's bytecode caches are written to a new file and renamed over), now said where the links are made. test_portable_setup_transaction.py also runs the engine mode's reuse: linked, in a standard and a portable home, the version in use whole. test_portable_builder_home.py's fixture compiles LaunchList.cs with Storage.cs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the Linux builder, Cancel does nothing while an update is still downloading. The download carries on, the new builder starts and the update runs. Cancel only knew how to stop make_game.py, and during the download there is none yet. The Windows builder cancels its download.
fetch_bytesandinstallinlinux_updatenow take an optional stop event. With one, a part is read a megabyte at a time and the download ends withCancelledat the next one, or in the wait between two tries, and a cancelled download gets none of the retries a failed one does.installlooks at the event between parts and once more before the new folder takes the old one's place, and removes what it staged, as for any error. Without an event they read and wait as before. The builder's Cancel sets the event when no make_game.py is running, and the page then says "The update was cancelled. Your game folder is unchanged." with Back, the way a failed update does.Once the new version is completely in place the new builder still starts; from there Cancel stops make_game.py as it always did.
Testing (Linux, Tk 8.6)
--update, with a 30 MB part served slowly by a patched opener. Before: Cancel changes nothing, the version is installed and the new builder is started. After: the page shows the cancelled message at once, the staging folder is gone and the installed version is untouched.tests/test_linux_update_cancel.py(new, 13 tests) covers the read in pieces (same bytes, same size limit), no retry after a cancel, Cancel during the wait between tries,installcancelled in the first or the last part, an update nobody cancels, and the builder's owncancelandbegin_updateon a stand-in window. 12 fail on main.tests/test_linux_update.pyandtests/test_update_tracks.pypass.