背景
开发了一个Windows服务A,并且使用了NSIS制作了安装包,在更新了一个版本之后,突然收到了测试小伙伴的反馈,说服务A安装失败了,并且是概率性的,概率为1/10左右,异常日志为"user abort"。
分析
对于概率性的问题,最让人头疼了,这意味着你没有必现步骤,只能硬着头皮从脚本/流程上进行分析,过程如下:
- 这个版本对于之前的版本更新了什么?
去掉了手撸的服务A的守护,改用Windows服务自带的守护方案,在安装包的AfterInstall的模块中添加了如下代码:
nsExec::Exec '$SYSDIR\sc failure 服务A actions=restart/0/restart/0restart/0 reset=0'
- 异常日志为"user abort"?
- 删除文件失败
- 解压文件失败
再次分析下日志,发现是在解压文件到的文件夹的时候出现了异常,再进一步分析,应该是要覆盖的文件被占用了,解压覆盖失败了。
从理论上来说,服务A在安装的是时候,会先删除对应安装目录下所有的所有文件再解压文件的,是不会出现解压出来的文件需要覆盖的情况,所以大概率的问题就在于
文件被占用了,也就是说服务A的进程一定是没有退出,还在运行
ps:因为我们服务A只是全家桶中的其中一个,全家桶安装完会重启,破坏了现场
- 开始分析NSIS的脚本 在覆盖安装应用程序的时候,我们NSIS的安装流程是这样子的
- 执行beforeinstall模块
- 执行un.beforeinstall模块
- ...
beforeinstall模块是没有任何脚本的,所以忽略,再看看un.beforeinstall模块,代码如下:
ExecWait '$SYSDIR\net stop "服务A"'
ExecWait '$SYSDIR\sc delete "服务A"'
从流程上来看,是没有任何问题的,停止了服务,并且Windows自带的守护程序也不会重启服务,再加上已删除了服务,接下来进行文件夹的删除以及解压释放文件了
- 迷茫,问题在哪?
由于我们公司的NSIS脚本还有一个基座部分,我们仓库只是业务层的代码,所以决定去基座仓库看下代码,看了半个小时,也没看到个什么,只知道基座只会在执行un.beforeinstall前去杀掉安装目录下所有应用程序,当然也包括服务A,当然,这个没问题。
- 干想也没用,写个模拟程序实践下
我用C#(我擅长的语言)写了一个模拟了下NSIS的安装流程的应用程序,运行了几次,突然报错了,报错的原因竟然是在sc stop "服务A"这条命令,重新整理了下思路:
(1)杀进程
(2)守护重启(杀进程是异常行为,Windows服务自带的守护会触发)
(3)执行sc stop异常,ExceptionMessage为服务状态为启动中,执行sc stop失败
(4)执行sc delete,将服务标记为已删除
问题已经很明显了,出现安装失败的原因就是在服务A进程被杀掉之后,守护又重新启动它,此时服务处于启动中的状态,sc stop是无效的,这时候服务A又重新运行了,此时删除服务A的文件失败了,解压覆盖也失败了,安装也就异常了。
- 非常简单的问题,该如何修改呢? 因为不想动基座的代码,所以我这边的修改方案是:
- 在un.beforeinstall模块中的第一行添加取消守护的操作(我感觉可以不要)
ExecWait '$SYSDIR\sc failure 服务A reset= 0 actions= //////'
- 在un.beforeinstall模块的最后异常添加再一次执行杀掉安装目录下的所有应用程序(这一步才是最关键的)
${ProcessKiller::KillProcessesByDirectory} "服务A的安装目录"
7. 仅仅只改un.beforeintsall是不够的,因为旧版本的服务A已经发布了,改不了脚本,所以无论新版本怎么改,还是会出现一样的问题,所以还必须在beforeinstall做一些兼容的操作,才能彻底解决问题
ExecWait '$SYSDIR\sc failure 服务A reset= 0 actions= //////'
ExecWait '$SYSDIR\net stop "服务A"'
ExecWait '$SYSDIR\sc delete "服务A"'
${ProcessKiller::KillProcessesByDirectory} "服务A的安装目录" # 关闭可能正在运行的进程
8. 验证 压测1200+次,未再出现过问题,此问题终于完美解决。
总结
- 要看完整的代码,有时候你引用了CBB,这时候最好也能看CBB的代码,完整弄明白主流程
- 如果你遇到的语言不熟悉,问题不好定位,可以翻译为你擅长的语言去定位问题
- 要考虑跟旧版本的兼容