在NSIS中实现安装时取消并回滚(2)——写个线程控制插件实现它
在上一篇在NSIS中实现安装时取消并回滚(1)——现状中分析了现在NSIS对取消安装并回滚功能的支持。结论是目前NSIS本身还不支持,需要自己完成这个功能。下面就来介绍一种相对比较简单的实现方式。
功能难点在上一篇中已经有描述,这里就不赘述了。基本原理就是在安装过程中,使用插件中把后台的释放文件的线程挂起,问用户是不是真要取消安装,是的话就使用安装过程中生成的Log文件进行回滚,不是的话就让被挂起的线程继续运行。
大致要解释如下几个问题
1. 编写线程控制插件。
2. 开启安装页面的取消按钮,并自定义取消函数。
3. 与安装日志功能集成,实现回滚操作。
首先是实现线程控制插件。这个插件有如下几个功能:
1. 挂起后台所有线程
2. 继续后台所有线程
3. 终止后台所有线程
为什么是后台?因为UI线程要用来弹出一个MessageBox让用户点,如果也挂起了,程序就死了。为什么是所有线程呢?因为如果有多个后台线程,如果那个线程不主动配合调查,想要找出哪个线程是用来释放文件似乎不是很容易。简单起见,就全挂起得了,好在不会有什么问题。更好的是,这个安装程序在安装页面只有两个线程,一个UI线程,一个后台释放文件。
还有一点要注意的就是TerminateThread函数是异步的。最好多调用一个WaitForSingleObject来保证函数返回时,线程的确已经被和谐了才行。(不调似乎也没有什么问题)
代码就不贴了,可以从这里下载。关于如何编写NSIS插件可以参考制作NSIS命令行窗口输出插件这篇文章。
然后就是开启安装页面的取消按钮,在上一篇在NSIS中实现安装时取消并回滚(1)——现状提到的几个帖子有示例的。代码如下:(仅适用于MUI2)
!define MUI_PAGE_CUSTOMFUNCTION_PRE OnInstFilesPagePre ;必须放在下面的语句前面
!insertmacro MUI_PAGE_INSTFILES
Function OnInstFilesPagePre
GetDlgItem $0 $HWNDPARENT 2
EnableWindow $0 1
FunctionEnd
自定义取消函数的代码如下:
!define MUI_CUSTOMFUNCTION_ABORT OnUserAbort
Function OnUserAbort
…
nsExec::ExecToLog 'cmd /c copy /Y "$INSTDIR\install.log" "$INSTDIR\temp.log"'
Call CreateLogFromFile
Call RemoveDirectoriesFromLog
…
FunctionEnd
函数中给出的代码就是实现回滚操作部分的代码。在调用回滚函数(其实就是卸载时调用的根据日志删除文件的函数)前要把install.log复制一份,然后用复制的日志文件进行回滚。为什么要这样呢?因为后台的释放文件的线程被Terminate掉了,而就是那个线程打开的日志文件并写入安装日志,线程被和谐的,那么释放文件句柄的代码就不会被执行。说白了就是资源泄露了。后果就是再次打开这个文件的时候会失败,就算是只读也不行。用NSIS脚本举个例子。
FileOpen $0 “txt.txt” “w”
FileOpen $1 “txt.txt” “r” ;失败
第一次打开写,可以正确打开,第二次打开,虽然只读也会失败。但是如果像下面这样两次都是读。两次都是可以成功的。
FileOpen $0 “txt.txt” “r”
FileOpen $1 “txt.txt” “r” ;成功
上面只列出了功能实现的比较关键的代码。完整的代码可以从这里下载。如果要正确编译成安装包,还要下载这个头文件。
到此为止,基本在NSIS中实现了取消安装并回滚。但是这种实现有下面一些入缺陷。(都快成定律了,解决一个问题,引出三个问题。)
1. 回滚时install.log文件不能被删除。因为在上面已经解释了。
2. 回滚时,滚动条不回滚。其实应该是可以实现的。大家自己试下吧,应该不难。
3. 回滚后程序直接退出,而不会显示安装失败页面之类的页面。因为我们的回滚操作就是在User Abort里做的嘛。使用User Abort的后果就是直接退出了。应该也是有方法绕过去的。