Repository navigation
fix: a terminal pane reconnects when you touch it, and says why it dropped - #71
Merged
Merged
Conversation
…opped
Reported: the terminal freezes on paste or while scrolling, shows "Terminal is
not connected", and stays dead until the browser window is reloaded.
Three client-side causes, all of them "the code gives up instead of
reconnecting":
- term.onData guarded on readyState === 1 and nothing else, so a keystroke into
a dead socket vanished silently. xterm renders locally, so the pane still
looks alive — that is the freeze.
- pasteToTerminal and sendToTerminal toasted term.paste.notconnected and
returned, naming the problem on the two controls that exist BECAUSE a pane has
gone unresponsive.
- Every automatic reconnect path is conditional on a state a half-dead socket
does not report: onclose only runs if a close event arrives, visibilitychange
only on a tab switch, and the backoff can be 30s out.
_reviveTerminal() makes any interaction a reconnect trigger and carries the
frame through. Only the LAST frame is queued (stale keystrokes replayed into a
TUI are worse than a dropped one), a CONNECTING socket is not raced with a
second one, and _pending is cleared before the send so a failure cannot replay
it on the next open.
Diagnosis, which did not exist: ws.onclose took no argument and there was no
onerror on the terminal socket at all, so a failure left no trace on either end.
Both exist now; the log sits after the staleness guard so a superseded socket
closing does not bury the real ones.
NOT fixed, and stated as such in the CHANGELOG: what drops the socket in the
first place. The close code added here is what will name it. An earlier
hypothesis — a missing ws.on('error') server-side — was checked and is wrong;
it is there, at the top of the wssTerm connection block.
test/composer-terminal.test.js gains 13 checks and had to be repinned twice: it
caught the staleness guard being displaced and the send-line behaviour changing,
which is exactly what it was written for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The report
What was wrong
Three separate places where the client gives up instead of reconnecting. None of them is the thing that drops the socket — they are what turns a dropped socket into a pane that stays dead until F5.
1. Typing into a dead pane was a silent drain.
No
else. The keystroke vanished — no reconnect, no message, nothing in the console. xterm renders locally, so the pane still looks alive. That is the freeze in the report.2. Paste and the send line named the problem and did nothing about it.
Both did
toast(t('term.paste.notconnected'), true); return;— on the two controls that exist precisely because a pane has gone unresponsive. That toast is the "Terminal is not connected" the reporter sees.3. Every automatic reconnect path is conditional on a state a half-dead socket does not report.
oncloseonly runs if a close event ever arrives;visibilitychangeonly fires on a tab switch; the backoff can be 30 s out; andentry.exitedlatches the retry off for good. The only unconditional path was the 🔄 button.What this changes
_reviveTerminal(sessionId, frame)makes any interaction a reconnect trigger and carries the frame through, so the first thing you type is not the thing that gets lost.CONNECTINGis not raced with a second one; it delivers the queued frame from its ownonopen._pendingis cleared before the send, so a failing send cannot replay it on the next open too.exitedlatch is cleared, for the same reasonrefreshTerminalSession()clears it: the user asking outranks the latch.Diagnosis that did not exist
ws.onclosetook no argument — the close code was discarded — and the terminal socket had noonerrorat all (the chat socket has had one since it was written). A terminal failure therefore left no trace on either end, which is why "it freezes sometimes" was never explained. Both exist now. The log sits after the staleness guard, because a superseded socket closing is routine and would bury the closes that mean something.Next occurrence will say which it was:
1006abnormal close with no FIN,1009frame overmaxPayload,1011server error.What this does NOT fix
What drops the socket in the first place is still unidentified. This PR fixes what the UI does about it. The close code added here is what will name the cause.
One hypothesis was checked and is wrong: a missing server-side
ws.on('error')on the terminal socket. It is there, at the top of thewssTermconnection block — it just sits above both sub-blocks, which is why it does not appear near either handler.Verification
npm test→ exit 0.test/composer-terminal.test.jsgains 13 checks and had to be repinned twice: it caught the staleness guard being displaced by a log line, and the send-line behaviour changing. That is exactly what it was written for, and both are now pinned in their new form — including the negative pin that the dead-socket refusal is gone.