Skip to content

fix(builder): Cancel stops a Linux update while it downloads - #2160

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
jarynclouatre:fix/linux-update-cancel
Nov 11, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
jarynclouatre:fix/linux-update-cancel

Conversation

@jarynclouatre

Copy link
Copy Markdown
Contributor

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_bytes and install in linux_update now take an optional stop event. With one, a part is read a megabyte at a time and the download ends with Cancelled at the next one, or in the wait between two tries, and a cancelled download gets none of the retries a failed one does. install looks 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)

  • The real window on Xvfb, started with --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, install cancelled in the first or the last part, an update nobody cancels, and the builder's own cancel and begin_update on a stand-in window. 12 fail on main.
  • tests/test_linux_update.py and tests/test_update_tracks.py pass.
  • Not tested: a real update from GitHub in the AppImage.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-981 with :1140-1141: the thread checks stop.is_set() and then queues do-build, but pump only drains the queue every 100 ms. If Cancel lands after the check and before pump handles do-build (most likely on the already-fetched path, where the thread does no download and finishes at once), start_make_update still runs. make_game.py then 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 Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@jarynclouatre
jarynclouatre force-pushed the fix/linux-update-cancel branch from 24c12fe to f62186e Compare November 11, 2026 21:50
@jarynclouatre

Copy link
Copy Markdown
Contributor Author

Thanks. Rebased on main (6f4f28a) and done in f62186e:

  • The conflict with fix(linux): prevent implicit game downgrades in the builder #2406: main's manifest.tag != target check stays, right after the startup_manifest(..., get=...stop) call.
  • The window's do-build handler checks the stop event, so a Cancel that comes after the thread's last check and before the window takes the line ends as cancelled ("The update was cancelled. Your game folder is unchanged.") and does not start make_game.py. A new test covers that order.
  • test_linux_downgrade.py (from main) builds its window by hand, so it now gives it the stop event too; and the cancel tests' stub gained what main's begin_update now asks for (game_version, older_version, older_ok).

Tested on Linux: test_linux_update_cancel.py (14 tests), test_linux_downgrade.py, test_linux_update.py, test_linux_update_skip.py and test_linux_saves_move.py pass.

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hckrman101
hckrman101 merged commit ed2fff0 into NotRexed:main Nov 11, 2026
13 checks passed
madebyjordan added a commit to madebyjordan/Longshot that referenced this pull request Nov 11, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants