Electron 升级后热补丁为何失效 双安装致通道分裂

4 阅读5分钟

把一只 Electron 应用当成"一个装在机器上的程序"来管理,是很多自动化脚本踩坑的起点。真实场景里,同一台机器上常常同时躺着两套安装:一套是你手动解包、亲手打过热补丁的工作目录,另一套是官方升级后落进 Program Files 的标准安装。补丁只活在旧的那一套里,可你的发布通道却可能悄悄调起了新的那一套——于是"补丁明明打进了文件,行为却还是老样子"。

现象:补丁进文件了,发布却没变

最典型的表现是:你改了 resources/app/dist/electron/main.js,把某段发布逻辑从"弹窗确认"改成"自动确认",本地验证也过了。过几天应用自动升级,再跑自动化,发现那个被修掉的弹窗又回来了,日志里重新出现"点击确认后发布弹窗仍未关闭"。第一反应往往是"补丁被冲掉了",这个判断只对了一半。

真正麻烦的是:升级后两台并存,旧的 unpacked 目录还在,新的 Program Files 安装也在。你以为自己在调旧的那套(带补丁),实际 CI 或计划任务里写死的路径、或者某个包装脚本的分支,已经指向了新的、只有 app.asar 的那套。补丁从未生效于你真正调用的二进制。

根因:机器上其实有两套 MatrixMedia

resources/app 之所以能覆盖 app.asar,是因为 Electron 的加载优先级:resources/app/ 目录下的代码先于 app.asar 被读取。这套机制只对你手动解包的那份生效。官方升级走的是标准安装流程,它往 resources\ 下只放一个 app.asar,你之前所有对 main.js 的手写改动,在升级那一刻就被干净地覆盖掉了。

后果有两个层面:

安装 A(工作目录,带补丁)
  build\win-unpacked\
    resources\
      app\dist\electron\main.js   ← 你改过,补丁在这里
      app.asar
安装 B(Program Files,官方标准)
  resources\
    app.asar                     ← 只有它,补丁被升级冲掉

注意第二套里根本没有 app/ 目录,所以"解包后改 main.js"这条路在第二套上压根走不通,必须先解包才能补,而解包本身又会引入新的路径。

通道分裂:你调用的二进制不是被打补丁那套

更隐蔽的是"通道分裂"。同一个发布动作,可以走 HTTP API,也可以走 CLI(直调 matrixmedia.exe)。如果这两条通道解析到的 exe 不是同一个,就会出现"HTTP 通道修好了、CLI 通道还是旧的",或者反过来。某次视频号补发就踩到这个:会话分裂导致 HTTP 通道挂起,只能退回 CLI 通道,而 CLI 指向的恰好是没打补丁的那套,于是行为对不上。

判别键很简单,但很多人懒得查:

# 看计划任务 / 包装脚本实际解析到哪个 exe
(Get-Command matrixmedia.exe).Source
# 或者直接打印包装脚本里的 EXE 变量
Select-String -Path "mmcli_env.cmd" -Pattern "set \"EXE="

如果打出的路径和你打补丁的 build\win-unpacked 不是同一个目录,那就是通道分裂。

怎么排查:先确认自动化到底在调谁

别靠记忆。把"自动化引用的 exe"和"带补丁的 exe"做一次哈希比对:

# 两个安装目录的 main.js 是否一致
sha256sum \
  "/c/Users/molan/WorkBuddy/2026-08-28-23-12-48/MatrixMedia/build/win-unpacked/resources/app/dist/electron/main.js" \
  "/c/Program Files/MatrixMedia/resources/app.asar"

app.asar 是归档不是明文,比不了哈希,但思路一致:解包后比 main.js 的关键补丁锚点字符串是否还在。也可以用 Python 直接 grep 补丁里的唯一锚点(例如你加的那行日志 console.log("准备确认发布",m)),能命中说明这套二进制带补丁,命中不了说明这套是干净的。

import subprocess
anchor = '准备确认发布'
# 对每个疑似安装目录的 main.js 检查锚点
for p in candidate_main_js_paths:
    txt = open(p, encoding='utf-8', errors='ignore').read()
    print(p, 'PATCHED' if anchor in txt else 'CLEAN')

修复:把补丁部署到两个位置,或钉死一个

有两个务实出路,按成本从低到高:

  1. 钉死一个安装。让所有发布通道(HTTP、CLI、包装脚本)都解析到同一个 build\win-unpacked 目录,并且关掉官方自动升级,避免它再产出一个没补丁的副本。代价是失去升级带来的修复,但对"生产流水线"来说可控性更重要。
  2. 补丁脚本化,两边同时打。把补丁逻辑本身存成脚本(而不是只改文件),每次确认安装路径后跑一遍。升级后新目录解包、打补丁、再验证锚点存在,一步不漏。
  3. Program Files 那份也补。需要提权(UAC)解包 app.asar 再写回,未完成前这套始终落后,调用它就会回退到老行为。

关键不是"打补丁"这个动作,而是让"被调用的二进制"和"被打补丁的二进制"是同一个。二者不一致,补丁等于没打。

给自动化的三条铁律

  • 路径要写死且定期复核:安装目录随升级变,写死的路径会静默失效。每次发布前用上面的锚点检查确认调用的是带补丁那套。
  • 补丁存脚本不存文件:文件会被升级覆盖,脚本重跑就能恢复。把"此处有临时补丁、升级后重打"也记进台账。
  • 多通道先对齐二进制:HTTP 和 CLI 若解析到不同 exe,修一处等于没修。发布前统一出口,再谈修逻辑。

双安装不是特例,而是 Electron 应用"解包覆盖 + 官方升级"这对机制共存的必然产物。把它当成流水线的常态去设计,比每次踩坑再补救省心得多。