×

SSH 断连后终端满屏 `35;24;53M`:一次鼠标上报乱码的排查

Falcon 2026-10-13 views:
自动摘要

正在生成中……

现象

我在 Mac 上用 SSH 连着家里的 NAS,挂了三个多小时。回来时连接已经断了:

Read from remote host fn.local: Connection reset by peer
Connection to fn.local closed.
client_loop: send disconnect: Broken pipe

断连本身不稀奇,奇怪的是之后的事:只要鼠标在终端窗口上移动,命令行里就不停冒出这样的字符:

35;6;53M35;24;53M35;25;53M35;26;53M35;27;53M35;35;53M35;34;51M...

不小心按了回车,又刷出一屏报错:

zsh: command not found: 35
zsh: command not found: 43
zsh: command not found: 43M35
zsh: command not found: 44
...

想敲命令修复也很别扭,因为手一碰触控板,新的乱码就插进正在输入的命令里。

这些字符是什么

它们是终端发给程序的鼠标事件,完整形态是一段转义序列:

ESC [ < 35 ; 24 ; 53 M

这是 SGR 鼠标协议的格式 ESC [ < 按键码 ; 列 ; 行 M。三个数字的含义:

  • 35:按键码。32 表示"移动",加上 3 表示"没有按任何键",合起来就是纯粹的鼠标移动。
  • 24:鼠标所在的列。
  • 53:鼠标所在的行。

所以数字会随着鼠标移动连续变化,一串乱码其实是鼠标在窗口里的移动轨迹。

zsh 的行编辑器不认识这段序列,开头的 ESC [ < 被吞掉,剩下的 35;24;53M 就当作普通输入显示在命令行上。

为什么会残留

终端模拟器默认不上报鼠标。vim、tmux、htop 以及各种 TUI 程序想要支持鼠标时,会在启动时向终端输出几段转义序列来开启它,常见的有:

| 序列 | 作用 | | --- | --- | | ESC[?1000h | 上报按下和松开 | | ESC[?1002h | 额外上报按住拖动 | | ESC[?1003h | 上报所有移动,不论是否按键 | | ESC[?1006h | 使用 SGR 编码(就是上面那种格式) |

把结尾的 h 换成 l 就是关闭。程序正常退出时会自己发一遍关闭序列,把终端恢复原样。

关键在于:这个开关的状态存在本地的终端模拟器里,而不在远程机器上。 远程程序通过 SSH 把"开启"指令传过来,本地终端照做。连接被强行切断时,远程程序根本没有机会发"关闭"指令,本地终端就一直停留在上报模式,把鼠标事件发给了接手的本地 shell。

我看到的全是 35 开头的移动事件,说明远程那个程序开的是 1003(上报所有移动)加 1006。

为什么回车后一堆 command not found

; 在 shell 里是命令分隔符。35;43;43M35;44 回车后被拆成 35、43、43M35、44 四条命令依次执行,每条都找不到,于是各报一次错。

这些报错无害,没有任何东西被真正执行。

当场修复

  1. 把鼠标指针移出终端窗口,或者手离开触控板。
  2. 按 Ctrl+C 清空当前输入行。
  3. 只用键盘输入:
reset

reset 做两件事:把 tty 驱动恢复到正常设置(相当于 stty sane),再向终端发送完整的复位序列。后者会关闭鼠标上报、退出备用屏幕、恢复光标和颜色,并清屏。它不影响当前目录、环境变量和历史记录。

注意 clear 不行,它只清屏,不改终端模式。

如果不想清屏,可以只关鼠标上报:

printf '\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?1015l'

实在不行就关掉标签页重开一个,新会话默认不开鼠标上报。

顺便查了断连原因

第一反应是空闲超时,想去加保活配置。打开 ~/.ssh/config 才发现早就有了:

Host *
  ServerAliveInterval 10
  ServerAliveCountMax 5

每 10 秒探测一次,连续 5 次无响应才放弃。而且保活失败时的报错是 Timeout, server ... not responding,我看到的却是 Connection reset by peer,意思是对端主动回了 RST。所以空闲超时可以排除,更可能是下面几种之一:

  • Mac 睡眠后唤醒,NAS 那边早已丢弃了连接。
  • NAS 重启,或者上面的 sshd 被重启。
  • 网络切换,比如 Wi-Fi 重连、IP 变化。

具体是哪个我没有去确认。重要的是,这几种情况保活都救不了,断连没法根除。

排查时还发现配置里 fn.local 被定义了两次。SSH 对每个选项取第一次匹配到的值,后一条实际上不起作用,就顺手删了。想知道某个主机最终生效的配置,可以用:

ssh -G fn.local

根治:让 ssh 退出后自动复位

既然断连避免不了,那就让终端在 ssh 结束后自己恢复。在 ~/.zshrc 里给 ssh 包一层:

# ssh 退出后关闭鼠标上报并恢复光标
ssh(){
  command ssh "$@"
  local rc=$?
  [[ -t 1 ]] && printf '\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?1015l\e[?25h'
  return $rc
}

几个细节:

  • command ssh 调用真正的 ssh,避免函数递归调用自己。
  • 先保存退出码,最后原样返回,这样 ssh host cmd && ... 之类的写法不受影响。
  • [[ -t 1 ]] 判断标准输出是不是终端。没有这一句,x=$(ssh host cmd) 或 ssh host cmd | grep ... 捕获的结果里会混进转义字符。
  • 末尾的 \e[?25h 是恢复光标显示,有些程序异常退出后光标也会消失。

这个函数只在交互式 shell 里生效。git、rsync、scp 直接调用 ssh 二进制,不会经过它。

小结

  • 满屏的 35;列;行M 是鼠标移动事件,不是程序输出,也不是终端坏了。
  • 根源是远程程序开了鼠标上报,SSH 异常断开导致关闭指令没发出来,而开关状态留在了本地终端。
  • 当场用 reset 解决,长期用一个 ssh 包装函数自动复位。
  • 保活配置管不了 Connection reset by peer 这类断连。
本文收录于