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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.
SkiaConsole.launch()returns early on the canceled path (if (d.cancelled.get()) … return@post) without ever callingnativeConsoleSessionPhase. No phase, ever: the takeover stayed up until the process died.Failed/Ended, but the pump was parked inside the blockingNativeClient::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-uidrops the takeover itself on Back. Cancel is the user's decision and needs no confirmation from the wire; theCancelConnectaction 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.cancelingand the "Canceling…" card go with it — the stuck state is now unrepresentable rather than merely fixed.NativeClient::connect_with_audio_formattakes 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
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.)Dial.cancelled. A comment at the call site says so, in case that stops being true.Verification
Run in the
punktfunk-rust-ciimage (macOS can't compile thecfg(linux)halves):cargo test -p pf-console-ui— 185 passed, 0 failed, including the rewrittenconnect_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 onpf-client-coreandpunktfunk-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 referenced this pull request2026-08-19 23:24:49 +00:00