Skip to content

fix(ios): native host terminal server and PTY reconnect - #8

Merged
deadlyjack merged 3 commits into
Acode-Foundation:mainfrom
bajrangCoder:fix/ios-native-terminal
Sep 27, 2026
Merged

deadlyjack merged 3 commits into
Acode-Foundation:mainfrom
bajrangCoder:fix/ios-native-terminal

Conversation

@bajrangCoder

@bajrangCoder bajrangCoder commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

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 isAxsRunning still returned true.

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 on 127.0.0.1:8767) are unchanged.

Android:  xterm.js ──▶ AXS (native in proot) ──▶ bash                          (unchanged)
iOS:      xterm.js ──▶ TerminalServer (Swift) ──▶ guest /dev/pts ──▶ bash (only bash is emulated)

iOS terminal

  • Bridge/AlpineTerminal.c: a tty driver registered through the emulator's pty_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 and SIGWINCH.
  • TerminalServer / TerminalSession (Swift): implement the AXS routes (/terminals, /resize, /terminate, /status, /execute-command, WebSocket).
    • Output is batched like AXS (8 ms / 8 KB).
    • The last 256 KB of output is replayed when a client reattaches.
    • A program writing faster than the client reads is paused instead of growing memory.
  • LocalWebSocket: a minimal RFC 6455 server layer on top of the existing LocalHTTPServer / LocalHTTPConnection (new upgrade()).
  • The emulator's socket restart hooks (sockrestart) now run on suspend/resume, and the terminal listener restarts when the app returns to the foreground.
  • init-alpine.sh --prepare runs once per boot; each terminal then starts bash --rcfile /initrc -i directly. The axs binary stays in the guest for users and plugins.

Shared / Android

  • init-alpine.sh:
    • one apk info -e call instead of four on every start;
    • new --prepare flag;
    • the bash DEBUG-trap binary check (two subshells per command) now only runs where /sdcard or /storage exists.
  • 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.
    • Behaviour change: a socket error after connecting no longer shows the error alert immediately; the close handler decides whether to retry.

Other fixes

  • LocalHTTPServer: loopback listeners on a fixed port failed with EINVAL, because the port was passed both to on: and inside requiredLocalEndpoint. This also broke EmbeddedProxyServer.
  • spawnStream (language servers):
    • output goes straight from the pipe to the socket on its own queue, instead of through AlpineRuntime.queue;
    • stopped stream servers are removed instead of piling up.

Testing

On the iOS simulator (iPhone 17 Pro), all four Alpine suites pass when run together:

  • AlpineTerminalTests, extended to check:
    • resize reaches the PTY (stty size → 40 100);
    • output is replayed after reattaching;
    • the exit frame is sent;
    • terminate works.
  • AlpineInteractionTests, extended to drop a live tab's socket and check that it reattaches to the same shell (the exported variable is still set).
  • AlpineLspTests and AlpinePackageTests.
  • PreviewServerTests also pass.

Not yet verified:

  • Physical iPhone: memory limits, backgrounding, performance.

🤖 Generated with Claude Code

bajrangCoder and others added 2 commits September 27, 2026 22:41
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>
@github-actions github-actions Bot added the docs label Sep 27, 2026
…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>
@deadlyjack
deadlyjack merged commit c89f73d into Acode-Foundation:main Sep 27, 2026
3 checks passed
@bajrangCoder
bajrangCoder deleted the fix/ios-native-terminal branch September 28, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants