「装完更新,桌面没了。」

用户原话就这一句,后面跟了张截图:全黑,中间一个白色鼠标箭头。不是花屏,任务管理器还能按出来,explorer.exe 那一栏是空的。

这是七亿压缩 0.34 那阵子的事。当时更新包刚改成覆盖安装,装完自动重启一次,我觉得这步没什么技术含量,结果第一批升级的用户就撞上了。七亿压缩跟另一条线——七亿批量远程管理工具——都在维护 Windows 客户端,会话这块的坑,两个产品其实共用一套经验。

桌面黑、鼠标在、资源管理器进程没了——这个组合基本不用猜,外壳进程被杀了,没人把它拉回来。让用户手动在任务管理器里起一个 explorer.exe,桌面立刻恢复,说明显示、用户会话、文件系统都正常,问题只出在「谁负责把 shell 拉起来」这一环。

回头看安装脚本,收尾就三行:

taskkill /F /IM explorer.exe
(拷贝、替换文件)
Exec explorer.exe

写的时候觉得挺顺:先把桌面进程杀掉,免得 dll 被占用替换不了;文件拷完再起一个新的,用户几乎无感。实际跑起来,这两句发生在两个不同的会话里。

taskkill /F /IM explorer.exe 没有限定会话,/IM 是按镜像名匹配,机器上所有会话里的 explorer 一起被杀。安装程序自己呢,多半是被更新器、服务或者计划任务拉起来的,跑在它自己的会话中。后面那句 Exec explorer.exe,新进程就生在安装程序所在的会话,用户面前那个交互桌面里,一个 explorer 都没有。

所以现场是:文件其实装好了,版本号也是新的,但桌面没人管。远程连进去更别扭,用过七亿批量远程管理工具的人大概都体会过:黑屏看得见,鼠标点不了几个地方,只能靠任务管理器摸。

资源管理器.png

修法不复杂,但顺序有讲究。不再用 Exec,改成先建一个计划任务:

schtasks /create /IT ...

/IT 是让任务在登录用户的交互会话里跑的意思。任务建好,再由它去把 explorer 拉起来,拉回之后把任务删掉——不删的话,下次登录还会再跑一遍。

关键不在这条命令,在它对失败的处理:建不成就坚决不杀。脚本里先执行 schtasks,检查返回码,任务没建成,直接退出安装流程。宁可这次更新装不上,也不能把人家桌面弄黑。

这个顺序当时还争过。先杀后建写起来顺,代码也短;先建后杀多一层判断,还得处理建任务失败的分支。争到最后统一了:杀桌面是破坏性操作,动手之前必须先准备好怎么活回来。这句后来写进了内部约定,凡是动 explorer、动 shell 的地方都照这个来。

从七亿压缩 0.34 初版一直改到 0.34-11,才把这套东西收干净。中间补的也是这类边角:任务名要能区分版本,拉回成功后清理,安装程序异常退出时别把任务留在系统里。

explorer 这进程我们不是第一次栽。更早那回是在 Win11 上,装完资源管理器反复崩,我前后猜错两次根因,最后靠事件查看器里的故障模块名才定位到 twinui.pcshell.dll,那次的弯路单独写过一篇。这东西跟七亿批量远程管理工具里那些远程会话问题是一个祖宗:Windows 的会话边界是隐形的,代码里看不出来,只有真机上才给你一巴掌。

现在再写安装器,我的习惯是先问一句:这一步失败,用户的机器还能不能自己回到可用状态?回不去,就别做。

当初写那句 Exec explorer.exe 的时候,我大概觉得进程名一样就是同一个东西吧。