跳到正文
Hacker News· dragon-lady·· 8 天前AI 评分22

Twin Update:一条命令同步更新两台 Linux PC 上的 apt 应用

Twin Update – update the same apt apps on two Linux PCs in one run

AI 导读

Twin Update 可一条命令在两台 Debian/Ubuntu 系 Linux 机器上同步更新同一批 apt 应用,更新前保留经 sha256 校验的回滚副本,并支持一条命令回滚。

正文 · AI 翻译

译文尚不完整,完整内容请切换到原文。

在一次运行中更新两台 Linux 机器上的相同桌面应用,保留一份经过验证的已安装内容回滚副本,并用一条命令回滚。

它面向 Debian/Ubuntu 系列桌面(已在基于 Ubuntu 24.04、使用 Python 3.12 的系统上测试;需要 Python 3.11+、systemd 用户会话和 apt),以及以来自厂商仓库的 apt 包形式安装的应用。内置条目:Cursor(cursor)、ChatGPT 桌面版(chatgpt)和 Grok Bot 桌面版(grok-bot)。其他以 apt 打包的应用可在配置中添加。Snap、Flatpak、AppImage 和 CLI 不在范围内。

机器标签就是你在配置中给它们起的名字;下面的示例使用 desktop-a 和 laptop-b。

安全模型简述

  • 你控制的两台机器,一条可信链路。你启动运行的机器通过 ssh(BatchMode,检查主机密钥)连接到另一台:Tailscale SSH 或普通 ssh 密钥。没有这条链路时,只有 --local-only --dry-run 可用。
  • 每次运行只询问一次 sudo,并在两台机器上校验。密码只通过 stdin 传给 sudo -S(对另一台机器则通过本次运行的 ssh 会话);绝不进入 argv、环境变量、文件、日志或报告。参见 SECURITY.md。
  • apt only. Root runs only apt-get update, apt-get install of a pinned version or of a stored, sha256-checked .deb, and apt-mark hold/unhold. Each machine installs from its own configured apt sources.
  • 仅优雅关闭。使用 SIGTERM,绝不使用 SIGKILL。如果某个应用未及时关闭,本次运行会在任何更新发生之前于两台机器上中止。
  • 每次更新前先做回滚副本,并与 dpkg 安装的内容进行校验;两台机器上任一缺少经过验证的副本,该应用就会在两台机器上都被跳过。
  • 无常驻进程。没有守护进程、定时器或监听器;应用只在你启动的运行过程中关闭。
  • 它恢复的是程序文件,不是应用数据(见下文)。

一次运行会做什么

twin-update run [--apps cursor,chatgpt] [--dry-run]
  1. 两台机器上的预检:身份(whoami,可选的 machine-id)必须与配置匹配;apt/dpkg 必须空闲;每个 lock_paths 文件在整个运行期间都会被锁定,因此同步或备份任务无法在更新期间运行。只询问一次 sudo 密码并在两台机器上校验,然后在两台机器上执行 apt-get update。
  2. 计划:只有当两台机器都安装了某应用、都提供相同的候选版本,且没有保留标记阻止时,该应用才会被更新。否则它会在两台机器上都被跳过,并给出原因。确切版本会被固定。
  3. 先做回滚副本:已安装的 .deb 会以 sha256 形式存储在 ~/.local/share/twin-update/rollback/<app>/<version>/ 中,来源为 apt 缓存、apt-get download <pkg>=<installed> 或匹配的本地 .deb。它会与 dpkg 对已安装构建的 md5sums 进行校验(对于没有 md5sums 成员的厂商 deb,则以流式方式对 deb 的文件进行哈希并比较)。不匹配则拒绝。两台机器上任一缺少副本 = 该应用在两台机器上都被跳过。
  4. 保留标记:Twin Update 未设置的、针对这些应用的保留标记会被清除(每条各占一行报告),除非 keep_holds 列出了它们。其他软件包上的保留标记仅作报告。
  5. 两台机器上优雅关闭:通过 pidfd 向应用进程树的主进程发送 SIGTERM,这些进程通过可执行文件路径和软件包的文件列表找到。不强制终止。如果在 close_s 之后仍有进程在运行,本次运行会在任何更新之前于两台机器上中止,指明应用和进程,并重新打开它已关闭的内容。
  6. 更新每台机器从其各自的仓库进行:apt-get install --only-upgrade <pkg>=<version>。机器之间不复制任何内容。
  7. 验证:dpkg 版本、dpkg -V,以及在图形会话中做一次启动冒烟测试(systemd-run --user):保持运行 smoke_s 秒,journal 中无崩溃,优雅关闭。失败时你会获得回滚选项。
  8. 重新打开仅限之前处于打开状态的应用。
  9. 清理:每个应用只保留一份回滚副本(最新更新之前的版本)。旧副本仅在新版本通过检查 且 经过宽限期(7 天或 2 次成功启动)后才会被删除。
  10. 报告:在两台机器上生成 ~/.local/state/twin-update/runs/<ts>-run.json(权限 0600,仅含机器标签),外加一条桌面通知,例如 Cursor laptop-b 2.4.1→2.5.0 ✓(通过 notify-send,或在未安装 notify-send 时通过 gdbus)。

应用只会在你启动的一次运行中被关闭。没有守护进程、定时器、监视器或监听器;到另一台机器的 ssh 会话仅在运行期间存在。运行过程会将自己移入其专属的 systemd-run --user --scope,因此关闭启动它的终端所在的应用不会使其停止。

其他命令

twin-update check [--notify] [--json]      # read-only: are updates available? (no sudo)
twin-update status [--local-only]          # per machine/app: installed, candidate, running, rollback copy, hold
twin-update rollback <app> [--machine <label>|both] [--dry-run] [--no-hold]
twin-update holds
twin-update unhold <app> [--machine ...] [--dry-run]
twin-update doctor                         # identity, sudo group, apt idle, locks, session, ssh; exit 4 on problems
twin-update local --serve                  # per-machine engine; the peer runs this over ssh

每日检查(可选)

twin-update check 以非 root 身份询问两台机器,已启用的应用是否有更新版本。首先,它仅为应用自身的仓库获取最新的软件包列表到用户拥有的缓存中(~/.cache/twin-update/apt 或 $XDG_CACHE_HOME/twin-update/apt),并将 apt-get update 指向该缓存:仓库现有的 sources.list.d 条目被原样链接进来,因此其 Signed-By 密钥环和 apt 的签名检查按配置生效;系统的 apt update 钩子不会运行;/var/lib/apt 永远不会被写入。然后 apt-cache policy 读取同一缓存。如果获取失败或超时(2 分钟),则使用系统列表并说明这一点,同时告知上次刷新距今多久。--no-fetch 跳过获取。它从不请求 sudo,不关闭任何东西,不安装任何东西,也不存储回滚副本。(twin-update run 仍以 root 身份通过系统 apt 刷新和安装。)

使用 --notify 时,当有可用更新时,它会在其运行的机器上发送桌面通知;当无法联系到另一台机器或发生错误时,发送一条简短的“检查未完成”通知。同一通知每天最多发送一次(状态保存在 ~/.local/state/twin-update/ 中)。

无法访问的机器会以直白的语言报告:名称查找失败、ssh 登录被拒绝、连接被拒绝、Tailscale SSH 要求进行交互式检查,或超时(关机、休眠或离线)。

check 的退出码:0 无更新,10 有可用更新(仅检查了一台机器时也如此),1 检查未完成(出错或另一台机器无法访问,且未发现更新),2 用法或配置错误。

示例用户定时器(无需 root;在你登录时运行,或启用 lingering 后始终运行):

# ~/.config/systemd/user/twin-update-check.service
[Unit]
Description=Twin Update: daily read-only update check

[Service]
Type=oneshot
ExecStart=%h/.local/bin/twin-update check --notify
SuccessExitStatus=10
TimeoutStartSec=5min
Nice=10
IOSchedulingClass=idle

# ~/.config/systemd/user/twin-update-check.timer
[Unit]
Description=Twin Update: daily update check

[Timer]
OnCalendar=*-*-* 09:13:00
Persistent=true

[Install]
WantedBy=timers.target

systemctl --user daemon-reload && systemctl --user enable --now twin-update-check.timer

rollback 检查所存储副本的 sha256,优雅地关闭应用,使用 apt-get install --allow-downgrades ./<deb> 安装它,设置 apt-mark hold(记录为 Twin Update 自身的 hold;下一次有意执行的 run 会将其释放),进行验证,并在应用之前处于打开状态时重新打开它。

回滚恢复的是程序文件,而非应用数据。如果新版本迁移了其设置或数据库,请保留你自己的数据备份。Twin Update 从不打开、复制或修改应用数据库。

安装(每台机器)

pipx install twin-update          # or: uv tool install twin-update

或来自发行版的单文件 zipapp:

install -D -m 0755 twin-update.pyz ~/.local/bin/twin-update

在两台机器上的相同路径安装它(配置中的 remote_command 指向它)。然后从 config.example.toml 创建 ~/.config/twin-update/config.toml(权限 0600)。同一文件在两台机器上均可使用。用 twin-update doctor 检查,然后 twin-update run --dry-run。

传输

你运行的机器通过普通 ssh 访问另一台机器(BatchMode,检查主机密钥,无密码提示)。远程端在运行期间执行 twin-update local --serve。有两种选择:

Tailscale SSH。在每台应接受运行的机器上: sudo tailscale set --ssh。“Shields up”会阻止传入连接,包括 Tailscale SSH,因此在这些机器上必须关闭它;改为让策略来限制。带标签的策略片段示例(将其合并到你的策略中;给设备打标签会使其成为标签所有而非用户所有):

{
  "tagOwners": {
    "tag:twin-a": ["autogroup:admin"],
    "tag:twin-b": ["autogroup:admin"]
  },
  "grants": [
    { "src": ["tag:twin-a"], "dst": ["tag:twin-b"], "ip": ["tcp:22"] },
    { "src": ["tag:twin-b"], "dst": ["tag:twin-a"], "ip": ["tcp:22"] }
  ],
  "ssh": [
    { "action": "accept", "src": ["tag:twin-a"], "dst": ["tag:twin-b"], "users": ["alice"] },
    { "action": "accept", "src": ["tag:twin-b"], "dst": ["tag:twin-a"], "users": ["alice"] }
  ]
}

使用 "action": "accept":"check" 需要交互式浏览器登录,而 BatchMode 运行无法响应它。如果你的策略仍有允许全部规则,它也会让 tailnet 中的其他所有内容访问这些机器;请收窄它。如果你总是在同一台机器上启动运行,只保留你需要的方向。

普通 ssh 密钥。在另一台机器的 authorized_keys 中放置一个无需密码提示的专用密钥(或由你的 agent 持有的密钥),最好用 from="<the other machine's address>" 加以限制,并将其主机密钥预先放入 known_hosts。

开发

PYTHONPATH=src:tests python3 -m unittest discover -s tests
python3 tools/build_zipapp.py        # -> dist/twin-update.pyz

仅使用标准库。退出码(check 不同,见上文):0 正常 / 试运行,4 doctor 发现问题(包括无法访问的另一台机器;同步锁繁忙只是警告),1 失败,2 用法或配置,3 中止(未更改任何内容)。

许可证

Apache License 2.0。参见 LICENSE。

来源:Hacker News · github.com