Skip to content

A second StemDeck instance silently adopts the first one's backend #424

Description

@thcp

What happens

Launching a second StemDeck while one is already running does not start a second backend. The new window ends up driving the already-running instance's backend, including its data directory and library.

Found while testing a portable install in an isolated folder: its window showed the library and jobs of a completely different install on another drive.

Why

reserve_port (desktop/src-tauri/src/main.rs) tries the configured port (default 8000):

if let Ok(listener) = TcpListener::bind(("127.0.0.1", desired)) { ... }

On Windows, binding 127.0.0.1:8000 succeeds even while another process holds 0.0.0.0:8000, because neither socket sets SO_EXCLUSIVEADDRUSE. So the port looks free.

The spawned uvicorn then binds 0.0.0.0:<port> and dies:

ERROR: [Errno 10048] error while attempting to bind on address ('0.0.0.0', 8000):
only one usage of each socket address (protocol/network address/port) is normally permitted

wait_for_health(port, ...) then polls http://127.0.0.1:8000/api/health, gets a 200 from the first instance, and reports success. The shell navigates the webview there and everything looks fine.

The mismatch is that the reservation probes 127.0.0.1 while the child binds 0.0.0.0, and the health check never verifies that the process answering is the child we just spawned.

Reproduce

  1. Start StemDeck (binds 0.0.0.0:8000).
  2. Extract a separate portable build elsewhere and start it.
  3. The second window shows the first install's library. data/logs/backend.log in the second install contains the 10048 error; no backend process exists for it.

Impact

  • Two installs share one backend and one data dir, with no indication anything is wrong.
  • The second instance's own backend is dead, so its settings, jobs folder, and device selection are ignored.
  • Closing the first window kills the backend the second is still using.
  • Anything the second window does writes into the first install's data.

Suggested fix

Any one of these closes it; the third is the real guard:

  1. Bind the reservation probe to 0.0.0.0, matching what uvicorn actually binds, so a conflict is detected.
  2. Set SO_EXCLUSIVEADDRUSE on the probe socket.
  3. Verify the health responder is ours, e.g. have the backend report its PID on /api/health (or a nonce passed in via env) and have wait_for_health require a match before proceeding. Without this, any process answering on that port is trusted.

Also worth surfacing the child's exit: start_backend currently reports success while the process it spawned has already died.

Environment

Windows 11, StemDeck portable (Windows x64 CPU build). Unrelated to #421; found while testing that work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions