Others

Windows 上名为 Updater 的程序,普通用户一启动就报 740

1710 字约 6 分钟

摘要测试开源 Electron 应用的开发版时,Windows 虚拟机里普通用户启动会报 EACCES,管理员却正常。同一个 EXE 换个文件名就能启动,最后发现缺少一份声明执行级别的 manifest。顺带记下提 PR 之前没查重,结果维护者早提了同样的修复。

我参与的那个开源 Electron 桌面应用发布了一个开发版,需要有人先在两个平台上测一遍,通过了才会纳入自动更新,这个任务落到了我头上。我的电脑是 Mac,Windows 这边用的是 Parallels 里的 Windows 11 ARM64 虚拟机,通过 x64 兼容层运行 Windows 的 x64 安装包。

Mac 上的功能基本都通过了,但到了 Windows 上,应用根本启动不起来。

普通用户启动不了

启动之后弹出一个错误页,标题是“更新组件无法运行”,展开技术详情是这样的:

spawn ...\EduWork-Updater.exe EACCES

页面给了三条建议:查看安全软件的拦截记录;检查目录和文件的读写执行权限;重新下载完整安装包。页面上还有一个“修复后重试”按钮。

助手逐条核对了这些建议:Windows Defender 没有任何拦截记录;文件存在,目录可写;压缩包和更新组件的校验值都与发行包一致。至于“修复后重试”,它读了应用源码,这个按钮只会重启客户端,没有任何修复动作。所以这四件事都帮不上忙。

返回 740,说明需要提权

EACCES 在 Windows 上说得很模糊。助手改用 Parallels 的命令行,直接以普通用户的身份去创建这个更新器的进程,返回的是 Win32 错误 740,意思是“请求的操作需要提升权限”。

这里有一个我差点漏掉的坑。prlctl exec 默认是以 SYSTEM 身份执行命令的,不代表登录的普通用户。助手最初有一次没有加参数,测的其实是 SYSTEM 的结果,后来发现并纠正了,之后所有的测试都带上 --current-user。

接着做了一组对照实验。在同一个目录里,给这个 EXE 建几个 NTFS 硬链接,内容、权限和数据流完全相同,只有文件名不一样:

文件名以普通用户创建进程
EduWork-Updater.exe740
Test-Updater.exe740
EduWorkLinkProbe.exe成功创建进程

同一份内容,名字里带 Updater 就需要提权,换个中性的名字就不需要。

为什么文件名会影响权限

Windows 对没有声明“需要什么权限”的老式程序,会按文件名等特征猜测它是不是安装或更新程序,猜中了就要求提权,这套机制叫“安装程序检测”。按我的理解,名字里带有 Setup、Install、Update 这类字样的,最容易被猜中。

一个程序只要自带一份 manifest,明确写上“按调用者的权限运行”,Windows 就不会再猜。

助手继续检查虚拟机和 EXE 本身:虚拟机里 EnableLUA 和 EnableInstallerDetection 都是 1;这个更新器是用 Go 编译出来的,构建脚本里没有嵌入任何 manifest,EXE 的资源目录是空的,旁边也没有外置的 manifest。线索都对得上,但它仍把结论写成“高度怀疑,待修复后用原名组件复测确认”,因为中性名字的对照实验只验证了进程能被创建,不等于更新功能是好的。

补上 manifest

修法是给更新器嵌入一份声明 asInvoker 的 manifest,也就是明确告诉 Windows 按当前用户的权限运行。助手在构建脚本里 Go 编译完成后加了一步,嵌入 manifest,并读回来确认已经写进去。

验证分了三层:

  • 拿原来的更新器副本,在同一个普通用户令牌下,嵌入前创建进程返回 740,嵌入后可以创建。
  • 用分支源码重新交叉编译一份,重复上面的测试,结果一样。
  • 把新的更新器放进完整的安装包,替换掉原来的那个,在虚拟机的全新目录里以普通用户启动。更新器进程和主界面都出现了,版本检查读到了预期的客户端版本,正常退出后再启动一次也没有问题。

这时才算确认,缺少 manifest 加上带 Updater 的文件名,就是普通用户无法启动的原因。

先用管理员身份探索

在修复包出来之前,Windows 的其他功能还是要测。我们试过用管理员身份启动同一个原包,更新器正常运行,也进入了主界面。这里要注意一个细节:这个办法只能用来“探索功能”,它不能算这个版本在 Windows 上通过了验收。所以之后管理员身份下跑通的所有项目,都在记录里标注成“管理员环境”,和普通用户的结果分开。

验收的边界

这个虚拟机是 ARM64,运行的是 x64 兼容层。普通用户启动通过,不能写成“原生 Windows x64 也通过”。真实的在线更新也没有测。这两项在提交的说明里都明确写成了待测。

提 PR 之前没有查重

到这里,我让助手按流程提 PR:问题已经有了 Issue,这次改动单独开一个分支,创建了 Draft PR。随后,我在 PR 列表里看到,比我们更早已经有两个标题几乎一样的 PR,分别对应我们提的两个。

助手核对之后确认,那两个 PR 早了大约 22 分钟和 12 分钟,针对同一个问题,目前还没合并,而且包含更多的打包校验。助手也承认,在它建议我把 PR 转为正式审阅时,漏查了已有的 PR,这是它的失误。

处理方式是:把我们在真实客户端上的验收结果作为评论补到对方的 PR 里,同时明确说明我们测试的是自己构造的本地候选包,不能算作对方分支的构建产物通过了验收,然后把我们的 PR 标为重复并关闭。

这次的验收证据没有浪费,但重复的实现是白做了。后来我让助手把整个流程复盘成一个本机的 Skill,里面加了一个只读的查重脚本,在提 Issue、创建 PR 和转正式审阅之前分别运行,列出关联的 PR、关键词相近的候选和最近打开的 PR。我们用这次的两个问题测试了脚本,它能找出当时漏看的那两个。

小结

这次排查有三点可以带走。第一,同一份文件内容、不同文件名的对照实验,比阅读文档更快地定位了原因。第二,用命令行控制虚拟机时,要先弄清命令是以谁的身份在运行。第三,提 PR 之前先看一眼别人有没有提过,花一分钟,能省掉一晚上的重复工作。

评论