Skip to content

fix(desktop): run the long setup and update commands off the main thread - #908

Merged
thcp merged 1 commit into
big-releasefrom
fix/b34b-async-setup-commands
Nov 3, 2026
Merged

thcp merged 1 commit into
big-releasefrom
fix/b34b-async-setup-commands

Conversation

@thcp

@thcp thcp commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Refs #405

What changes for the person running setup

  • The window now closes during the GPU step. Before, closing it during a CUDA torch install did nothing until pip finished: the whole step ran on the window's own thread.
  • Closing it ends what that step started: pip, and the step's own checks (nvidia-smi, the torch import and CUDA checks). On Windows they run in a job that the close ends, so no python.exe is left behind.
  • The pip progress line on the GPU step and on the karaoke split repair can now update while the download runs. Before, the step held the thread the window paints on. That follows from how Tauri runs the command; I did not watch it on a running app.
  • The same holds for the other setup steps that take seconds or minutes: checking the runtime, verifying and installing the runtime pack, downloading FFmpeg, starting the backend, and applying an in-app update. The window keeps painting and can close during each of them.

Why, and how it works

  • The cause. A Tauri command that is not async runs on the main thread (tauri-macros 2.6.0, body_blocking). While one runs, the window cannot paint or take a close, and no other command reaches the shell. fix(setup): make the model step honest and interruptible #907 fixed this for the model step. ensure_torch_device and repair_torchvision had the same problem, and so did six more commands.
  • The fix. Each of those commands is now async and does its work in spawn_blocking, as warmup_models does since fix(setup): make the model step honest and interruptible #907. The pattern is one helper, off_main_thread, which warmup_models now uses too.
  • The GPU step's own checks. pip already ran through run_setup_child (fix(desktop): keep the portable WebView2 profile in data, end setup children with the app #897, fix(setup): make the model step honest and interruptible #907). nvidia-smi, the site-packages probe, the import checks and the CUDA verify did not. A CUDA verify wedged in the driver ran on for up to 2 minutes after StemDeck closed ([Bug]: Stuck First Launch #502). They now run through run_setup_child too, via setup_probe_output.
  • Nothing starts after the close. The GPU step runs pip passes and checks one after another. Once the window closes, the next one could start in the moment before StemDeck exits, and on Linux and macOS nothing would stop it. stop_backend now sets a closing flag, and run_setup_child refuses to start a child after it.
  • The GPU step stops touching torch after the close. A check stopped by the close reads as a failed check. Without a guard, the step would go on to move the working torch aside, or delete a build, while StemDeck exits. still_open is now checked before each move, the commit and the saved result. A swap left half done is put back at the start of the next GPU step (Falling back from CUDA to CPU torch leaves torch unimportable on Windows #731).
  • The close waits for an update's renames. apply_app_update replaces backend/ and the executable with a few renames, and each one retries for up to about 5 s while a virus scanner holds a handle. Stopped between two renames, the install would have no backend/ or no executable, with nothing left to repair it.
    • While the renames and the relaunch run, a close is held off (prevent_close). The update then relaunches and exits by itself.
    • A close that came first cancels the swap before it starts.
    • Before this change the main thread gave the same protection, only with a frozen window.
  • One setup step at a time. The main thread used to run these commands one after another. Now a reload of the wizard during a CUDA install could start the GPU step again beside the first one. Its nvidia-smi check would then be refused the one setup slot and read as "no GPU", and it could start the backend while pip was still writing into python/.
    • A SetupStep claim is now held by the GPU step, the torchvision repair, the model step, the runtime install and the FFmpeg download. A second one fails at once and says which step is running.
    • start_backend keeps its starting guard, and apply_app_update keeps its claim.
  • A close in the middle of the other steps is recovered from.
    • Runtime pack extract: runtime.tmp is cleared on the next try, and ensure_workspace puts runtime.old back.
    • FFmpeg: the same, with ffmpeg.old.
    • Backend start: the backend runs in its job on Windows, and watches StemDeck's pid elsewhere.

Every command in the shell

Command Was Verdict
ensure_torch_device blocking Fixed. nvidia-smi, Python checks, several GB through pip.
repair_torchvision blocking Fixed. A Python check, then a pip download.
start_backend blocking Fixed. Spawns the backend and waits up to 90 s for health.
probe_runtime blocking Fixed. Runs the interpreter and the FFmpeg pair.
verify_runtime_pack blocking Fixed. SHA-256 of the runtime archive.
extract_runtime_pack blocking Fixed. Hash and extract of the runtime archive.
ensure_external_assets blocking Fixed. Downloads FFmpeg.
apply_app_update blocking Fixed. Hash and extract of the update, up to 15 s backend stop.
warmup_models async (#907) Unchanged, now through the shared helper.
ensure_workspace blocking Stays. Creates folders, renames on one volume, copies two small JSON files.
runtime_pack_status blocking Stays. Reads small files.
skip_model_warmup blocking Stays. A lock and one job terminate.
build_target blocking Stays. One file check.
open_url blocking Stays. Spawns the browser opener and never waits on it.
native_file_drop blocking Stays. A constant.
current_exports_dir blocking Stays. A store read and one folder create.
store_get, store_set, reset_user_data blocking Stay. The small JSON store.
mark_store_migration_done blocking Stays. Writes an empty file.
download_runtime_pack, check_app_update, download_app_update, restart_backend, next_drop_signal, read_dropped_file, save_audio_file, pick_export_destination, download_to_path, reveal_saved, start_audio_drag, pick_stems_folder, pick_exports_folder async Already off the main thread.

A test now holds this list: no_command_that_can_run_long_holds_the_main_thread fails if a command is blocking and not on the "stays" list.

How it was verified

Red then green: I ran each new test against the old code first.

  • no_command_that_can_run_long_holds_the_main_thread
    • Old code: failed, "these commands run on the main thread: probe_runtime, verify_runtime_pack, extract_runtime_pack, apply_app_update, ensure_external_assets, start_backend, ensure_torch_device, repair_torchvision".
    • Now: passes.
    • It reads main.rs and checks which commands are async, because that keyword is what tauri-macros uses to choose the main thread. No test here drives a real WebView's main thread.
  • closing_the_window_ends_a_gpu_step_probe
    • A stand-in interpreter that runs for 20 s, checked with the torchvision import check, then stop_backend as the close handler calls it.
    • Old code: failed, "the probe ran on for 20.17s".
    • Now: passes, ended at once.
  • no_setup_child_starts_once_the_window_is_closing
    • Old code: failed, a setup child was started after the close.
    • Now: passes. It also checks that still_open reports the close.
  • a_second_setup_step_is_refused_while_one_runs and the_close_waits_for_an_update_swap_and_cancels_one_not_begun
    • New. On the old code they do not compile, because the claims they test do not exist there. Neither was a behaviour test on the old code.
  • cargo test (Windows): 234 passed, 2 ignored.
  • cargo fmt --check and cargo clippy --tests -- -D warnings on Windows: clean.
  • Not compiled here: the Linux and macOS cfg lines (verify_mps_torch, the macOS branch of repair_torchvision, the unix stand-in interpreter). The PR's Rust check jobs cover them.
  • No real CUDA torch was installed anywhere: the tests use stand-in processes in throwaway folders.

Guide and languages

No Guide entry and no i18n change. No control changed, and no text in desktop/ui or static/js changed.

Update path

  • Changes the desktop shell (Rust), so it needs a new binary, not a payload-only update.
  • Does not touch uv.lock.

Not changed here

  • The GPU step has no Skip. Closing the window is the way out. A Skip would have to stop pip and also end the loop over CUDA builds, then put CPU torch back. That is its own change, with its own wording.
  • python_stdlib_ok (in probe_runtime) waits on import encodings with no time limit. It is now off the main thread, so a hang there no longer freezes the window, but it still has no timeout.
  • apply_app_update checks the archive's hash, then opens it again by path to extract it. download_app_update could now run in between and rewrite that file. Only the release URLs it accepts can be written there, and the page hides the download button by then. Hashing and extracting from one open file would close this gap.
  • A wizard reloaded while the torchvision repair runs on the fast path is refused the repair step. It says so in the console and goes on to start the backend while the first run's pip may still be writing torchvision. That is much narrower than before, but not closed.
  • A close during start_backend's health wait, on Linux or macOS, leaves the backend running until its parent watchdog sees StemDeck gone, about 1 s later. On Windows the backend's job ends it at once.

What needs a human

On Windows, NVIDIA build, in a throwaway portable folder. No automated test here drives a real WebView2.

  1. Unzip the NVIDIA portable build into a new folder, with no data folder next to it.
  2. Start StemDeck. At the GPU step, check that the line shows pip's progress ("Downloading torch-... N / M MB") while it runs.
  3. During that download, close the window. It should close within a few seconds.
  4. In Task Manager, check that no python.exe and no pip from that folder remain.
  5. Start again. The GPU step should run again from the start and finish, or fall back to CPU with the usual message.

A Tauri command that is not async runs on the main thread (tauri-macros
2.6.0 body_blocking). The GPU step, the torchvision repair, the runtime
check, verify and install, the FFmpeg download, the backend start and the
in-app update all did, so for as long as each ran the window could not
paint or close, and a stuck pip froze the app. Each is now async and works
in spawn_blocking, as warmup_models does since #907.

The GPU step's probes (nvidia-smi, the torch import and CUDA checks) now
run through run_setup_child, so closing the window ends them as it ends
pip. Off the main thread, three things it used to guarantee are now
explicit: no setup child starts after the close, and the GPU step stops
before it moves torch; one setup step runs at a time, so a reload cannot
start the GPU step beside a running one; and the close waits for an
update's renames and relaunch, which cut midway would leave no backend/ or
no executable.

A test reads main.rs and fails when a command is blocking without being on
the list of commands that cannot run long.

Refs #405
@thcp
thcp merged commit 4ee9d9e into big-release Nov 3, 2026
13 checks passed
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.

1 participant