# 架构与线程模型 > **English summary:** module layout of the KMP codebase, the expect/actual seams, > the per-platform transport table, and the threading model that keeps the > terminal buffer safe: SSH output is queued and consumed serially on the main > thread (bounded queue + backpressure); the Mosh shadow terminal is owned by a > session coroutine and synced to the UI buffer via a conflated channel. ## 模块布局 ``` composeApp/ ├── commonMain/ # 全平台共享(核心资产) │ ├── term/ # 纯 Kotlin 终端模拟器(零平台依赖,见 terminal-emulator.md) │ ├── mosh/ # 纯 Kotlin mosh 客户端(SSP 协议,见 mosh.md) │ ├── ssh/ # 平台无关会话抽象:SshSession / SftpSession / MoshSession │ │ # + expect 工厂 createSshSession / createSftpSession │ ├── crypto/ # 纯 Kotlin 密码原语(未审计——见其 README;生产只用 Sha256) │ ├── data/ # Host / AppSettings @Serializable 模型、HostRepository │ │ # (multiplatform-settings + JSON)、SecretStore expect │ ├── herdr/ # herdr 控制面域:HerdrProbe(探测)/ HerdrApi(命令构造)/ │ │ # HerdrMonitor(轮询)/ HerdrModels(快照模型) │ ├── ui/ # AppRoot 导航、SessionManager、TerminalScreen/View、 │ │ # KeyToolbar、SFTP、Host/Connections/Settings 页、theme/ │ │ # TerminalController(会话状态/UI 观察点)+ SessionConnector │ │ # (连接编排:三模式建连/重连/herdr 引导降级——状态所有权在 │ │ # controller,connector 经同包 internal 读写) │ └── util/ # SessionKeepAlive / NetworkChange / Dispatchers / │ # MonoFont / AppLifecycle 等 expect 接缝 ├── androidMain/ # MainActivity + SessionService(前台服务保活)+ Keystore │ ├── ssh/SshSessionSshj # sshj + BouncyCastle(ed25519 / chacha20 / RSA / kb-int) │ └── mosh/MoshPlatform.android # UDP 套接字 + java.util.zip 实现 ├── iosMain/ # MainViewController + libssh2 引擎 + Keychain + POSIX UDP ├── commonTest/ # crypto RFC 向量 + 终端模拟器 + mosh 单元测试 └── androidUnitTest/ # JVM 本地测试:会话/仓库/sshj/SFTP(真实 sshd,127.0.0.1:22222) ``` Android 本地测试通过 `:composeApp:testDebugUnitTest` 运行,也包含 `commonTest`。 会话与控制器测试使用 Robolectric 提供 Android 环境;仓库测试使用内存 `MapSettings`。 ## 平台分工 | 平台 | SSH 引擎 | 认证 | Mosh 客户端 | 密钥存储 | |------|----------|------|-------------|----------| | Android | sshj + BouncyCastle | password / publickey / keyboard-interactive | 纯 Kotlin `dev.termish.mosh`(UDP 直连) | Android Keystore(AES-GCM) | | iOS | libssh2 + OpenSSL(静态链接) | 同上 | 纯 Kotlin `dev.termish.mosh` | Keychain | ## expect/actual 接缝 平台代码只允许出现在这些接缝之后: | 接缝 | commonMain | androidMain | iosMain | |------|-----------|-------------|---------| | `ssh.createSshSession` | expect | sshj 引擎 | libssh2 引擎 | | `ssh.createSftpSession` | expect | SftpSessionSshj | SftpSessionLibssh2 | | `data.SecretStore` | expect | Keystore AES-GCM | Keychain | | `util.SessionKeepAlive` | expect | 前台服务 + wakelock | no-op | | `util.NetworkChange` | expect | ConnectivityManager | no-op | | `util.Dispatchers` | expect | IO/Default | 自定义 IO 队列 | | `util.MonoFont` | expect | 捆绑 JetBrains Mono | PingFang SC | | `mosh.MoshUdpSocket` / zlib | expect | JVM socket + java.util.zip | POSIX socket + 系统 zlib | | `ui.FilePicker/FileSaver/DirectorySaver` | expect | SAF/DocumentFile | UIDocumentPicker | ## 线程模型(2026-08 重构后的约定) 约束一句话:**`TerminalBuffer` 的全部读写(渲染 / resize / 选择 / 模拟器写入)串行在主线程**。 ### SSH 输出路径 ``` sshj/libssh2 reader 协程(IO 线程) └─ onOutput/onStderr(挂起回调) └─ TerminalController.enqueueOutput └─ Channel(256) ← 有界队列 └─ 主线程消费协程 ├─ emulator.write(chunk) ← buffer 唯一写入口 ├─ 8ms 帧预算内批量抽干,合并重绘 └─ frame++(Compose 重绘信号) ``` - **背压**:队列满时 `send` 挂起 → reader 停止读 socket → TCP 窗口收敛。不丢字节也不撑爆内存(`cat` 大文件场景)。 - **串行化**:sshj 的 stdout/stderr 两个 reader 用 `limitedParallelism(1)` 共享调度器,转义序列不会被并发打断。 - 消费循环 `receiveCatching` + `tryReceive` + 预算到点 `yield()`,小包洪泛时每包一次重绘的抖动被消除。 ### Mosh 输出路径 ``` KmpMoshSession 事件循环协程(Default 线程) ├─ 影子终端(ShadowTerminal.buffer,Mutex 保护) └─ onStateUpdate → Channel(CONFLATED) └─ 主线程消费:uiBuffer.copyContentFrom(view.buffer) └─ 行级 (identity, version) 增量比对,无视觉变化不重绘 ``` - CONFLATED 只保留最新影子状态,突发更新最多一个主线程拷贝在途。 - 拷贝持影子锁:会话协程 applyDiff/resize 中途不能读,否则 resize 半程行状态不一致会越界。 ### 写路径 - sshj:`sendData` 经 `limitedParallelism(1)` 写调度器(避开主线程 socket 写 + 保证输入顺序)。 - mosh:`sendData` 投递到事件 Channel,由会话协程统一 push 进 `UserStream`。 - 认证/主机密钥弹窗:`onPrompt` 挂起 120s 超时按取消处理;`verifyHostKey` 同步阻塞 120s 超时按拒绝——防止连接线程永久悬挂。 ### 控制器生命周期 - `TerminalController` 持 `ioDispatcher + SupervisorJob` 作用域;`close()` 保留重入能力(可 reconnect),`destroy()` 关闭队列并取消作用域(回收重连退避/主题注入/稳定期重置等延迟任务)。 - 自动重连任务统一挂在 `reconnectJob`,close 时取消,防关闭后仍被延迟协程拉起。 ## 会话保活与网络事件 - Android:`SessionService` 使用 `connectedDevice` 前台服务 + PARTIAL_WAKE_LOCK;按 `sessionId` 幂等登记活跃会话,最后一个会话关闭后停止。回前台先做 SSH 健康检查,失效时才重连。 - iOS:后台挂起即断线,回前台 `SessionManager.reconnectDroppedSessions()` 自动重连(缓冲保留)。 - 网络切换:SSH 在「新网络就绪」事件下先做健康检查,仅失效时快速重连(30s 免疫期 + 15s 防抖 + 单调钟);mosh 不重建,靠 UDP 漫游自愈。 ## 持久化 - 非秘密数据(主机列表/设置/最近会话)→ `multiplatform-settings`(JSON,`ignoreUnknownKeys`)。 - 秘密数据(密码/私钥)→ `SecretStore`(Keystore/Keychain)。 - 解析失败不丢数据:原始串备份到 `*.corrupt.` key(最多 3 份)。