【Windows】记录一次应用安装失败的原因

461 阅读1分钟

背景

开发了一个Windows服务A,并且使用了NSIS制作了安装包,在更新了一个版本之后,突然收到了测试小伙伴的反馈,说服务A安装失败了,并且是概率性的,概率为1/10左右,异常日志为"user abort"。

分析

对于概率性的问题,最让人头疼了,这意味着你没有必现步骤,只能硬着头皮从脚本/流程上进行分析,过程如下:

  1. 这个版本对于之前的版本更新了什么?

去掉了手撸的服务A的守护,改用Windows服务自带的守护方案,在安装包的AfterInstall的模块中添加了如下代码:

        nsExec::Exec '$SYSDIR\sc failure 服务A actions=restart/0/restart/0restart/0 reset=0'

image.png

  1. 异常日志为"user abort"?
  • 删除文件失败
  • 解压文件失败

再次分析下日志,发现是在解压文件到的文件夹的时候出现了异常,再进一步分析,应该是要覆盖的文件被占用了,解压覆盖失败了。

从理论上来说,服务A在安装的是时候,会先删除对应安装目录下所有的所有文件再解压文件的,是不会出现解压出来的文件需要覆盖的情况,所以大概率的问题就在于

文件被占用了,也就是说服务A的进程一定是没有退出,还在运行

ps:因为我们服务A只是全家桶中的其中一个,全家桶安装完会重启,破坏了现场

  1. 开始分析NSIS的脚本 在覆盖安装应用程序的时候,我们NSIS的安装流程是这样子的
  • 执行beforeinstall模块
  • 执行un.beforeinstall模块
  • ...

beforeinstall模块是没有任何脚本的,所以忽略,再看看un.beforeinstall模块,代码如下:

        ExecWait '$SYSDIR\net stop "服务A"'
	ExecWait '$SYSDIR\sc delete "服务A"'

从流程上来看,是没有任何问题的,停止了服务,并且Windows自带的守护程序也不会重启服务,再加上已删除了服务,接下来进行文件夹的删除以及解压释放文件了

  1. 迷茫,问题在哪?

由于我们公司的NSIS脚本还有一个基座部分,我们仓库只是业务层的代码,所以决定去基座仓库看下代码,看了半个小时,也没看到个什么,只知道基座只会在执行un.beforeinstall前去杀掉安装目录下所有应用程序,当然也包括服务A,当然,这个没问题。

  1. 干想也没用,写个模拟程序实践下

我用C#(我擅长的语言)写了一个模拟了下NSIS的安装流程的应用程序,运行了几次,突然报错了,报错的原因竟然是在sc stop "服务A"这条命令,重新整理了下思路:

(1)杀进程
(2)守护重启(杀进程是异常行为,Windows服务自带的守护会触发)
(3)执行sc stop异常,ExceptionMessage为服务状态为启动中,执行sc stop失败
(4)执行sc delete,将服务标记为已删除

问题已经很明显了,出现安装失败的原因就是在服务A进程被杀掉之后,守护又重新启动它,此时服务处于启动中的状态,sc stop是无效的,这时候服务A又重新运行了,此时删除服务A的文件失败了,解压覆盖也失败了,安装也就异常了。

  1. 非常简单的问题,该如何修改呢? 因为不想动基座的代码,所以我这边的修改方案是:
  • 在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+次,未再出现过问题,此问题终于完美解决。

总结

  1. 要看完整的代码,有时候你引用了CBB,这时候最好也能看CBB的代码,完整弄明白主流程
  2. 如果你遇到的语言不熟悉,问题不好定位,可以翻译为你擅长的语言去定位问题
  3. 要考虑跟旧版本的兼容