Skip to content

fix(transport,pty): gRPC keepalive on both ends; reset local terminal on session end - #12

Merged
flyingEirc merged 1 commit into
masterfrom
fix/grpc-keepalive-terminal-reset
Jul 8, 2026
Merged

fix(transport,pty): gRPC keepalive on both ends; reset local terminal on session end#12
flyingEirc merged 1 commit into
masterfrom
fix/grpc-keepalive-terminal-reset

Conversation

@flyingEirc

Copy link
Copy Markdown
Owner

📌 Description

修复 PTY 长连接空闲断连及其两个连锁故障(2026-07-08 实测复现):

  • What problem does it solve?
    1. PTY gRPC 连接空闲 40~60 分钟后被 NAT/防火墙等中间设备回收,客户端报
      error reading from server: EOF,supervisor 联动停掉 daemon;
    2. 服务端全程无感知:attach handler 阻塞在 Recv() 永不返回,
      UnregisterPTY 不执行,activePTY 泄漏——重连一直报
      pty session already attached,只能重启 server;
    3. 断开后远端程序透传的 kitty keyboard protocol 残留在本地终端,
      每次按键回显 9;1:3u 之类的 release 事件残片。
  • What feature is added/changed?
    • pkg/transport/client.go:客户端 gRPC keepalive(Time=30s / Timeout=10s /
      PermitWithoutStream)。daemon 与 pty 共用该拨号入口,两条连接同时覆盖;
      空闲 PING 保活的正是被掐断的 client↔Caddy 段。
    • app/server/main.go:服务端 KeepaliveParams(30s/10s)+
      KeepaliveEnforcementPolicy(MinTime=10s)。后者必须配套:gRPC 默认
      MinTime=5min,不放宽会把明文直连客户端的 30s PING 判为滥用并 GOAWAY。
      死连接触发 Recv 报错后走既有 shutdown/UnregisterPTY 清理路径。
    • pkg/config/config.go:keepalive 常量与配对约束说明。
    • pkg/ptyattach/attach.go:新增 terminalResetSequence,会话结束的所有
      路径(正常退出/错误帧/流断开/Ctrl+C)向本地终端写保守复位序列(kitty
      键盘协议弹栈清零、modifyOtherKeys、alt screen、bracketed paste、鼠标/
      focus 上报、光标形状与可见性);拨号失败(会话未建立)不写。
    • deploy/tls/Caddyfile.example:补充 keepalive 逐跳语义注释。
  • Any relevant context:
    HTTP/2 PING 逐跳终止:Caddy 前置 TLS 时客户端 PING 只到 Caddy、服务端 PING
    只到回源 loopback,Caddy 侧无需且不应做任何配置改动(流级超时禁令不变)。

🔗 Related Issues

Closes #
Refs #


🧪 Testing

  • Local testing passed(make fmt / make lint 0 issues / make test 全包 ok,test 带 -race)
  • Unit tests passed(新增:TestGRPCKeepaliveDefaultsPairing 守护
    MinTime < Time 与 Time ≥ 10s 钳制下限;
    TestRunCommandDialFailureSkipsTerminalReset
    happy-path 断言 stdout 以复位序列收尾)
  • Manual test scenarios:
    • Scenario 1: 双端重新部署后挂 40+ 分钟空闲 PTY 会话,确认不再断连
    • Scenario 2: 强制断开会话后本地终端输入无 9;1:3u 残留、可直接重连

⚠️ Notes

  • 两端二进制都需重新部署(server + 本地 rclaude)才生效。
  • 遗留(本 PR 明确不做):attach takeover 语义。Caddy 部署下客户端暴毙
    (无 FIN 送达 Caddy)时 activePTY 仍可能泄漏;明文直连模式已由服务端
    keepalive 在约 40s 内兜底。
  • 若后续调整 keepalive 参数,必须保持 EnforcementPolicy.MinTime <
    客户端 Time(有单测守护)。

✅ Checklist

  • Code follows project conventions
  • Tests added/updated if needed
  • Documentation updated (if applicable)(Caddyfile.example 注释;
    docs/reference/grpc-keepalive.md 为本地文档,docs/ 不入库)
  • No sensitive information included

🤖 Generated with Claude Code

PTY sessions died after 40-60 min idle (client saw "error reading from
server: EOF"): with no keepalive anywhere, idle connections were reaped
by NAT/firewall middleboxes, the server never noticed the dead client,
so the attach handler blocked in Recv() forever and activePTY leaked --
every reconnect failed with "pty session already attached" until the
server was restarted. The abrupt disconnect also left the local
terminal with the remote app's kitty keyboard protocol still pushed,
echoing "9;1:3u" fragments on every key release.

- transport.Dial: client keepalive (30s/10s, PermitWithoutStream);
  covers both daemon and pty connections. Behind Caddy the PING
  terminates at the proxy, which is exactly the segment that was being
  idle-reaped; no Caddy config change is needed or allowed.
- server: KeepaliveParams (30s/10s) plus KeepaliveEnforcementPolicy
  (MinTime=10s) -- without relaxing the 5min gRPC default, plaintext
  direct deployments would GOAWAY the 30s client pings.
- ptyattach: emit a conservative terminal reset sequence (kitty
  keyboard pop/zero, modifyOtherKeys, alt screen, bracketed paste,
  mouse/focus reporting, cursor) on every session end after a
  successful attach; skipped when dialing failed.

Known leftover: attach takeover semantics. Behind Caddy a client that
dies without a FIN reaching the proxy can still leak activePTY; direct
deployments are now covered by server-side keepalive within ~40s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@flyingEirc
flyingEirc merged commit 090a741 into master Jul 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant