第 17 章:Remote Development——本地绘制,远端拥有工程资源
Remote Zed 不是远程桌面,也不是把仓库全量同步到本地。UI/GPUI 留在本机,Project 的文件、LSP、 Git、terminal 等资源放在 remote server,通过同一套 store/proto 边界重新连接。
1. 进程如何切开
flowchart LR
subgraph Local["本机 zed"]
GPUI["GPUI / Window"]
W["Workspace / Editor"]
RP["Remote Project proxies"]
RC["RemoteClient"]
GPUI --> W --> RP --> RC
end
subgraph Transport["SSH / WSL / Docker channel"]
MUX["multiplexed proto + heartbeat"]
end
subgraph Remote["远端 zed remote_server"]
PS["Project stores"]
FS["Worktree / filesystem"]
LS["LSP / DAP"]
GT["Git / terminal / tasks"]
PS --> FS
PS --> LS
PS --> GT
end
RC --> MUX --> PS鼠标、键盘、布局、文本渲染仍在本地,因此交互无需传像素;依赖 repo 的昂贵工作靠近数据执行,因此 不需要先下载整个 worktree。
2. RemoteClient 是长期会话
crates/remote/src/remote_client.rs 的 RemoteClient 持有:
- ChannelClient/ProtoClient;
- 唯一 connection id 与 connection options;
- remote platform、path style;
- project/connection state;
- heartbeat 与 reconnect task;
- 启动过的 setup/workspace server 信息。
它不是一次 SSH command wrapper,而是从启动 remote server 到项目关闭的长期对象。
3. 两层连接状态
公开 ConnectionState 面向 UI:Connecting、Connected、HeartbeatMissed、Reconnecting、Disconnected。 内部 State 还区分 ReconnectFailed、ReconnectExhausted、ServerNotRunning 等恢复细节。
显式状态让调用者知道:
- HeartbeatMissed 时旧数据仍可显示,但新请求风险升高;
- Reconnecting 是暂时态,不应立即关闭所有 editor;
- ReconnectExhausted 才需要用户采取动作;
- 主动 shutdown 与网络故障不是同一个错误。
源码入口:crates/remote/src/remote_client.rs:307-380。
4. 多种 transport,共同的 RemoteConnection
RemoteConnectionOptions 支持 SSH、WSL、Docker 和 Mock。每种 backend 处理自己的连接与进程启动:
- SSH:认证、host/port、shell 与远端命令;
- WSL:选择 distribution,通过 Windows/WSL 边界启动;
- Docker:container/exec 生命周期;
- Mock:测试断线、重连与协议行为。
后续 Project 不应出现 if ssh/if wsl。transport 都转换为同一个双向 async channel 和 platform/path metadata。
5. Remote server 获取流程
连接建立后,本地先探测远端 OS、architecture 与当前 remote server 状态。若不存在兼容 binary,则下载/ 上传正确构建,校验并启动 headless remote_server,最后握手协议版本。
connect transport
→ detect platform + arch + shell
→ locate compatible remote_server
→ acquire/upload if missing
→ launch setup server
→ negotiate protocol
→ launch/attach workspace server
→ open Projectbinary 版本必须与本地协议兼容;仅判断“文件存在”会让应用升级后连接到旧 server 并产生难以解释的解码 错误。
6. Setup 与 Workspace connection identity
ConnectionIdentifier 区分 Setup 和 Workspace。Setup 负责准备环境;Workspace 对应真正的 Project 会话。 重连时保持同一 workspace/socket identity,才能重新附着到原 server 进程和已打开资源,而不是悄悄创建第二个 LSP、terminal 和 worktree scanner。
源码:crates/remote/src/remote_client.rs:345-350。
7. Project stores 在 local/remote 何处分叉
UI 仍调用 Project 的统一 API;分叉集中在各 store:
| 能力 | Local store | Remote store |
|---|---|---|
| open/read/save Buffer | 本地 FS + Buffer | proto request + 同步 Buffer |
| Worktree scan | 本地 scanner | 远端 scanner,增量 entries 回传 |
| LSP | 本机 child process | 远端 server 进程与 LspStore |
| Git | 本机 Repository | 远端 GitStore |
| terminal/task | 本机 PTY/process | 远端 PTY/process,流式输出 |
| search | 本机遍历文件 | 远端 candidate/内容扫描 |
结果回到本机后都转换为同样的 Buffer、ProjectPath、diagnostic、search result 等领域对象,Editor 无需维护 两套 UI。
8. 为什么 Buffer 仍在本地有副本
每次绘制、光标移动和 selection 都依赖文本快照,不能为读取一行去远端 round trip。打开文件后本地建立 Buffer 副本,远端发送初始化内容和后续 operation;本地编辑立即应用,再发往 remote host。
这与协作复用 Buffer operation 基础,但参与者不同:remote 通常是同一用户的 UI 与工程 host,协作是多个 有角色的用户共享项目。
9. Heartbeat 不只是“网络还通”
TCP/SSH channel 可能半开:本地 socket 未报错,但远端进程已冻结或中间设备丢包。应用级 heartbeat 记录 最近响应时间,超过阈值进入 HeartbeatMissed 并触发恢复判断。
heartbeat payload/ack 应走与业务消息相同的复用通道,才能验证整个读写 loop,而不只是另开一个健康端口。
10. 重连的层次
sequenceDiagram
participant UI as Workspace
participant RC as RemoteClient
participant T as Transport
participant RS as Remote Server
UI->>RC: 普通 project request
RC--xUI: connection lost
RC->>UI: ConnectionState::Reconnecting
RC->>T: 重新建立 SSH/WSL/Docker channel
T->>RS: attach same workspace identity
RS-->>RC: handshake + session state
RC->>RS: resubscribe entities / synchronize buffers
RS-->>RC: missed deltas / snapshots
RC->>UI: Connected,恢复 requests重连不是简单替换 socket。必须依次恢复 transport、协议会话、workspace identity、entity subscription、Buffer 版本和 streaming services。
11. Request 的可重试性
断线发生时有三种情况:未发送、远端未执行、远端已执行但 response 丢失。只读 open/status 通常可重试; 创建 terminal、运行 task、写文件等副作用请求需要 operation id 或显式确认,不能盲目重放。
协议设计应给命令分类:
- idempotent query;
- 具有稳定 request/operation id 的可去重 mutation;
- 必须由上层重新决策的 non-idempotent action。
12. PathStyle 是领域数据
本机可能是 macOS,远端可能是 Windows/WSL。path separator、case sensitivity、可执行文件后缀和 shell quoting 不能用本机 std::path 规则解释。RemoteClient 在握手后保存 remote platform/path style;ProjectPath 通过 worktree id + 相对 path 跨协议传输。
UI 展示可使用友好格式,但查找、比较和命令参数必须遵守 remote 语义。
13. Remote 与 Collaboration 的关系
| 维度 | Remote Development | Collaboration |
|---|---|---|
| 目的 | 操作另一台机器上的工程 | 多人共享同一项目 |
| 信任 | 通常同一用户控制 host | 多用户、role/capability |
| 连接 | SSH/WSL/Docker 直连 | cloud/collab WebSocket |
| 权威 | remote server | project host + collab server 权限 |
| 文本 | 本地副本与 host 同步 | 多客户端 CRDT 收敛 |
| 资源 | 全部工程服务在远端 | host 执行工程服务 |
二者复用 Project store、proto 与 Buffer operation 思想,但不能混成一个状态机:认证、路由、权限和重连 语义不同。
14. 关闭与孤儿进程
正常关闭需停止 heartbeat/reconnect task、关闭 project channel,并请求 remote server 清理对应 workspace process;异常断线则可能保留 server 以便短时间重附着。过早 kill 会破坏重连,永不回收会留下 LSP/terminal 孤儿进程。
因此 server 端需要 session ownership、租约/超时和显式 shutdown 的组合,而不是把 child process 生命周期 绑死在单个 TCP socket。
15. 测试最重要的不是 happy path
Mock remote backend 应覆盖:
- handshake 前/后断线;
- heartbeat 丢失但 channel 未关闭;
- request 已执行、response 丢失;
- 重连时 Buffer 又发生编辑;
- server binary 版本不匹配;
- remote path style 与本机不同;
- 同一 workspace 被重复 attach;
- shutdown 与自动 reconnect 竞争。
16. 可迁移经验
- UI 留近用户,工程服务留近数据;
- local/remote 分叉收敛在领域 store,不扩散到视图;
- transport backend 统一为稳定 channel abstraction;
- 协议版本、平台和 path style 在握手时显式协商;
- heartbeat 检测半开连接,重连恢复完整会话而非仅 socket;
- 副作用请求明确去重/重试语义;
- server process 生命周期独立于瞬时网络连接。