fix(ios): native host terminal server and PTY reconnect - #8
Merged
deadlyjack merged 3 commits intoSep 27, 2026
Merged
Conversation
On iOS, AXS ran inside the Linux emulator, so every keystroke went through emulated sockets, epoll, tokio threads and a PTY. Terminals broke after the app was backgrounded because the emulator's socket restart hooks were never called. - Add a tty driver (Bridge/AlpineTerminal.c) that connects guest /dev/pts slaves directly to Swift via pty_open_fake, as iSH does. Only bash runs emulated; job control, termios and SIGWINCH keep working. - Serve the AXS HTTP/WebSocket API (/terminals, resize, terminate, status, execute-command) from Swift on 127.0.0.1:8767 so the JS terminal and plugins are unchanged. Output is coalesced like AXS, 256 KB scrollback is replayed on reattach, and writers are paused when the client falls behind. - Call sockrestart on suspend/resume and restart the listener on foreground. - Run init-alpine.sh --prepare once per boot instead of per AXS start, check required packages with one apk call, and skip the Android-only storage check in bash that forked two subshells per command. - Stream language server output directly from the pipe on its own queue instead of through AlpineRuntime.queue. - Fix LocalHTTPServer loopback listeners failing with EINVAL, which also affected EmbeddedProxyServer. - Remove stopped spawnStream servers instead of keeping them forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dropped WebSocket (e.g. after the app is suspended) closed the terminal tab even though the shell was still running. Retry the same session with backoff; the server replays recent output, so the screen is reset first. The tab only closes once the session is really gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…command - Park congested guest writers in C with the emulator's wait_for instead of an NSCondition in Swift. Pending guest signals (Ctrl-C, SIGKILL from unmount) now interrupt the wait with EINTR before any data is accepted, and closing a terminal releases waiting writers. - Keep the tab when reconnect attempts run out: mark it disconnected and retry on the next keypress or app resume. Only a process exit or an explicit close ends the session. - Run /execute-command on an 80x24 PTY like AXS, answer when the shell exits, return 400 for a missing cwd, and apply the 30 s deadline until the response is sent, terminating the session's process group on timeout. - Add AlpineTerminalServerTests (congested writer vs Ctrl-C, execute-command PTY and deadline) and extend the interaction test for kept tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Why
On iOS the terminal was unstable: it broke after backgrounding, started slowly and sometimes failed to connect. The Android design had been ported as is. On Android, AXS runs natively under proot. On iOS it ran inside the Linux emulator, so every keystroke went through emulated sockets, epoll, tokio threads and an emulated PTY. The emulator's socket restart hooks were also never called. iOS reclaims listening sockets of suspended apps, so AXS kept running while its port was dead, and
isAxsRunningstill returnedtrue.What changed
Each platform has its own native implementation behind the same API. JS and plugin APIs (
Terminal,Executor, and the AXS HTTP/WebSocket protocol on127.0.0.1:8767) are unchanged.iOS terminal
Bridge/AlpineTerminal.c: a tty driver registered through the emulator'spty_open_fake(the approach iSH uses). Guest output goes straight to Swift. Input, resize and hangup go straight to the guest tty. Bash still gets job control, termios andSIGWINCH.TerminalServer/TerminalSession(Swift): implement the AXS routes (/terminals,/resize,/terminate,/status,/execute-command, WebSocket).LocalWebSocket: a minimal RFC 6455 server layer on top of the existingLocalHTTPServer/LocalHTTPConnection(newupgrade()).sockrestart) now run on suspend/resume, and the terminal listener restarts when the app returns to the foreground.init-alpine.sh --prepareruns once per boot; each terminal then startsbash --rcfile /initrc -idirectly. Theaxsbinary stays in the guest for users and plugins.Shared / Android
init-alpine.sh:apk info -ecall instead of four on every start;--prepareflag;/sdcardor/storageexists.terminal.js: on an unexpected socket drop, the terminal reattaches to the same PTY with backoff (0 / 0.5 / 1.5 / 3 s) instead of closing the tab. The tab only closes when the session is really gone.Other fixes
LocalHTTPServer: loopback listeners on a fixed port failed withEINVAL, because the port was passed both toon:and insiderequiredLocalEndpoint. This also brokeEmbeddedProxyServer.spawnStream(language servers):AlpineRuntime.queue;Testing
On the iOS simulator (iPhone 17 Pro), all four Alpine suites pass when run together:
AlpineTerminalTests, extended to check:stty size→40 100);AlpineInteractionTests, extended to drop a live tab's socket and check that it reattaches to the same shell (the exported variable is still set).AlpineLspTestsandAlpinePackageTests.PreviewServerTestsalso pass.Not yet verified:
🤖 Generated with Claude Code