SDK安装程序为啥不动
今天又遇到SDK安装程序失败,停在Download状态长时间不动,倒一杯茶回来还是不动,开了一个小时的会回来也还是不动。安装程序出问题是常用的事,没有什么吸引力让我动调试器。但是杀掉再来一次,又是停在这个地方不动。是可忍孰不可忍,只好上调试器了。
时间关系,长话短说,是9号线程阻碍了“大军”的进程,加载了sos扩展,栈回溯如下:
0:009> !clrstack
OS Thread Id: 0x14d8 (9)
Child-SP RetAddr Call Site
000000001efbb560 000007ff002f4c2d SDKSetup.DownloadUtil.Dispose(Boolean)
000000001efbb590 000007fef950cb61 SDKSetup.DownloadThread.DownloadThreadProc()
000000001efbe5d0 000007fef916bc0c System.IO.__Error.WinIOError(Int32, System.String)
000000001efbe630 000007ff001b867e System.IO.FileInfo.get_Length()
000000001efbe660 000007ff001b82ac SDKSetup.DownloadUtil.DownloadFileAsyncUsingAsyncFileIO(System.String, System.String, Boolean)
000000001efbe6d0 000007ff001b7fe8 SDKSetup.DownloadUtil.DownloadFileAsync(System.Uri, System.Uri)
000000001efbe730 000007ff002f4a1d SDKSetup.DownloadUtil.DownloadFile(System.Uri, System.Uri)
000000001efbe770 000007fef8752bbb SDKSetup.DownloadThread.DownloadThreadProc()
000000001efbe830 000007fef87ea8dd System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
000000001efbe880 000007fef960d4a2 System.Threading.ThreadHelper.ThreadStart()
看来是负责下载文件的线程出了个异常,是调用取文件长度的方法get_Length时触发的异常,估计是文件根本不存在就去取长度,看一下当前线程的Last Error,果然如此:
0:009> !gle
LastErrorValue: (Win32) 0x3 (3) - The system cannot find the path specified.
LastStatusValue: (NTSTATUS) 0xc000003a - {Path Not Found} The path %hs does not exist.
观察一下SDKSetup.DownloadThread对象的属性,可以看到当前下载任务的目标是:
C:\Users\...\AppData\Local\Temp\SDKSetup\WinSDK\WinSDKInterop_ia64\cab1.cab
下载的来源是:
\\<网络目录>\Win7SDK\GRMSDKX_EN_DVD_7_0_64bit\Setup\WinSDKInterop_ia64\cab1.cab
因为我是从一个网络共享目录安装,其实就是从网络目录下载到本机的临时目录。这么简单点事怎么出异常了呢?软件的根本在代码,看代码吧!
IL_0059: ldarg.0
IL_005a: ldfld class [System]System.Uri SDKSetup.DownloadThread::m_SourceFile
IL_005f: ldarg.0
IL_0060: ldfld class [System]System.Uri SDKSetup.DownloadThread::m_TargetFile
IL_0065: callvirt instance void SDKSetup.DownloadUtil::DownloadFile(class [System]System.Uri,
class [System]System.Uri)
IL_006a: ldloc.0
IL_006b: callvirt instance float64 SDKSetup.DownloadUtil::get_DownloadSpeedBytesPerMilliSec()
IL_0070: pop
IL_0071: ldarg.0
IL_0072: ldfld class [System]System.Uri SDKSetup.DownloadThread::m_TargetFile
IL_0077: callvirt instance string [System]System.Uri::get_OriginalString()
IL_007c: newobj instance void [mscorlib]System.IO.FileInfo::.ctor(string)
IL_0081: stloc.1
IL_0082: ldarg.0
IL_0083: ldloc.1
IL_0084: callvirt instance int64 [mscorlib]System.IO.FileInfo::get_Length()
IL_0089: stfld int64 SDKSetup.DownloadThread::m_FileSize
看惯了x86汇编后,看这个字节码(MSIL)多少会有些不习惯,但是大意还是很容易理解的。
先是加载参数,要下载的源和目标,然后调用SDKSetup.DownloadUtil实例的DownloadFile方法。看一下后者的代码,它又调用SDKSetup.DownloadUtil::DownloadFileAsync,开始异步下载并等待如下事件:
SDKSetup.DownloadUtil::m_DownloadFileSynchronousCompletedEvent
0:009> !handle 7a8
Handle 7a8
Type Event
从上面的栈来看,还没有执行到等待的地方,因为DownloadFileAsync还没有返回,它继续调用了DownloadFileAsyncUsingAsyncFileIO,观察DownloadFileAsyncUsingAsyncFileIO的代码,它真的调用了get_Length方法:
IL_0071: ldarg.0
IL_0072: ldfld class [System]System.Uri SDKSetup.DownloadThread::m_TargetFile
IL_0077: callvirt instance string [System]System.Uri::get_OriginalString()
IL_007c: newobj instance void [mscorlib]System.IO.FileInfo::.ctor(string)
IL_0081: stloc.1
IL_0082: ldarg.0
IL_0083: ldloc.1
IL_0084: callvirt instance int64 [mscorlib]System.IO.FileInfo::get_Length()
IL_0089: stfld int64 SDKSetup.DownloadThread::m_FileSize
IL_008e: ldnull
从上面的调用关系可以看到,下载线程检测出了安装源是文件共享,因此启用了异步文件IO下载方法,如果是网络那么会DownloadFileAsyncUsingWebClient。在文件下载方法DownloadFileAsyncUsingAsyncFileIO中,先是调用get_Length取要下载文件的长度,这很对啊,没有问题啊!
Dump一下System.IO.FileInfo对象的属性,当时的文件名的确也是对的:
\\<网络目录>\Win7SDK\GRMSDKX_EN_DVD_7_0_64bit\Setup\WinSDKInterop_ia64\cab1.cab
但是再多看一眼,还是有点不顺眼,我选择的是64为安装包,我的机器当然是x64,不是安腾64,但是这里的确是准备下载安腾64的文件包,不过多安装一点也是常用的事,但是查一下远程的安装目录,哦,真的没有这个文件夹。
哦,原来要下载的这个文件真的不存在!看来说安装程序不好,还有点冤枉它么!
但是话又说回来,文件不存在也不该挂死在这啊,弹出个错误对话框也好啊!为什么不弹出来呢?那就要看get_Length触发异常后,如何处理这个炸弹了!
反向追索,寻找异常处理器,首先DownloadFileAsyncUsingAsyncFileIO没有异常捕捉器,它的父函数DownloadFileAsync也没有,DownloadFileAsync的父函数DownloadFile也没有,直到DownloadThreadProc才有异常捕捉函数,而且有三层,起作用的应该是最内层的,即如下这个:
finally
{
IL_00fe: ldloc.0
IL_00ff: brfalse.s IL_0109
IL_0101: ldloc.0
IL_0102: callvirt instance void SDKSetup.DownloadUtil::Dispose()
IL_0107: ldnull
IL_0108: stloc.0
IL_0109: endfinally
} // end handler
IL_010a: leave.s IL_0168、
没有catch,只有finally,上层有catch,因此逻辑是要先执行这个finally,这个finally中真的调用了SDKSetup.DownloadUtil::Dispose,析构SDKSetup.DownloadUtil对象,这也正是写这个finally的目的。在上面的栈回溯中,也是在执行这个Dispose方法。
看来Dispose出问题了,再看一下它的代码吧,整个方法并不长,因此完全复制过来:
.method family hidebysig instance void Dispose(bool disposing) cil managed
{
// Code size 57 (0x39)
.maxstack 8
IL_0000: ldarg.1
IL_0001: brtrue.s IL_0004
IL_0003: ret
IL_0004: ldarg.0
IL_0005: call instance void SDKSetup.DownloadUtil::CancelAsync()
IL_000a: br.s IL_0016
IL_000c: ldc.i4 0x1f4
IL_0011: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_0016: ldarg.0
IL_0017: call instance bool SDKSetup.DownloadUtil::get_IsBusy()
IL_001c: brtrue.s IL_000c
IL_001e: ldarg.0
IL_001f: call instance bool SDKSetup.DownloadUtil::get_CancelPending()
IL_0024: brtrue.s IL_000c
IL_0026: ldarg.0
IL_0027: call instance void SDKSetup.DownloadUtil::DisposeWebClient()
IL_002c: ldarg.0
IL_002d: call instance void SDKSetup.DownloadUtil::DisposeAsyncFileIO()
IL_0032: ldarg.0
IL_0033: call instance void SDKSetup.DownloadUtil::DisposeDownloadFileSynchronousCompletedEvent()
IL_0038: ret
} // end of method DownloadUtil::Dispose
首先,真的调用了Sleep,其次真的有一个循环,当SDKSetup.DownloadUtil::get_IsBusy返回真时,就一直循环,反复调用Sleep...、
观察DownloadUtil的m_isBusy成员,真的为1:
000007fef889afa0 40003f9 70 System.Int64 1 instance 1 m_isBusy
粗略想来,是在忙啊,不是在复制文件么,谁想到因为取文件长度而触发异常飞出来了呢?飞出来是没来得及改写Busy标志啊,没想到这个Busy标志不清,整个对象都析构不了,连整个安装程序都停止不动了!