Public Access
Record the renderer-reattach correction and its phone checks
The port notes carry the third correction of this round: the data plane assumed a renderer that attaches once and lives forever, which no foreground service can make true of Android's separate WebView renderer process. Phase 11 gains the two checks a phone can run — close and reopen a connection, and a backgrounded shell surviving its renderer being killed, banner and all.
This commit is contained in:
@@ -238,6 +238,37 @@ The parts that are definitely different are the on-screen keyboard, and the fact
|
||||
needs Ctrl, Esc, Tab and arrows that the software keyboard does not offer — every Android SSH client ships an
|
||||
accessory key row for this. That is UI work, not porting.
|
||||
|
||||
> **⚠️ Corrected by the build. The data plane assumed a renderer that attaches once and lives forever, and
|
||||
> that assumption is WebView2's truth, not Android's.** Desktop's WebView2 process starts with the window and
|
||||
> dies with it; `TerminalDataPlane` was written to that reality — one socket, attached once,
|
||||
> `Interlocked.Exchange`-guarded against a second attach ever happening at all. On a phone the WebView's own
|
||||
> renderer process is a separate thing from the app process the foreground service above is keeping alive,
|
||||
> and Android kills *that* independently — under memory pressure, or simply for being backgrounded — with no
|
||||
> foreground service able to save it. The page then reloads with a fresh socket, and three things broke on
|
||||
> that reload before this was found: the second attach was refused outright (`409 Conflict`), because a
|
||||
> second valid upgrade could only mean a bug or a hostile second process, never our own page coming back; a
|
||||
> send into the dead first socket threw, and that exception unwound `TerminalSessionPump`'s flush loop,
|
||||
> freezing the still-live shell behind it — `LiveSessionCount` kept counting a session nothing would ever
|
||||
> drain again; and every byte sent while no page was attached had already spent flow-control credit that no
|
||||
> acknowledgement could ever return, so a session outliving 256 KiB of output into a dead page stalled for
|
||||
> good regardless of the other two. Waiting for the old socket to notice it was dead and close on its own
|
||||
> was never going to be enough either — a killed renderer sends no TCP FIN, so the old receive loop could sit
|
||||
> unaware for the whole 30-second keepalive.
|
||||
>
|
||||
> Fixed as a takeover rather than a guard: a second valid upgrade — origin, token and subprotocol all
|
||||
> checked exactly as before — now displaces whatever socket was attached instead of being refused, since
|
||||
> only this app's own page ever knows the token, so a second valid attach *is* that page, back again.
|
||||
> `TerminalDataPlane.SendAsync` no longer lets a dead-socket send escape as a fault; it reads as "nobody
|
||||
> listening," same as no socket being attached at all. `TerminalWorkspace` resets each live session's credit
|
||||
> window on every attach and resends its `SessionOpened` frame, flagged as a replay, so the fresh page
|
||||
> rebuilds the pane and the pump stops waiting on an acknowledgement that was never coming. And
|
||||
> `terminal.js`'s socket now retries itself, forever, with backoff, instead of reporting the connection
|
||||
> failed and stopping — the page dies with the app anyway, so there is no case where retrying is the wrong
|
||||
> call. What is **not** recovered, and says so rather than pretending otherwise: scrollback across a page
|
||||
> reload. It lived in the page's own DOM, and a reloaded page is a new DOM. The replay banner — *"the view
|
||||
> reconnected; earlier output stayed on the host"* — is that honesty put where the person looking at the
|
||||
> terminal will actually read it, not buried in a log.
|
||||
|
||||
---
|
||||
|
||||
## Decisions taken
|
||||
|
||||
@@ -1745,6 +1745,39 @@ is a terminal that answers the buttons and ignores the keyboard: it reads as the
|
||||
Worth doing on the software keyboard too, where the same fault shows as the keyboard closing on the first
|
||||
tap of an arrow key.
|
||||
|
||||
### 11.11 Closing a connection and opening a new one both take you somewhere real
|
||||
|
||||
Open a shell, close its tab, then open a different one from HOSTS.
|
||||
|
||||
**Pass:** the new terminal renders and takes input straight away — no stuck "Connecting…" status, no blank
|
||||
pane that never receives the prompt.
|
||||
|
||||
**Failure means:** `TerminalDataPlane` refused the page's reattach. The renderer's `WebSocket` does not
|
||||
survive a tab going from one to zero and back to one on every device, and a host that answers a second valid
|
||||
upgrade with `409 Conflict` instead of taking the socket over leaves every terminal after the first
|
||||
permanently unreachable — see the correction in `docs/android-port.md`'s terminal section.
|
||||
|
||||
### 11.12 A backgrounded shell survives its renderer being killed · **needs several minutes, or developer tooling**
|
||||
|
||||
With a shell open and something worth reading in its scrollback, background the app (home button, not back)
|
||||
for several minutes — long enough for Android to consider reclaiming it — then return. If the device exposes
|
||||
it, forcing a stop of the WebView renderer process from Developer Options while backgrounded is the more
|
||||
reliable way to trigger the same thing on demand rather than waiting on the OS's own judgement. Either way,
|
||||
type something once you are back.
|
||||
|
||||
**Pass:** one of two honest outcomes, both good. Either the pane is exactly as it was — the renderer process
|
||||
survived, so nothing needed to happen — or the pane is empty but for a dim line reading `── the view
|
||||
reconnected; earlier output stayed on the host ──`, meaning the page reloaded and reattached. In both cases
|
||||
what is typed now reaches the shell, and the shell is still the same one — not a new tab, not a reconnect
|
||||
sheet, no "Connecting…" status stuck on screen.
|
||||
|
||||
**Failure means:** if the status stays stuck or nothing typed arrives, the renderer's socket did not retry
|
||||
itself — see `terminal.js`'s `connect()` and its backoff. If the pane came back empty with **no** banner, a
|
||||
session that survived a reload is being shown as though its scrollback had too, which is not true and is
|
||||
worse than saying nothing: the banner exists so this is never silently wrong. If typing does nothing but the
|
||||
banner is there, the session's credit window was not reset on reattach and the shell is frozen behind it —
|
||||
see `TerminalWorkspace.ReplayAfterAttachAsync`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 12 — Shared vaults: the operations that span two accounts
|
||||
|
||||
Reference in New Issue
Block a user