摘 要 本文讨论了程序员在软件开发应用及维护过程中对多客户端软件升级的困扰,提出软件自身强制自动升级的的基本思路,并提供在VB6.0环境下实现的演示程序。
关键词 自动升级,批处理,API
一、 前言
笔者多年来一直从事企业信息化的软件开发,对于客户/服务器体系结构(Client/Server Architecture)的应用软件在分发、维护、更新等日常工作过程中有较深的体会,尤其是客户端数量超出20个,应用软件处于不成熟的阶段或者应用程序的运行环境不断变化的情况下,软件的分发、维护、更新、同步将成为应用软件能否正常运行的致命问题。通过参考网上大量有关自动升级的资料和自身深入的探索,针对该问题笔者提出了比较完善的应用软件自动升级方法。
二、思路
实现客户端相关文件的自动更新有多种方法,常见的方法是通过人工的或智能的手段通知客户端有新的软件版本和更新方法,软件使用人员手动从网上下载必要的文件并更新;或者在应用软件的菜单条目中增加自动升级的功能,让使用人员控制软件升级的频率和时机。这些方法在特定条件下应当有它存在的意义和优势,但对那些与数据库有关的管理程序而言,考虑到多客户数据处理的同步与协调问题,如果软件的更新由使用人员掌握,很难保证版本之间数据处理的一致性,并且往往使得开发人员为了考虑前后程序的兼容性束缚了设计思路。因此,本文实现的软件自动升级方法,其中有一个重要的目标是保证软件强制性的升级,而且考虑到使用人员计算机应用能力的参差不齐,力求达到升级过程的无缝过渡。也就是说,使用人员在程序启动的时候,由程序本身检测是否有新的版本需要更新,并在用户完全不知情的情况下替换相关文件,最后又自动重新运行新的主程序。对用户来说,软件升级的过程是程序在后台自动、平滑地实现的,根本不需要关心软件的升级问题,只要放心地使用,完全由开发人员掌握软件的分发、维护、更新、同步等问题。为实现这一关键目标,设计思路应主要包括以下几点:
1.在服务器或网站上放置更新文件和包含执行程序和动态库的版本信息以及需要更新的文件清单等配置文件。
2.主程序执行文件和动态库在编译生成的时候应该包含版本信息。
3.程序启动阶段首先通过API函数得到现有执行文件或动态库的版本信息,然后下载服务器上配置文件,与配置文件包含的最新版本进行比较,如果检测到有新版本发布,继续升级过程,否则正常运行。
4.根据配置文件的信息和版本比较结果,下载需要更新的文件,对于目前正在使用的文件,先保存到临时文件,对于诸如模板、数据等当前不锁定的文件而言,只需要直接下载就可。
5.考虑到部分文件当前正在使用而被锁定的情况,主程序在结束自身运行前,需要生成一个批处理文件并运行,该批处理文件需要实现的功能是,删除先前被锁定的文件,并把已经下载存到临时文件名的更新文件按照原来的文件名复制或改名。
6.批处理文件在结束运行前,应当启动运行更新后的主程序,并删除自身。
三、关键代码及API函数
1.配置文件示例:MainPrg.ini
[ExeFile]
MainPrg.exe=1.16
[OtherFile]
Template1.XLT=Yes
Template2.XLT=Yes
说明:此文件记录客户端应用程序的清单,如主程序、动态链接库、报表模板等。其中[ExeFile]段放置客户端运行阶段会被锁定的更新程序,如主程序、动态链接库等,[OtherFile]段放置只需要直接下载的更新文件,如常见的报表模板等;其中等于左边的是更新文件名;等于右边的数字1.16是更新文件的版本号,必须与实际文件的内部版本号一致,否则自动升级会成为死循环;等于右边的Yes是表示该文件需要更新,NO表示不需要更新。
2.提取EXE文件或动态链接库的文件版本信息
该用户自定义函数主要利用了三个API函数。
`从支持版本标记的一个模块里获取文件版本信息,各参数描述如下: Private Declare Function GetFileVersionInfo Lib "Version.dll" _
Alias "GetFileVersionInfoA" _
(ByVal lptstrFilename As String, _ `欲从中载入版本信息的一个文件的名字
ByVal dwhandle As Long, _ `未用
ByVal dwlen As Long, _ `指定由参数lpdata指向的缓冲区大小
lpData As Any) As Long `指向接收文件版本信息的缓冲区指针
`确定操作系统是否能得到解决一个指定文件的版本信息,如文件不包含版本信息,则返回一个0值 Private Declare Function GetFileVersionInfoSize Lib "Version.dll" _
Alias "GetFileVersionInfoSizeA" _
(ByVal lptstrFilename As String, _ `指定特定的文件
lpdwHandle As Long) As Long `变量指针,此函数充其为0
`将内存块从一个地方移到另一个地方
Private Declare Sub MoveMemory Lib "kernel32" _
Alias "RtlMoveMemory" _
(dest As Any, _ `指向目的内存的起始地址的指针
ByVal Source As Long, _ `指向待移动的内存块的起始地址的指针
ByVal Length As Long) `设置所移动内存块的大小
`复制一个字符串到缓冲区
Private Declare Function lstrcpy Lib "kernel32" _
Alias "lstrcpyA" _
(ByVal lpString1 As String, _ `指向接收由参数lpstring2指向字符串内容的缓冲区
ByVal lpString2 As Long) As Long `指向待复制的以NULL为终止的字符串
`用来从指定的版本信息资源中获取指定版本信息
Private Declare Function VerQueryValue Lib "Version.dll" _
Alias "VerQueryValueA" _
(pBlock As Any, _ `存放版本资源的缓冲区
ByVal lpSubBlock As String, _ `期望获取的值
lplpBuffer As Any, _ `指向存放 版本值缓冲区的指针
puLen As Long) As Long `版本信息长度
`本用户自定义函数用来取得Exe、Dll文件的版本号
` FullFileName执行文件或动态库的全文件名
Public Function GetMyFileVersion(FullFileName As String) As String
Dim Buffer As String
Dim rc As Long
Buffer = String(255, 0)
Dim lBufferLen As Long, lDummy As Long
lBufferLen = GetFileVersionInfoSize(FullFileName, lDummy)
If lBufferLen < 1 Then
GetMyFileVersion = ""
`MsgBox "No Version Info available!"
Exit Function
End If
Dim sBuffer() As Byte
ReDim sBuffer(lBufferLen)
rc = GetFileVersionInfo(FullFileName, _
0&, _
lBufferLen, _
sBuffer(0))
If rc = 0 Then
GetMyFileVersion = ""
`MsgBox "GetFileVersionInfo failed."
Exit Function
End If
Dim lVerPointer As Long
rc = VerQueryValue(sBuffer(0), _
"\VarFileInfo\Translation", _
lVerPointer, _
lBufferLen)
If rc = 0 Then
GetMyFileVersion = ""
`MsgBox "VerQueryValue 函数调用失败."
Exit Function
End If
Dim bytebuffer(255) As Byte
MoveMemory bytebuffer(0), lVerPointer, lBufferLen
Dim Lang_Charset_String As String
Dim HexNumber As Long
HexNumber = bytebuffer(2) + bytebuffer(3) * &H100 + _
bytebuffer(0) * &H10000 + bytebuffer(1) * &H1000000
Lang_Charset_String = Hex(HexNumber)
Do While Len(Lang_Charset_String) < 8
Lang_Charset_String = "0" & Lang_Charset_String
Loop
Dim strVersionInfo As String
strVersionInfo = "FileVersion"
Dim strTemp As String
Buffer = String(255, 0)
strTemp = "\StringFileInfo\" & Lang_Charset_String & "\" & strVersionInfo
rc = VerQueryValue(sBuffer(0), strTemp, _
lVerPointer, lBufferLen)
If rc = 0 Then
GetMyFileVersion = ""
`MsgBox "VerQueryValue 版本函数调用失败."
Exit Function
End If
lstrcpy Buffer, lVerPointer
Buffer = Mid$(Buffer, 1, InStr(Buffer, Chr(0)) - 1)
GetMyFileVersion = Buffer
End Function
3.创建批处理文件的函数
创建用于删除原来和临时文件的批处理文件。它会重复不断地搜索是否有在配置文件中[ExeFile]段首行记载的EXE文件,直到删除为止;当删除完毕后,这个批处理文件就会把自己删除。本方法可以支持所有的 Windows 版本,即 Win9X/Me/NT/2000/XP。
`newFileName已经下载的更新文件的临时文件名
Private Function CreateBatchFile(newFileName() As String)
Dim line, oldFileName(MAXFILES) As String
Dim Max, i As Integer
Open App.Path & "\up.bat" For Output As #1
For i = 0 To UBound(newFileName)
oldFileName(i) = newFileName(i) & ".update"
line = ":Repeat" & CStr(i) & vbCrLf & "del " & newFileName(i) & vbCrLf & "if exist " & newFileName(i) & " goto Repeat" & CStr(i) _
& vbCrLf & "Copy " & oldFileName(i) & " " & newFileName(i) & vbCrLf _
& "Del " & oldFileName(i) & vbCrLf
Print #1, line
Next
Print #1, newFileName(0)
Print #1, "Del " & App.Path & "\up.bat"
Close #1
End Function
4.版本判别和更新文件下载并运行的函数,该函数主要利用了以下API函数。
`网站文件下载函数
Private Declare Function URLDownloadToFile Lib "urlmon" _
Alias "URLDownloadToFileA" _
(ByVal pCaller As Long, _
ByVal szURL As String, _ `下载文件的网址
ByVal szFileName As String, _ `下载文件存储在本机的路径名称
ByVal dwReserved As Long, _
ByVal lpfnCB As Long) As Long
`读取INI配置文件中指定的条目信息
Private Declare Function GetPrivateProfileString Lib "kernel32" _
Alias "GetPrivateProfileStringA" _
(ByVal lpApplicationName As String, _ `段名
ByVal lpKeyName As Any, _ `关键字名
ByVal lpDefault As String, _ `缺省字符串
ByVal lpReturnedString As String, _ `目标缓冲器
ByVal nSize As Long, _ `目标缓冲器大小
ByVal lpFileName As String ) As Long `初始化文件名
`根据文件名查找文件
Private Declare Function FindFirstFile Lib "kernel32" _
Alias "FindFirstFileA" _
(ByVal lpFileName As String, _ `欲搜索的文件名
lpFindFileData As WIN32_FIND_DATA) As Long
`此结构用于装载与找到的文件有关的信息
`用户自定义的自动升级函数
Public Function AutoUpdateFile()
Dim lReturn As Long
Dim IniFile As String
IniFile = App.Path & "\" & INIFILENAME
lReturn = URLDownloadToFile(0, szURL & INIFILENAME, IniFile, 0, 0)
Dim s As String * MAXLEN
Dim version As String * MAXLEN
`以下是升级ExeFile段中标识的文件,因为本地文件正在使用,所以必须先下载到临时文件再运
`行批处理文件去删除原文件并拷贝...
s = String(MAXLEN, 0)
version = String(MAXLEN, 0)
lReturn = GetPrivateProfileString("ExeFile", 0&, "", s, MAXLEN, IniFile)
Dim ss As String
Dim sLoc, eLoc, Count As Integer
Count = 0
sLoc = 1
Dim oldVer, newVer As String
Dim newFileName(MAXFILES) As String
Do While eLoc <= MAXLEN
eLoc = InStr(sLoc, s, Chr(0))
If eLoc - sLoc = 0 Then Exit Do
ss = Mid(s, sLoc, eLoc - sLoc)
lReturn = GetPrivateProfileString("ExeFile", ss, "", version, MAXLEN, IniFile)
`MsgBox ss
oldVer = Left(version, InStr(version, Chr(0)) - 1)
newVer = GetMyFileVersion(App.Path & "\" & ss)
` MsgBox newVer & oldVer
If StrComp(oldVer, newVer, vbTextCompare) <> 0 Then
lReturn = URLDownloadToFile(0, szURL & ss, App.Path & "\" & ss & ".update", 0, 0)
If lReturn <> S_OK Then
`MsgBox "Error code=" & lReturn
Else
newFileName(Count) = App.Path & "\" & ss
Count = Count + 1
End If
End If
sLoc = eLoc + 1
Loop
`以下是升级OtherFile段中标识的其它相关文件,当时本地文件并未在使用,因此只要直接
`覆盖即可
Dim FindFileData As WIN32_FIND_DATA
s = String(MAXLEN, 0)
version = String(MAXLEN, 0)
lReturn = GetPrivateProfileString("OtherFile", 0&, "", s, MAXLEN, IniFile)
sLoc = 1
Do While eLoc <= MAXLEN
eLoc = InStr(sLoc, s, Chr(0))
If eLoc - sLoc = 0 Then Exit Do
ss = Mid(s, sLoc, eLoc - sLoc)
lReturn = GetPrivateProfileString("OtherFile", ss, "", version, MAXLEN, IniFile)
oldVer = Left(version, InStr(version, Chr(0)) - 1)
If StrComp(oldVer, "Yes", vbTextCompare) = 0 Then
`或者采用On Error Resume Next
`MkDir App.Path & "\" & SUBDIR
lReturn = FindFirstFile(App.Path & "\" & SUBDIR, FindFileData)
If lReturn = -1 And Len(SUBDIR) > 0 Then
MkDir App.Path & "\" & SUBDIR
End If
lReturn = URLDownloadToFile(0, szURL & ss, App.Path & "\" & SUBDIR & "\" & ss, 0,0)
If lReturn <> S_OK Then
`MsgBox "Error code=" & lReturn
Else
`MsgBox "Successed !"
End If
End If
sLoc = eLoc + 1
Loop
`现在开始需要升级ExeFile
If Count > 0 Then
Dim UpFileName() As String
ReDim UpFileName(Count - 1) As String
Dim i As Integer
For i = 0 To UBound(UpFileName)
UpFileName(i) = newFileName(i)
Next
CreateBatchFile UpFileName()
`Shell App.Path & "\up.bat", vbHide
lReturn = ShellExecute(ByVal 0&, "Open", App.Path & "\up.bat", ByVal 0&, ByVal 0&, ByVal SW_HIDE)
MsgBox "搜索到该程序有新版本发布,正在升级,请耐心等待......"
GlobalDeleteAtom nAtom
ExitProcess (0)
End If
End Function
5.主程序调用子程序
首先判断主程序是否已经被调用,这时用App.PrevInstance判断最简单,但并不十分可靠,只要是同一程序的副本,就会被认为是两个程序,所以用全局原子判断更为严谨。
If GlobalFindAtom("PaperDemoApp") = 0 Then `没有找到先前运行的程序
nAtom = GlobalAddAtom("PaperDemoApp")
Else `找到了原来运行的程序
`GlobalDeleteAtom GlobalFindAtom("PaperDemoApp")
hwnd = FindWindow(vbNullString, "Update Version:" & App.Major & "." & App.Minor)
If hwnd = 0 Then
MsgBox "该程序已经运行!" & vbCrLf & "请先将进程退出!"
Else
sw = ShowWindow(hwnd, SW_NORMAL)
SetForegroundWindow (hwnd)
End If
Exit Sub
End If
AutoUpdateFile `检测最新厂版本并自动升级
frmSplash.Show
GlobalDeleteAtom nAtom
`End `结束当前进程
四、结语
实现软件的强制自动升级,需要解决的核心问题是前后版本的比较以及锁定程序的自删除等关键技术,本文采用了直接从执行文件或动态库提取版本信息和批处理删除自身程序等方法,经过大量客户的实际使用,可以成功应用于Win98/ 2000/ XP/ 2003 等系列平台。虽然程序用VB 6.0实现,但文中的原理还是能推广到其他不同的语言中,因此本文对于有大量客户端,且安装后常有软件升级、文件更新等需求的一些类似应用开发有比较大的借鉴意义。
|