深|码|洞|悉头像
关注

VS Code Remote-SSH 更换服务器 IP 的无缝迁移实践

摘要

服务器更换 IP 后,很多人会直接修改 VS Code 的 SSH 配置,然后重新打开项目。这样做通常可以重新连上服务器,但经常会出现几个问题:

  • VS Code 把新 IP 当成一台全新的服务器;
  • 原来的打开文件、编辑器布局、终端和会话状态没有恢复;
  • SSH 用户名变成本机用户名;
  • “近期项目”中仍然出现旧 IP;
  • 直接 IP 连接可以使用,但带 ProxyJump 的 SSH 别名连接失败;
  • 表面上是“项目打不开”,实际失败的是另一个被 VS Code 自动恢复的工作区。

这些现象的共同原因是:VS Code Remote-SSH 的身份并不只由 IP 决定,而是由 SSH 主机标识、用户名、远程路径、工作区缓存和最近项目索引共同决定。真正可靠的迁移,必须同时处理这些状态。

本文总结一次实际迁移中遇到的问题、根因、正确的处理方法,以及如何使用 Python 将流程固化为可重复执行的工具。

文中的服务器地址均使用占位符。实际使用时应替换为自己的旧地址、新地址和 SSH 别名。

一、问题背景:为什么只修改 IP 不够

假设服务器发生以下变化:

OLD_A -> NEW_A
OLD_B -> NEW_B
OLD_C -> NEW_C

其中某个服务器上的项目位于 Windows 远程主机的 C:\code,另一个连接通过 SSH 跳板访问 Linux 主机。

如果只修改 .ssh/config 中的地址,SSH 可能可以连接,但 VS Code 仍然会根据下面这样的 URI 区分工作区:

vscode-remote://ssh-remote%2BOLD_B/c%3A/code
vscode-remote://ssh-remote%2BNEW_B/c%3A/code

虽然远程目录看起来相同,但两者的 Remote-SSH authority 不同,VS Code 会把它们视为两个工作区。

因此,迁移目标不是“把配置文件中的旧字符串替换掉”,而是:

将旧 Remote-SSH 身份对应的全部本地状态,迁移到新身份,并让 VS Code 的活动索引只保留新身份。

二、最容易混淆的三个概念

1. SSH 用户名不等于本机用户名

SSH 连接中的用户名由 SSH 配置决定:

Host NEW_A
    HostName NEW_A
    User user
    IdentityFile "C:\Users\<local-user>\.ssh\id_rsa"

IdentityFile 的路径位于本机,并不意味着远程用户名也是本机用户名。

如果新 IP 没有对应的 Host 块,SSH 可能退回默认行为,最终表现为使用本机用户名登录。因此迁移时必须完整复制旧主机块中的:

  • User
  • HostName
  • Port
  • IdentityFile
  • IdentitiesOnly
  • ProxyJump
  • ProxyCommand
  • ForwardAgent
  • 其他连接选项

不要根据本机用户名推断远程用户名。

2. 直接主机和 SSH 别名是两个不同身份

例如:

Host OLD_C
    HostName OLD_C
    User Administrator

Host OLD_C-ws1
    HostName 172.30.134.134
    User root
    Port 2222
    ProxyJump OLD_C

迁移后应同时得到:

Host NEW_C
    HostName NEW_C
    User Administrator

Host NEW_C-ws1
    HostName 172.30.134.134
    User root
    Port 2222
    ProxyJump NEW_C

不能只把 OLD_C 替换为 NEW_C,却遗漏 OLD_C-ws1。也不能把别名中的内部地址 172.30.134.134 错误地替换成公网或 Tailscale 地址。

3. 近期项目和本地文件历史不是一回事

VS Code 中至少存在两类容易混淆的历史数据:

  • 近期项目、工作区恢复状态、Remote-SSH 缓存;
  • 文件级 Local History 历史快照。

前者决定旧 IP 是否继续出现在“近期项目”或 Remote-SSH 项目列表中;后者保存过去编辑过的文件内容,里面可能仍然包含旧 IP。

备份目录和 Local History 中保留旧地址,不代表活动连接仍然使用旧地址。迁移时应优先清理活动索引,同时保留历史快照和备份以便回滚。

三、VS Code Remote-SSH 状态分布在哪里

下面是一次迁移中需要重点检查的位置:

位置作用迁移策略
~/.ssh/configSSH 主机、用户、端口、密钥、跳板克隆主机块并更新引用
User/settings.jsonRemote-SSH 平台、代理及其他设置替换旧 authority 或地址
User/globalStorage/storage.json近期项目、最近打开路径、最后活动窗口必须更新,否则旧项目会重新出现
User/globalStorage/state.vscdbVS Code 全局状态扫描 SQLite 表中的字符串和字节字段
User/workspaceStorage/<hash>工作区布局、打开文件、终端、聊天会话整个目录复制到新 URI 对应的目录
CachedConfigurations/userRemote-SSH 缓存配置克隆新主机缓存并隔离旧缓存
User/History文件本地历史快照默认保留,不当作近期项目索引处理
Code/Backups异常退出或窗口恢复数据保留备份,但避免让它成为活动索引来源

其中最关键的是 workspaceStorage。在当前 VS Code 环境中,目录名可以验证为工作区 URI 的 MD5:

import hashlib

def workspace_hash(uri: str) -> str:
    return hashlib.md5(uri.encode("utf-8")).hexdigest()

因此,下面这种做法是不完整的:

只修改 workspace.json 中的旧 IP

因为目录名仍然对应旧 URI,VS Code 可能找不到原有状态,或者同时枚举旧目录和新目录。

正确做法是:

  1. 读取旧 workspace.json 中的完整 URI;
  2. 生成替换后的新 URI;
  3. 计算新 URI 对应的工作区目录名;
  4. 复制整个旧工作区目录;
  5. 更新复制后的 workspace.json 及其内部状态;
  6. 确认新目录存在后,再把旧目录移出活动 workspaceStorage

四、Remote-SSH 地址不只有一种写法

简单的文本替换经常失败,是因为同一个主机可能以多种形式出现。

1. 明文 authority

ssh-remote+OLD_A

2. URI 编码形式

ssh-remote%2BOLD_A

3. 十六进制编码 authority

VS Code 的部分缓存目录或状态中会出现类似:

ssh-remote+7b22686f73744e616d65223a224f4c445f4122...

解码后可能是:

{"hostName":"OLD_A","user":"Administrator"}

也可能使用小写字段名:

{"hostname":"OLD_A","user":"Administrator"}

因此解析代码应同时兼容 hostNamehostname,不能只搜索明文 IP。

一个安全的处理方式是先解析十六进制 JSON,再只修改主机字段:

def replace_authority_payload(payload: dict, mapping: dict[str, str]) -> dict:
    key = "hostname" if "hostname" in payload else "hostName"
    host = payload.get(key)
    if host in mapping:
        payload = dict(payload)
        payload[key] = mapping[host]
    return payload

此外,直接 IP 的正则替换也要避免误伤别名:

OLD_C-ws1

应优先建立完整别名映射:

OLD_C-ws1 -> NEW_C-ws1

然后再处理普通 IP 映射。

五、无缝迁移的正确流程

第一步:停止 VS Code

必须确认没有 Code.exe 进程。VS Code 运行期间会持续写入 storage.json 和 SQLite 状态库,迁移过程中修改这些文件可能导致:

  • 修改被 VS Code 覆盖;
  • 数据库锁定;
  • 近期项目索引重新出现旧项;
  • 工作区状态损坏。

脚本可以主动检测 VS Code 是否运行,并在默认情况下拒绝继续。

第二步:建立映射表

不要在代码中散落多个替换语句,统一集中管理:

MAPPINGS = (
    ("OLD_A", "NEW_A"),
    ("OLD_B", "NEW_B"),
    ("OLD_C", "NEW_C"),
)

每次新增服务器迁移时,只扩展这张表。

第三步:备份

每次运行生成独立的时间戳目录,例如:

Code/remote-ip-migration-backup-YYYYMMDD-HHMMSS/

备份至少包括:

  • 原始 SSH 配置;
  • 原始 settings.json
  • 原始 storage.json
  • 被修改的 SQLite 数据库;
  • 原始工作区目录;
  • 被移出活动目录的旧工作区和缓存。

迁移应优先使用“移动到隔离备份”而不是直接删除。这样既可以避免 VS Code 继续枚举旧项目,也保留了恢复路径。

第四步:复制并更新工作区

整个工作区目录都应复制,而不是只复制 workspace.json。这样可以保留:

  • 上次打开的文件;
  • 编辑器分栏与布局;
  • 集成终端状态;
  • 调试状态;
  • Remote-SSH 扩展状态;
  • Copilot Chat 或其他扩展产生的工作区会话。

复制后,再对 JSON、JSONL、聊天会话和 SQLite 状态执行地址迁移。

第五步:更新活动索引

storage.json 是最容易遗漏的文件。重点关注:

  • backupWorkspaces
  • workspaces3
  • windowsState.lastActiveWindow
  • 其中的 folderUriremoteAuthority

如果只更新 workspaceStorage,而不更新 storage.json,VS Code 仍可能在“近期项目”中显示旧地址,甚至在下一次启动时把旧工作区重新写回来。

第六步:隔离旧工作区

只有在以下条件同时满足时,才能移动旧目录:

  1. 旧工作区 URI 已成功解析;
  2. 新 URI 对应的目录存在;
  3. 新目录中的 workspace.json 与新 URI 一致;
  4. 旧目录不是当前唯一可恢复副本。

如果新目录无法验证,宁可保留旧目录并输出警告,也不要盲目移动。

六、Python 自动化脚本的核心设计

本次迁移最终使用 Python 实现,原因是它更适合同时处理:

  • JSON/JSONL 文本;
  • URI 编码与十六进制 JSON;
  • SQLite 数据库;
  • 目录复制与可恢复移动;
  • Windows 进程检测;
  • SSH 配置解析。

脚本建议具备以下接口:

python replace_vscode_remote_ip.py --dry-run
python replace_vscode_remote_ip.py
python replace_vscode_remote_ip.py --keep-old

推荐行为:

  • --dry-run 只显示计划,不修改文件;
  • 默认迁移并隔离已验证成功的旧工作区;
  • --keep-old 仅用于排查或需要暂时保留旧目录的场景;
  • 每次实际运行生成新的备份目录;
  • 默认不允许 VS Code 运行时执行;
  • 所有不确定的工作区都输出警告并跳过。

SQLite 处理不能只查固定表名,因为不同版本和扩展可能产生不同表。更通用的方法是:

  1. 枚举所有用户表;
  2. 读取表结构;
  3. 检查字符串字段;
  4. 尝试解码 UTF-8 字节字段;
  5. 只更新确实发生变化的单元格;
  6. 提交前先备份数据库。

需要注意:SQLite 的空闲页可能仍然保留旧字符串。用二进制搜索数据库文件时,可能还能搜到已删除记录中的旧 IP,但这不代表 VS Code 查询活动表时仍会使用它。判断是否影响功能,应以 SQLite 活动行和 VS Code 实际索引为准。

七、如何判断“打不开”的真正原因

不能只看到 VS Code 窗口打不开,就认定 IP 替换失败。应按层次验证。

1. SSH 配置层

ssh -G NEW_B

重点确认:

user       远程用户名
hostname   实际目标地址
port       端口
identityfile 密钥
proxyjump  跳板配置

2. 网络层

Test-NetConnection NEW_B -Port 22

如果 TCP 22 端口不通,应先解决网络、路由、防火墙或目标服务问题。

3. SSH 认证层

ssh -o BatchMode=yes -o ConnectTimeout=10 NEW_B "echo SSH_OK"

如果能返回 SSH_OK,说明用户名、密钥和基本 SSH 握手正常。

4. 远程路径层

Windows 远程主机还要确认路径存在:

if exist C:\code (echo CODE_EXISTS) else (echo CODE_MISSING)

Linux 主机则应确认目录权限、用户和工作目录。

5. VS Code Server 层

查看 Remote-SSH 日志,关注:

  • listeningOn==...:远程 VS Code Server 是否启动;
  • Resolved "ssh-remote+...":VS Code 实际连接的是哪个 authority;
  • EACCES:远程用户是否有权限写 .vscode-server
  • Canceled:通常是连接被取消或上游远程窗口失败;
  • remote.SSH.remotePlatform:平台判断是否正确。

一次典型的误判是:直接主机 NEW_C 已经成功启动了 Windows VS Code Server,但 VS Code 同时恢复了另一个别名 NEW_C-ws1。后者使用 root 连接 Linux,并因为无法创建 /root/.vscode-server/data/logs 而失败。此时用户看到的是“项目打不开”,但失败的并不是 C:\code 对应的直接主机。

八、常见失败方式与改进方案

失败方式一:全局字符串替换

问题:无法处理哈希目录、编码 authority、SQLite 和别名关系。

改进:先解析数据结构,再替换字段;对工作区使用“新 URI → 新哈希目录”的迁移方式。

失败方式二:只改 SSH 配置

问题:SSH 可以连接,但 VS Code 认为这是新服务器,原工作区状态不恢复。

改进:同步迁移 workspaceStoragestorage.json 和 Remote-SSH 缓存。

失败方式三:遗漏 User

问题:新主机没有明确的 User,SSH 退回本机用户名。

改进:从旧 Host 块复制远程用户和全部连接选项,禁止自动推断。

失败方式四:把旧目录永久留在活动目录

问题:VS Code 会继续扫描旧工作区,近期项目中仍出现旧 IP。

改进:验证新目录完整后,把旧目录移动到时间戳备份目录。

失败方式五:只处理明文 hostname

问题:VS Code 的编码对象可能使用 hostName 字段,也可能出现在 %2B URI 中。

改进:兼容 +%2B、十六进制 authority,以及 hostname/hostName 两种字段名。

失败方式六:在 VS Code 运行时迁移

问题:迁移完成后,VS Code 退出或启动时又把旧索引写回来。

改进:强制要求所有 Code.exe 进程退出,迁移完成后再启动 VS Code。

九、迁移验收清单

迁移完成后,建议按下面清单验收:

  • ssh -G NEW_A 显示正确的远程用户名;
  • ssh -G NEW_B 显示正确的端口和密钥;
  • ProxyJump 别名指向新主机;
  • TCP 22 端口可达;
  • 直接 SSH 命令可以执行;
  • Windows/Linux 远程路径存在;
  • workspaceStorage 目录存在;
  • workspace.json 使用新 URI;
  • 原工作区的打开文件和布局状态被复制;
  • storage.json 的近期项目和最后活动窗口没有旧 IP;
  • Remote-SSH 缓存中没有旧主机活动项;
  • 旧目录只存在于可恢复备份;
  • VS Code 完全退出后再启动验证;
  • 不把 User/History 中的历史快照误判为活动连接配置。

结语

VS Code Remote-SSH 的 IP 迁移,本质上不是一次简单的文本替换,而是一次本地工作区身份迁移。可靠方案必须同时维护四种一致性:

  1. SSH 连接身份一致;
  2. Remote-SSH authority 一致;
  3. 工作区 URI 与哈希目录一致;
  4. VS Code 活动索引与缓存一致。

只要按照“先备份、再解析、复制完整工作区、更新活动索引、最后隔离旧项”的顺序处理,就可以在更换服务器 IP 后尽量保留原来的编辑器状态,并避免用户名错位、项目重复、旧历史反复出现和别名连接失败等问题。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_33843237/article/details/166249465

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--