Engineering note

云端 Mac 远程会话防断:让 SSH 任务持续运行

远程 Mac ·约 6 分钟阅读

云端 Mac 远程会话防断:让 SSH 任务持续运行

你通过 SSH 在云端 Mac 上启动一次耗时较长的构建,合上笔记本,再连回来时终端已经消失。最麻烦的不是重新执行,而是无法判断任务究竟失败、完成,还是仍在后台运行。稳定做法不是盲目延长连接超时,而是把网络连接、终端会话和实际任务拆成三层:SSH 负责进入节点,tmux 保存终端上下文,日志与退出码负责证明结果。

先分清断线影响哪一层

远程桌面或 SSH 断开,只表示客户端与云端 Mac 之间的交互通道中断,不等于物理节点停止运行。但直接挂在 SSH 伪终端上的前台进程,可能在会话关闭时收到挂断信号。通过图形界面打开的终端也不适合作为无人值守任务入口,因为关闭窗口或退出桌面会改变它的生命周期。

先用一个十分钟的小任务验证环境,不要直接拿正式构建试错:

mkdir -p "$HOME/jobs/session-check"
cd "$HOME/jobs/session-check"
date -u +"start=%Y-%m-%dT%H:%M:%SZ" > run.log
sleep 600
date -u +"finish=%Y-%m-%dT%H:%M:%SZ" >> run.log

执行后主动断开,再连接并检查 run.log。这个基线测试能确认节点本身是否持续运行,但它不能替代会话托管。

SSH 保活解决的是“多久发现连接失效”,tmux 解决的是“连接失效后任务是否仍有可恢复的终端”,两者不能互相替代。

配置 SSH 保活与明确超时

在本地 Mac 的 ~/.ssh/config 中为节点建立独立配置。地址和用户名应替换为控制台提供的实际值:

Host minid-node
    HostName <node-address>
    User <system-user>
    ServerAliveInterval 30
    ServerAliveCountMax 3
    TCPKeepAlive yes
    ConnectTimeout 10

ServerAliveInterval 30 表示客户端每三十秒发送一次应用层探测;连续三次没有响应后,SSH 会退出失效连接,而不是让终端无限卡住。它不会自动重连,也不会保证前台进程存活。

连接前可检查最终生效的配置:

ssh -G minid-node | grep -E 'serveralive|tcpkeepalive|connecttimeout'
ssh minid-node

如果网络经常切换,不建议把超时拉到几十分钟。更快确认连接已经失效,反而便于重新进入 tmux 查看真实进度。

用 tmux 托管长时间任务

先确认 tmux 是否存在;没有时再通过当前环境的软件包管理方式安装:

command -v tmux || brew install tmux
tmux new-session -s ios-build

会话名应表达任务用途,例如 ios-buildintegration-testasset-export,不要长期复用含义模糊的 work。进入会话后创建独立目录,并让输出同时显示在终端和日志中:

job_dir="$HOME/jobs/ios-build-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$job_dir"
cd "$job_dir"

set -o pipefail
caffeinate -i /path/to/run-build.sh 2>&1 | tee build.log
status=${PIPESTATUS[0]}
printf '%s
' "$status" > exit-code.txt
date -u +"%Y-%m-%dT%H:%M:%SZ" > finished-at.txt

Control-b,再按 d,即可脱离会话而不终止任务。重新连接节点后执行:

tmux list-sessions
tmux attach-session -t ios-build

caffeinate -i 只在命令运行期间声明需要避免空闲休眠。它应与具体任务绑定,不要为了省事永久运行。对于纯命令行构建,不需要强制保持显示器唤醒。

避免重复启动

网络中断后,先执行 tmux list-sessionspgrep -fl run-build。确认旧任务不存在后才能重启。否则两个构建可能同时写入同一个派生数据目录、缓存或产物路径,最终留下难以复现的混合结果。

让日志和退出码成为验收依据

“重新连上后看见终端还在滚动”不是验收。每次长任务至少应留下开始时间、结束时间、完整日志和退出码。退出码为 0 才表示脚本按约定成功结束;日志末尾出现某个单词并不能替代它。

可以用下面的检查顺序快速判断状态:

检查项 命令 结论
会话是否存在 tmux list-sessions 存在不代表任务仍在运行
进程是否存在 pgrep -fl run-build 确认当前执行实例
日志是否增长 tail -n 30 build.log 判断进度或卡点
是否正常结束 cat exit-code.txt 0 表示脚本成功
结束时间 cat finished-at.txt 判断结果是否属于本次执行

脚本还应为每次运行创建新目录,不要覆盖上一轮日志。需要清理时按日期删除已确认归档的目录,而不是对整个 ~/jobs 执行递归删除。

上线前做一次主动断线演练

正式使用前,启动一项可安全重复的测试任务,然后依次验证 SSH 断开、远程桌面关闭和本地网络切换。每次重新连接后,都检查 tmux 会话、目标进程、日志增长和退出码文件。

如果任务依赖图形界面,应单独验证它在远程桌面断开后是否继续执行;命令行工具和图形应用的生命周期并不相同。遇到任务提前结束时,先查脚本退出码和日志,再查终端是否被关闭,最后检查存储空间与进程资源,不要把所有失败都归因于网络。

一套可交付的检查清单应包含:

  • SSH 配置能够通过 ssh -G 验证;
  • 每类长任务使用独立 tmux 会话;
  • 任务目录按时间创建,不覆盖旧结果;
  • 标准输出与错误输出写入同一份可检索日志;
  • 退出码和 UTC 结束时间单独保存;
  • 断线后先恢复现场,不重复启动任务;
  • 完成后主动关闭无用会话并归档产物。

常见问题

SSH 断开后,云端 Mac 上的任务一定会停止吗?

不一定。直接依附 SSH 终端的进程可能收到挂断信号,而运行在 tmux 会话中的任务会继续执行;应重新连接后用 tmux attach 和日志确认状态。

只配置 SSH ServerAliveInterval 就能保证长任务不中断吗?

不能。它只能更快发现失效连接,不能自动恢复终端内的进程。长任务仍应放入 tmux,并将标准输出、错误输出和退出码写入文件。

MiniD Cloud Mac

按天、周或月租用独享物理 Mac mini

整机独享的物理 Mac mini,远程桌面与 SSH 均可接入;具体机型、区域与租期以订购页当前信息为准。

查看订购选项