Canceling a connect brings the console straight back, instead of sticking on "Canceling…" #329

Merged
enricobuehler merged 1 commits from worktree-console-cancel-connect-stuck into main 2026-08-19 11:29:23 +00:00
Owner

Pressing B on the console's connect takeover left it latched on "Canceling…", with no input that could reach it — on Linux and Android both. Killing the app was the only way out.

Why

The takeover could only be dismissed by a session phase coming back from the embedder, and nothing guaranteed one would arrive.

  • AndroidSkiaConsole.launch() returns early on the canceled path (if (d.cancelled.get()) … return@post) without ever calling nativeConsoleSessionPhase. No phase, ever: the takeover stayed up until the process died.
  • Linux — the shell waited for the pump's Failed/Ended, but the pump was parked inside the blocking NativeClient::connect*, which had no abort. That is 15 s on a normal dial and 185 s on a request-access connect the host holds pending an operator's approval.

What changed

Two halves, each at the point every caller routes through.

pf-console-ui drops the takeover itself on Back. Cancel is the user's decision and needs no confirmation from the wire; the CancelConnect action still goes out, and every embedder already handles a dial that lands afterwards (quit-close the connector, route the end back silently). With no waiting state left to render, Connecting.canceling and the "Canceling…" card go with it — the stuck state is now unrepresentable rather than merely fixed.

NativeClient::connect_with_audio_format takes an optional abort switch, polled while the call is blocked, and the session pump passes its stop flag. The embedder's cancel now reaches a dial that has not landed yet, instead of being answered whenever it eventually does. Taking it is the same give-up as running out of budget (quit close + shutdown), so the worker stops re-dialing and the host tears down rather than lingering for a reconnect nobody wants.

The second half is not cosmetic. Without it, dismissing the modal early would let a re-launch reach prev.shutdown() and join a pump still parked in the dial — trading a stuck UI for a frozen one.

Two decisions worth flagging

  • The caller's flag is deliberately not aliased onto the client's own shutdown. The pump reads that one to mean "this connection died" and derives the session's end reason from it; a caller-set flag would race it, and a dropped link would have reported no reason at all. (This was my first cut, and it was wrong.)
  • The Android JNI dial gets no flag. Its connect runs on a pool thread, so a parked one costs a thread, not a stuck UI — the Kotlin side already discards the late handle via Dial.cancelled. A comment at the call site says so, in case that stops being true.

Verification

Run in the punktfunk-rust-ci image (macOS can't compile the cfg(linux) halves):

  • cargo test -p pf-console-ui185 passed, 0 failed, including the rewritten connect_flow_raises_launch_and_cancel, which now asserts the takeover is gone the moment B is pressed rather than that a second B is swallowed.
  • cargo check — clean on pf-client-core and punktfunk-core --features quic.
  • cargo clippy --all-targets -- -D warnings — clean on all three crates.
  • rustfmt --check — clean on all six touched files.

Not exercised on a real Deck or phone.

Pressing B on the console's connect takeover left it latched on "Canceling…", with no input that could reach it — on Linux and Android both. Killing the app was the only way out. ## Why The takeover could only be dismissed by a session phase coming back from the embedder, and nothing guaranteed one would arrive. - **Android** — `SkiaConsole.launch()` returns early on the canceled path (`if (d.cancelled.get()) … return@post`) without ever calling `nativeConsoleSessionPhase`. No phase, ever: the takeover stayed up until the process died. - **Linux** — the shell waited for the pump's `Failed`/`Ended`, but the pump was parked inside the blocking `NativeClient::connect*`, which had no abort. That is 15 s on a normal dial and **185 s** on a request-access connect the host holds pending an operator's approval. ## What changed Two halves, each at the point every caller routes through. **`pf-console-ui` drops the takeover itself on Back.** Cancel is the user's decision and needs no confirmation from the wire; the `CancelConnect` action still goes out, and every embedder already handles a dial that lands afterwards (quit-close the connector, route the end back silently). With no waiting state left to render, `Connecting.canceling` and the "Canceling…" card go with it — the stuck state is now unrepresentable rather than merely fixed. **`NativeClient::connect_with_audio_format` takes an optional abort switch**, polled while the call is blocked, and the session pump passes its stop flag. The embedder's cancel now reaches a dial that has not landed yet, instead of being answered whenever it eventually does. Taking it is the same give-up as running out of budget (quit close + shutdown), so the worker stops re-dialing and the host tears down rather than lingering for a reconnect nobody wants. The second half is not cosmetic. Without it, dismissing the modal early would let a re-launch reach `prev.shutdown()` and join a pump still parked in the dial — trading a stuck UI for a frozen one. ## Two decisions worth flagging - The caller's flag is **deliberately not aliased** onto the client's own `shutdown`. The pump reads that one to mean "this connection died" and derives the session's end reason from it; a caller-set flag would race it, and a dropped link would have reported no reason at all. (This was my first cut, and it was wrong.) - The **Android JNI dial gets no flag**. Its connect runs on a pool thread, so a parked one costs a thread, not a stuck UI — the Kotlin side already discards the late handle via `Dial.cancelled`. A comment at the call site says so, in case that stops being true. ## Verification Run in the `punktfunk-rust-ci` image (macOS can't compile the `cfg(linux)` halves): - `cargo test -p pf-console-ui` — **185 passed, 0 failed**, including the rewritten `connect_flow_raises_launch_and_cancel`, which now asserts the takeover is gone the moment B is pressed rather than that a second B is swallowed. - `cargo check` — clean on `pf-client-core` and `punktfunk-core --features quic`. - `cargo clippy --all-targets -- -D warnings` — clean on all three crates. - `rustfmt --check` — clean on all six touched files. Not exercised on a real Deck or phone.
enricobuehler added 1 commit 2026-08-19 11:28:14 +00:00
fix(console-ui): canceling a connect returns the console immediately, and aborts the dial
ci / bun-nix (pull_request) Successful in 30s
ci / docs-site (pull_request) Successful in 1m19s
ci / web (pull_request) Successful in 1m24s
apple / swift (pull_request) Successful in 2m13s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m4s
android / android (pull_request) Successful in 6m35s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m36s
ci / rust (pull_request) Successful in 7m56s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 14m36s
8f4e71f8dc
The connect takeover could only be dismissed by a session phase coming back from the
embedder, and nothing guaranteed one would. Pressing B latched "Canceling…" and waited:

- Android's console shell never sent a phase at all on the canceled path — `launch()`
  returns early when `Dial.cancelled` is set — so the takeover stayed up until the app
  was killed.
- The desktop shell waited for the pump's `Failed`/`Ended`, but the pump was parked
  inside the blocking `NativeClient::connect*`, which had no abort. That is 15 s on a
  normal dial and 185 s on a request-access connect the host holds pending approval.

Two halves, both at the point every caller routes through:

`pf-console-ui` drops the takeover itself on Back. Cancel is the user's decision and
needs no confirmation from the wire; the `CancelConnect` action still goes out, and
every embedder already handles a dial that lands afterwards (quit-close the connector,
route the end back silently). With no waiting state left to render, `Connecting.canceling`
and the "Canceling…" card go with it.

`NativeClient::connect_with_audio_format` takes an optional abort switch, polled while
the call is blocked, and the session pump passes its stop flag — so the embedder's cancel
now reaches a dial that has not landed yet instead of being answered whenever it does.
Taking it is the same give-up as running out of budget (quit close + shutdown), so the
worker stops re-dialing and the host tears down rather than lingering for a reconnect
nobody wants. Deliberately NOT aliased onto the client's own `shutdown`: the pump reads
that to mean "this connection died" and derives the session's end reason from it, which a
caller-set flag would race — a dropped link would have reported no reason at all.
enricobuehler merged commit fa946a16b9 into main 2026-08-19 11:29:23 +00:00
enricobuehler deleted branch worktree-console-cancel-connect-stuck 2026-08-19 11:29:24 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#329