Files
terminalX/docs/HANDOFF.md
kid 866df1a323 feat: M3 mosh 挂起→恢复<3s(唤醒脉冲 + 会话状态机三相)
- SessionMachine 加 mosh 三相(moshActive/moshParked/moshResuming)+看门狗兜底,
  mosh 接管即拆 SSH、后台冻结不重建、前台唤醒脉冲等 SSP 续(+5 单测)
- bridge.go: Node.WakeUp()(tsnet InjectEvent+MagicSock.Rebind/ReSTUN) +
  MoshRelay.Rebind()(同端口重开 loopback、泵按 socket 生成容错)
- iosclient.cc: g_mosh_last_heard 探针(导出 mosh_last_heard_ms) + SIGCONT 全屏重绘
- MoshSession: nudge()(SIGCONT) + lastHeardMs();SSHTerminalModel 前台脉冲三连
  + healthy 轮询 + beginBackgroundTask;URL scheme 供无头前台唤醒
- 验证(vohive-vm over tsnet):15s 冻结→前台 wake pulse→1.3s recovered,<3s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-24 17:04:33 +08:00

92 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# terminalX 交接文档HANDOFF
> 目的:让**没有上下文的新会话**读完即可掌握项目全貌并继续工作。配合 `../CLAUDE.md`(操作规范/命令)与项目记忆(`~/.claude/projects/-Users-kid-...-terminalX/memory/`,本项目会话会自动注入)一起看。
> 最后更新2026-07-24。
---
## 0. 一句话现状
一个**原生 iPad SSH 终端**已可用并真机验证libghostty 渲染 + 键盘输入 + SSH 直连 + **SSH-over-tsnet(Tailscale 用户态)** + **tmux -CC 原生 tab** + **mosh(LAN + over-tsnet 均已端到端验证)** + 断线自动重连 + **mosh 挂起→恢复<3s(M3)**。四大核心需求libghostty/tailscale/tmux/**mosh 均已验证**)。仅在 iOS 模拟器验证(未上真机/未签名)。
> **mosh 状态2026-07-24 全部验证通过)**mosh(C++,blinksh/ios)+protobuf-lite 交叉编译成 `MoshCore.xcframework`CommonCrypto 后端,含 arm64 模拟器 slicetsnet 桥加 UDP relay(`StartMoshRelay`)`MoshSession`(pipe↔FILE*/pthread/SIGWINCH)+`MoshConnectScanner`(TXCore,39 单测)+`SSHTerminalModel` 编排SSH 引导 mosh-server→解析 MOSH CONNECT→接管
> - **LAN 直连端到端验证**192.168.9.199mosh-server 存活 104s≫60s 无客户端超时+子 shell+心跳=真连。
> - **mosh over tsnet 端到端验证**vohive-vm 100.69.201.101app 日志 `parsed MOSH CONNECT port=60002`→`relay up localPort=63542`→`activateMosh started`;屏幕渲染远端 shell 且无 mosh 断连 overlay=relay UDP 双向流通。**R1(tsnet UDP 数据报语义) 证伪**。
> - 复现 tsnet模拟器已持久化 tsnet 节点状态(`~/Library/.../tsnet-main`)`-txTsnetKey` 传任意非空串即可凭存量身份重连(节点未被 tailnet 清理时);节点被清理则需新 ephemeral key。
## 1. 项目愿景与硬约束
- 目标iPad/iPhone 远程轻量运维 macOS/Linux + vibe coding。平台优先级 **iPadOS > iOS > macOS(Apple Silicon)**
- iOS/iPadOS **只远程**(沙盒禁 fork/exec不能跑本地 shellmacOS 才支持本地 PTY。
- 分发目标 App StoreM5已暂缓
- 四大核心:**libghostty**(终端引擎) · **mosh** · **Tailscale tsnet(用户态,非 NetworkExtension)** · **tmux control-mode(-CC) hook**
## 2. 关键架构决策(已定,勿推翻)
1. 终端引擎用 **libghostty**,经社区 **GhosttyKit**`Lakr233/libghostty-spm`,已 vendor 到 `vendor/libghostty-spm`)。`TerminalViewState``ObservableObject`;喂字节入口 `InMemoryTerminalSession.receive(Data)`SwiftUI 视图 `TerminalSurfaceView(context:)`
2. Tailscale 走 **tsnet 用户态**Gogomobile bind → `TsnetBridge.xcframework`**不用 NetworkExtension**(避开 VPN entitlement + 50MB 内存墙)。
3. SSH 用 **libssh2 + mbedTLS**`CSSHCore.xcframework`),支持传入外部 fd。
4. **SSH-over-tsnet = fd 桥**`tsnet.Dial()` 得 net.Conn无真实 fd`socketpair(AF_UNIX)` + 双向 io.Copy 泵 → 把另一端 fd 交给 libssh2。见 `vendor/tsnet-bridge/tsnetbridge/bridge.go: DialTCPFD`
5. 一进程只一个 Go runtime、只一次 gomobile bindC/C++ 库符号隐藏只导出 C API。
6. 多账号(未实现):多 tsnet 节点并存;每 host 绑 egress profile(Direct/tailnet)**禁止多 tailnet 并发盲探**(重叠 100.x/同名会误连)。
## 3. 环境(决定构建方式,极重要)
- **带认证的 HTTPS 代理**`HTTPS_PROXY=...@...:38128`)。`github.com` release blob 能 curl**go.dev / ghcr.io 被代理 abort****SwiftPM 不支持带认证代理** → 所有 SPM 依赖 vendor 本地化。Go 模块用 `goproxy.cn`
- **Go 1.26.5 在 `~/.local/go`**(不在默认 PATH经 aliyun 镜像 curl 装),**gomobile/gobind 在 `~/go/bin`**`go env -w GOPROXY=https://goproxy.cn GOSUMDB=off` 已固化。
- 工具链Xcode 26.6 / Swift 6.3 / xcodegen 2.46 / zig 0.15.2 / cmake 4.4 / ninja 1.13 / git。
## 4. 里程碑状态
| 里程碑 | 状态 | 真机实证 |
|---|---|---|
| **M0** SSH 直连终端 | ✅ 完成 | libghostty 渲染(SGR/CJK)、键盘输入、连接;**白屏 bug 修复**(OutputGate);会话状态机+自动重连(31→含后加共 35 单测) |
| **M1** tsnet + SSH-over-tsnet | ✅ 完成 | app 内 tsnet 加入 tailnet(IP 100.74.60.108);对端枚举;**SSH-over-tsnet 登录 Ubuntu shell**(服务端 `Last login from 100.x` 证明走 tailnet |
| **M1.5** tmux -CC 原生 tab | ✅ MVP | 直连跑 `tmux -CC attach` → 渲染 fish/tmux 两原生 tab + 活动窗口内容 |
| dial 重试打磨 | ✅ | `TsnetManager.dialFD` 6 次退避重试,解决 tsnet 首连路径预热超时 |
| **R5** Go+gvisor+C+++Swift 同进程 | ✅ retire | 全程真机无崩溃 |
| **M2** mosh | ✅ 完成LAN + tsnet 均验证) | libmoshios(C++)+protobuf-lite→`MoshCore.xcframework`UDP relay over tsnet**LAN 直连 + mosh-over-tsnet 均端到端验证通过**R1 证伪) |
| **M3** 生命周期深化 | ✅ mosh 快恢复(真机深挂起待补) | scenePhase FSM + **mosh 挂起→恢复<3s 已验证**模拟器15/30s 冻结→前台wake pulse→1.3s 收到服务器包,"已恢复");冷启动重连(SSP 序号+key 持久化)划入 M4/M5 |
| **M4** 安全 | ◻️ 未做 | **当前 SSH 接受任意 host key**(见 SSHSession TODO)known_hosts 固定 + Secure Enclave 密钥 待做 |
| **M5** 合规提审 | ⏸️ 暂缓 | 用户指示暂缓 |
## 5. 代码地图与数据流
- `packages/TXCore/Sources/TXCore/`
- `Tmux/TmuxControlParser.swift` `TmuxEvent.swift` `TmuxIDs.swift` `TmuxOutputDecoder.swift` `TmuxLayout.swift` `TmuxControlSequence.swift`DCS 检测)—— 纯解析35 单测在 `Tests/`
- `Session/SessionMachine.swift` —— `reduce(event)->[Effect]` 会话状态机(指数退避重连/后台冻结/前台恢复)。**M3 加 mosh 三相** `moshActive/moshParked/moshResuming` + 事件 `moshEstablished/moshHealthy/moshExited/resumeWatchdogFired` + 效果 `nudgeResume/scheduleResumeWatchdog/teardownMosh`mosh 接管即拆 SSH、后台冻结不重建、前台打唤醒脉冲等 SSP 续、两次脉冲无效兜底全量重建。
- `packages/TXTransport/Sources/`
- `TXTransport/Transport.swift`(协议+SSHConfig+SSHError`SSHSession.swift`libssh2`preconnectedFD` 支持外部 fd事件循环 poll+非阻塞)。
- `TXTransport/MoshSession.swift`M2驱动 `mosh_main`pipe↔FILE* 桥in/out、Foundation.Thread 跑阻塞主循环+`pthread_self()``pthread_kill(SIGWINCH)` resize、忽略 SIGPIPE、setenv UTF-8 locale。
- `CSSH/`C targetlibssh2 头 + modulemap`import CSSH`)、`CSSHCore`binaryTargetlibrary-only
- `CMosh/`C target`moshiosbridge.h`(mosh_main)+modulemap+占位 shim.c`import CMosh`)、`MoshCore`binaryTargetlibrary-onlyTXTransport 链 `libc++`/`libz`mosh 是 C++ 且 compressor 用 zlibCommonCrypto 属 libSystem 自动解析)。
- `TXCore/Mosh/MoshConnect.swift``MoshConnectScanner` 流式扫描 `MOSH CONNECT <port> <key>`(注意 Swift 视 `\r\n` 为单 Character行尾判定须用 `isNewline`)。
- **M3 快恢复链**`bridge.go``Node.WakeUp()`(tsnet `InjectEvent`+`MagicSock.Rebind/ReSTUN`) 与 `MoshRelay.Rebind()`(同端口重开 loopback、泵按 socket 生成容错)`iosclient.cc``g_mosh_last_heard`(收到服务器包即更新,导出 `mosh_last_heard_ms()`) 并恢复 SIGCONT 全屏重绘;`MoshSession.nudge()`(SIGCONT) + `lastHeardMs()``SSHTerminalModel` 前台执行"唤醒脉冲三连"(WakeUp+Rebind+nudge)+healthy 轮询(探针增长→`moshHealthy`)+恢复看门狗+`beginBackgroundTask`
- `apps/TerminalX/iOS/`
- `SSHTerminalModel.swift` —— 核心编排:`OutputGate`(surface-ready 缓冲)、`TransportHolder``TsnetManager`(tsnet 节点 up/dial+重试)、`TsnetError``TmuxRouter`(raw/gateway 字节分流)、状态机驱动 startSession(直连/tsnet 分支)、`autoConnectIfConfigured`(启动参数)。
- `ContentView.swift` —— 分支:`tmuxController != nil``TmuxTabbedView`(tab 条+活动窗口);否则 `RawTerminalView``TsnetProbeView`(纯自检)`ConnectionForm`
- `TmuxController.swift` —— tmux 网关消费解析事件、window↔`TmuxWindow`(各自终端会话+OutputGate)、`list-windows`/`list-panes`/`capture-pane` 握手、`send-keys`/`select-window`/`new-window`
- `TsnetProbe.swift` —— M1 自检视图(加入 tailnet + 对端列表)。
- `TerminalXApp.swift` —— @main
- **数据流(直连)**transport.onBytes → `TmuxRouter.feed` → (raw) `OutputGate.deliver``session.receive`→libghostty(检测到 DCS) → `TmuxController.feed`→解析→各 window。输入libghostty→session write 闭包→transport.sendtmux 模式→send-keys
- **数据流(tsnet)**`TsnetManager.ensureUp`(auth key)→`dialFD`(socketpair fd)→`SSHSession(preconnectedFD:)`
## 6. 已知问题 / TODO继续工作的入口
- **M4 安全**`SSHSession.connect()``TODO(M4)`:不校验 host key接受任意。需接 known_hosts 三元组 pin(egress,host,port) + Secure Enclave 私钥(`libssh2_userauth_publickey_frommemory`+`SecKeyCreateSignature`)。
- **tmux MVP 简化**:每 window 只显示活动 pane**多 pane 分屏未做**);历史靠 `capture-pane` 抓当前屏(非完整 scrollback命令应答 FIFO 简化匹配resize 未回传(默认 80x24应发 `refresh-client -C`)。
- **tsnet 首连**:已加 dial 重试;更优是等 peer online 再 dial读 PeersJSON online 状态)。
- **签名/真机**`CODE_SIGNING_ALLOWED=NO`,仅模拟器;上真机/TestFlight 需配置签名 + entitlementstsnet 用户态**无需** NE entitlement利好审核
- **git**:已 `git init`**尚无 commit**`artifacts/*.xcframework` 已 gitignore`make`/脚本重建或从 Release 拉)。
- **多平台**iOS target 已跑macOS target本地 PTY, `TXPTY`)未建。
## 7. 下一步建议(按价值)
1. **M2 mosh 打磨**(功能已通,打磨项):`activateMosh` 写死 80x24 初值(靠首次 resize 纠正SSH 会话在 mosh 接管后保持 idle 未关(占一条连接);`MoshSession.close()` 后 mosh 线程可能滞留(阻塞主循环,见 TODOmoshiosbridge.cc 顶部 `fwrite("Hello from the Bridge!")` 调试行可清理;`SSHTerminalModel``NSLog("MOSHDBG …")` 诊断日志可按需保留/删除mosh-server 引导后远端会累积 detached 会话(`mosh-server new` 每次新建),可考虑复用或清理。**挂起→恢复<3s M3待专门测**。
3. **M3 真机深挂起补测**模拟器不复现 socket defunct且看门狗会杀掉被 `kill -STOP` 冻结过久的 app>~30s 概率被杀),故 WG 密钥过期(>180s)/DERP 死链/socket defunct 路径需**真机锁屏数分钟**用例补测(`MoshRelay.Rebind`/`hop_port` 的正确性靠代码 + 真机)。
4. **tmux 多 pane 分屏**`TmuxLayout` 已能解析布局树 → 把 window 内多 pane 渲染成 SwiftUI 分屏(每 pane 一 surface
5. **M4 安全 + 冷启动 mosh 重连**known_hosts 固定 + SE 密钥mosh 进程死后重连原 detached 会话需持久化 MOSH_KEY+port 且改 mosh 序列化 SSP 序号/终端状态Blink 式,大活)。
5. **产品化**:主机列表持久化(Keychain)、多标签(非 tmux)、软键盘运维工具栏(GhosttyKit 内建,接线即可)。
## 8. 测试资源(用户提供,**凭据勿写入仓库/勿硬编码**,每次向用户索取)
- 一台局域网 macOS(有 tmux + **mosh-server**,如 MacBook-Air-M1fish 为默认 shell`mosh-server``/opt/homebrew/bin` 且交互 shell PATH 可见)、一台 tailnet 内 online 的 Ubuntu VM(vohive-vm)、一个 tailnet auth keyephemeral可能过期用户会给新的。用于直连 SSH / SSH-over-tsnet / tmux / M2 mosh 端到端验证。凭据每次向用户即时索取。
- tailnet 是 `tail3b5057.ts.net`已见节点vohive-vm(linux)、iphone-air、ipad-pro、macbook-air-m1、macbook-air。
## 9. 工作纪律(务必遵守)
- **绝不臆想工具结果**:调用后等真实返回再推理,绝不编造 BUILD/日志/截图。曾因连续编造工具输出(含假 BUILD SUCCEEDED、错误归因 GhosttyKit而误诊——真相全靠老实 `Read` 完整 log。此规则已写入全局 `~/.claude/CLAUDE.md`
- 每步用真实证据验证:`swift test` 看真实计数、`xcodebuild` 看真实 BUILD 结果、模拟器 `screenshot` + `Read` 看真实画面。
- 与用户用**简体中文**commit/push 只在明确要求时。
</content>