Vasily's Blog

一个记录学习经历的站点

0%

VS Code Remote-SSH 老服务器兼容:node-a 成功与 node-b 终端困局

VS Code Remote-SSH 老服务器兼容:node-a 成功与 node-b 终端困局

问题

VS Code 从 1.86 开始,远程 vscode-server 中的 Node.js 要求 glibc >= 2.28。CentOS 7 默认 glibc 2.17,低于此要求。现象:

  1. SSH 隧道能通,code-<commit> CLI binary 可能启动成功
  2. server 进程(code-server + node)启动即崩溃
  3. VS Code 认为"server 未安装",联网下载 → 服务器不能上网 → 404
  4. 无限循环重试,自动删除并重建 server 目录

目标是不降级 VS Code,在 glibc 2.17 的老服务器上跑通 Remote-SSH。

Remote-SSH 远程架构的演进

要理解修复思路,必须先搞清楚 vscode-server 在远程的目录结构。

经典旧架构(VS Code < 1.86)

1
2
3
4
5
6
~/.vscode-server/
├── bin/<commit-hash>/
│ ├── node ← ELF 64-bit
│ ├── out/server-main.js
│ └── extensions/
└── code-<commit> ← CLI binary(启动器)

启动链路: VS Code 客户端 → SSH 隧道 → CLI binary 启动 → 调用 bin/<commit>/bin/code-server shell 脚本 → 脚本调用 node out/server-main.js

关键发现:旧版 bin/code-server 是一个 shell 脚本(约 249 字节),不涉及 glibc,只是调用 node:

1
2
3
#!/usr/bin/env sh
ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")"
"$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@"

glibc 负担完全在 node 这个 ELF 二进制上。

新 CLI 架构(VS Code 1.86~1.98,过渡期)

1
2
3
4
5
6
7
8
9
~/.vscode-server/
├── cli/
│ └── servers/
│ └── Stable-<commit>/
│ └── server/
│ ├── bin/code-server ← 仍是 shell 脚本
│ ├── node
│ └── out/
└── code-<commit>

server 从 bin/<commit>/ 移到了 cli/servers/Stable-<commit>/server/,内部结构大体相同。

全新架构(VS Code 1.126,当前版本)

1
2
3
4
5
6
7
8
9
10
11
~/.vscode-server/
├── cli/
│ └── servers/
│ └── Stable-<commit>/
│ └── server/
│ ├── bin/
│ │ ├── code-server/ ← **变成目录**(内含 ELF 二进制)!
│ │ └── ...
│ ├── node ← 96MB ELF,可能是目录
│ └── out/
└── code-<commit> ← 32MB 静态链接 ELF

关键变化: bin/code-server 从 shell 脚本变成了自包含 ELF 二进制,同样需要 glibc 2.28+。这是新版 VS Code 在旧服务器上完全不能用的根本原因——两个地方都卡 glibc

各版本修复需求

VS Code 版本 需要修 node 需要修 code-server server 路径
< 1.86 ❌ (shell 脚本) bin/<commit>/
1.86~1.98 ❌ (shell 脚本) cli/servers/Stable-/server/
1.99~1.110 ❌? cli/servers/Stable-/server/
1.126+ ✅ (注意可能是目录) ✅ (变成了 ELF 目录) cli/servers/Stable-/server/

三轮尝试

❌ 第一轮:替换 CLI binary + 伪装旧 server 目录

直接把旧版 server 目录 cp -r 改名成新版 commit hash,再用旧版 CLI binary 覆盖新版。

结果: VS Code 连上了,server 进程启动成功,但立刻报 Client refused: version mismatch。旧 server 的 JS 代码报告的是旧版本号(1.98.2),VS Code 1.126 客户端不认。

❌ 第二轮:下载新版 server 替换 JS 代码

在本机下载新版 server,scp 传上去,用新版 out/package.jsonproduct.json 覆盖旧目录。

结果: 仍然连不上。新版 server 的 bin/code-server 变成了 ELF 目录,在 glibc 2.17 上启动即崩溃。VS Code 进入无限重试循环。

✅ 第三轮:旧版 code-server + 新版 JS,混合部署

核心洞察: bin/code-server 在 1.98 中是 shell 脚本,在 1.126 中是 ELF 目录。用旧版的 shell 脚本覆盖新版目录,就可以绕过 ELF binary 的 glibc 需求。同理,node 也用旧版的。

操作步骤:

在服务器上:

1
2
3
4
5
6
7
8
9
10
11
# 1. 从旧版重建新版目录
cp -r ~/.vscode-server/cli/servers/Stable-<old-commit> \
~/.vscode-server/cli/servers/Stable-<new-commit>

# 2. 确认旧版 code-server 是 shell 脚本
file ~/.vscode-server/cli/servers/Stable-<new-commit>/server/bin/code-server
# 输出应为: "a /usr/bin/env sh script, ASCII text executable"

# 3. 确认 node 能跑
~/.vscode-server/cli/servers/Stable-<new-commit>/server/node --version
# 输出: v20.18.2

在本机 scp 新版 JS 代码覆盖:

1
2
3
scp -J jump-user@jump-host -r C:\path\to\vscode-server-linux-x64\out user@target:~/.../server/
scp -J jump-user@jump-host C:\path\to\vscode-server-linux-x64\package.json user@target:~/.../server/
scp -J jump-user@jump-host C:\path\to\vscode-server-linux-x64\product.json user@target:~/.../server/

关键细节: 旧版 bin/code-server 和旧版 node 的执行权限在新版目录中可能没有保留,务必补上:

1
2
chmod +x ~/.vscode-server/cli/servers/Stable-<new-commit>/server/node
chmod +x ~/.vscode-server/cli/servers/Stable-<new-commit>/server/bin/code-server

最终文件结构:

1
2
3
4
5
6
7
8
9
server/
├── bin/
│ └── code-server ← 旧版 shell 脚本(不是目录!)
├── extensions/ ← 新版(scp 覆盖)
├── node ← 旧版 v20.18.2
├── node_modules/ ← 新版
├── out/ ← 新版 JS 代码
├── package.json ← 新版
└── product.json ← 新版

VS Code 的无限重试机制(最坑的地方)

每次连接失败后,日志显示 VS Code 在反复循环:

1
2
3
4
[server] Installing and setting up...
[server] Error installing server: ... 404 Not Found
[server] Checking ... log.txt and pid.txt ...
[server] Installing and setting up...

同时服务器上 cli/servers/ 目录里的 Stable-<commit>自动删除并重建为空的 .staging 目录。

根本原因: VS Code 检测到 server 进程启动失败后触发自动修复机制:

  1. CLI binary 上报「server 在端口 X 上监听」(实际是通信端口,不是真 server)
  2. exec server 发现真正的 VS Code Server 没有启动
  3. 触发 server 安装流程 → 联网下载 → 404 → 重试

解决方法:必须完全关闭 VS Code 窗口。 只断开远程连接不够——后台 exec server 仍在持续重试。正确流程:

  1. 关掉整个 VS Code(File → Exit)
  2. 在服务器上重建 server 目录
  3. 清理锁文件:rm -f ~/.vscode-server/.cli.*.lock
  4. 重启 VS Code
  5. 立即连接

node-b 实战:文件编辑成功,终端困局

背景

node-a 修复成功后,用完全相同的混合 server 目录部署到 node-b。node-b 是全新服务器,之前没用 VS Code 连过,所以需要从头创建 server 目录。

SSH 配置:

1
2
3
4
Host remote-server
HostName node-b
User user
ProxyJump jump-host

第一步:上传 server 目录(成功)

把已在本地准备好的 vscode-server-linux-x64/(含旧版 node v20.18.2 + 旧版 bin/code-server shell 脚本 + 新版 out/)直接 scp 到 node-b:

1
scp -J jump-host -r /path/to/vscode-server-linux-x64 user@node-b:~/.vscode-server/cli/servers/Stable-<commit>/server

注意事项:

  • node-b 没有旧版 VS Code 连过,Stable-<commit> 目录需先手工创建

  • scp 的目录层级问题:源目录本身会作为子目录传过去,需上移一层

第二步:修复 CLI binary(GLIBCXX 检查)

node-b 在旧版 CLI binary 上没有 GLIBCXX 兼容性问题。将旧版 CLI binary(v1.98.2)复制为新版路径:

1
2
3
cp /backup/path/.vscode-server/code-ddc367ed... \
~/.vscode-server/code-7e7950df89...
chmod +x ~/.vscode-server/code-7e7950df89...

VS Code 连接成功,文件编辑功能完好。可以正常浏览文件、编辑代码。

第三步:终端修复尝试(全线失败)

虽然文件编辑正常,但 Ctrl+` 终端完全无法使用——有光标但无命令提示符,bash 未启动。以下是所有尝试过的方案:

方案 A:旧版 node + 旧版 CLI binary(纯替换)

文件编辑 ✅,终端 ❌。有光标但 bash 未启动。

失败原因:新版 out/server-main.js 使用 node-pty 进行 PTY 进程管理。node-pty 包含原生模块 pty.node,该模块为 Node.js v24(NODE_MODULE_VERSION 128)编译,旧版 Node.js v20.18.2(NODE_MODULE_VERSION 115)无法加载它:

1
2
Error: Failed to load native module: pty.node
checked: build/Release, build/Debug, prebuilds/linux-x64

这是旧版 node 不可逾越的障碍——N-API ABI 不兼容。

方案 B:新版 node(v24)+ 自备 glibc 2.28 + patchelf

文件编辑 ❌,终端 ❌。LD_LIBRARY_PATH 污染子进程。

操作: 1. 从 tar.gz 提取原始新版 node 二进制(122MB) 2. 从 CentOS 8 RPM 提取 glibc 2.28 so 文件 3. 用 patchelf 修改新版 node 的 ELF 解释器

失败原因(两个层面):

层面 1 — patchelf 在大文件上 segfault:新版 node 二进制 122MB,patchelf --set-rpath 一致崩溃。只加 --set-interpreter 可以但找不到 so 文件。

层面 2 — LD_LIBRARY_PATH 污染子进程:通过包装脚本设置 LD_LIBRARY_PATH 后,该环境变量被所有子进程继承。bash、rm、ls 等系统命令被迫加载 glibc 2.28 的 libc.so.6,与自身 glibc 2.17 编译不兼容,全部崩溃:

1
2
rm: relocation error: .../libc.so.6: symbol _dl_fatal_printf...
bash: exit code 127

方案 C:新版 node + process.execPath 包装

文件编辑 ❌,终端 ❌。/proc/self/exe 指向 ld-linux 而非 node。

失败原因:当 node 通过 ld-linux-x86-64.so.2 启动时,/proc/self/exe 指向 ld-linux 自身。因此:

  • process.execPath = /path/to/ld-linux-x86-64.so.2(错误!)

  • child_process.spawn(process.execPath, args) → 把 --dns-result-order=ipv4first 传给 ld-linux

  • ld-linux 不认识这个参数 → 报错退出

方案 D:patchelf --set-interpreter + wrapper 传 --library-path

文件编辑 ⚠️ 主进程 OK 但子进程全挂,终端 ❌。

失败原因--library-path 只对直接通过 ld-linux 调用的主进程有效。通过 child_process.fork()child_process.spawn() 启动的子进程(ptyHost、extensionHost、fileWatcher)没有继承 --library-path

方案 E:旧版 node(v20)+ 其余全部新版(最关键的诊断)

文件编辑 ⚠️ 部分功能正常,终端 ❌。

失败原因:日志精确显示:

1
2
3
[IPC Library: Pty Host] Uncaught Exception:
Error: Failed to load native module: pty.node
checked: build/Release, build/Debug, prebuilds/linux-x64

pty.node 是 VS Code 1.126 的 node-pty 包提供的原生 C++ 拓展,为 Node.js v24 编译(NAPI 128)。旧版 Node.js v20.18.2(NAPI 115)无法加载。这是所有方案都无法绕过的根本障碍。

终端失败的终极根因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
旧版 node (v20.18.2)
→ 能运行 JS 代码 ✅
→ 能启动 server-main.js ✅
→ 能打开 SSH 隧道 ✅
→ 文件编辑功能正常 ✅
→ 但无法加载 pty.node ❌ ← 原生模块 ABI 不兼容
→ ptyHost 进程崩溃 ❌
→ 终端无法使用 ❌

新版 node (v24.15.0)
→ 需要 glibc 2.28 ❌ ← CentOS 7 只有 2.17
→ patchelf --set-interpreter 可以 ✅(~130ms 启动)
→ patchelf --set-rpath 大文件 segfault ❌
→ 没有 RPATH 找不到 glibc 2.28 的 so ❌
→ LD_LIBRARY_PATH 污染子进程 ❌

双服务器对比

功能 node-a node-b
文件编辑
终端 (Ctrl+`)
SSH 外部终端
扩展安装
文件监控 (ENOSPC) ⚠️ 不影响使用 ⚠️ 不影响使用
修复方法 旧 binary 替换法 旧 binary 替换法(文件编辑)
终端修复 巧合 + rootless 适用 不可行

node-a 的终端能在旧版 node 下工作纯属巧合——可能因为其中间版本的 vscode-server 留有兼容旧 ABI 的 pty.node 版本,或 kernel/devpts 配置不同。具体原因未深究。

可行的工作区方案

当终端无法工作时,有两种替代方案:

方案 A:外置终端 在 VS Code 外打开 PowerShell 通过 SSH 直连,VS Code 内正常编辑文件:

1
ssh -J jump-host user@node-b

两个窗口并排使用,日常编辑不受影响。

方案 B:VS Code 1.98 便携版 1. 下载 VS Code 1.98.x 便携版 2. 用旧版连 node-b → 服务器自动下载匹配的 CLI binary + server 3. 新旧两个 VS Code 互不冲突 4. 日常用 1.126 编辑,需要终端时用 1.98 连接

方案 需要旧 server 需要本机上网 需要 root 一次施工管多久 终端可用
旧 binary 替换法(本文) ✅(只需一次) 直到 VS Code 更新 ⚠️ 因环境而异
patchelf 方案 ✅(只需一次) 直到 VS Code 更新 ❌(子进程污染)
升级服务器 glibc 永久的
降级 VS Code 直到你受不了旧版
  • 有旧版 server 残留,且终端非必须 → 用本文的方案,文件编辑完美

  • 需要终端 → 用外置 SSH 终端或 VS Code 1.98 便携版

  • 没有任何旧版 server → 即使考虑 patchelf,终端也无法工作

  • 能 root 且不怕风险 → 升级 glibc,一劳永逸

只要 VS Code 1.126 的 protocol 不变,旧 binary 替换法就一直有效。VS Code 更新后 commit hash 会变——那时需要重新下载新版 server、重新 scp、重新替换 binary。


附录:完整诊断命令速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看所有 server 目录
ls -la ~/.vscode-server/cli/servers/

# 检查 node 能否运行
~/.vscode-server/cli/servers/Stable-<commit>/server/node --version

# 检查 code-server 类型(文件 vs 目录/ELF)
file ~/.vscode-server/cli/servers/Stable-<commit>/server/bin/code-server

# 查看 server 日志
cat ~/.vscode-server/cli/servers/Stable-<commit>/log.txt

# 清理锁文件
rm -f ~/.vscode-server/.cli.*.lock

# 查看所有 CLI binary 版本
for f in ~/.vscode-server/code-*; do echo "=== $f ===" && $f --version 2>&1; done

node-b 终端诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 检查 pty.node 是否存在及 ABI
file ~/.vscode-server/cli/servers/Stable-<commit>/server/node_modules/node-pty/prebuilds/linux-x64/pty.node

# 查看 server-main.js 中 ptyHost 的加载报错
cat ~/.vscode-server/cli/servers/Stable-<commit>/log.txt | grep -i "pty\|native module\|uncaught"

# 检查旧版 node 的 N-API 版本
~/.vscode-server/node --version
~/.vscode-server/node -e "console.log(process.versions.modules)"

# 检查新版 node 的 N-API 版本(如果已下载)
./original_new_node_v2 -e "console.log(process.versions.modules)"

# 测试 patchelf(注意大文件 segfault)
patchelf --set-interpreter ./glibc228/lib/ld-linux-x86-64.so.2 ./original_new_node_v2

# pty.node 原生模块位置
ls -la ~/.vscode-server/cli/servers/Stable-<commit>/server/node_modules/node-pty/prebuilds/linux-x64/