你好,欢迎来到电脑编程技巧与维护杂志社! 杂志社简介广告服务读者反馈编程社区  
合订本订阅
 
 
您的位置:杂志经典 / 编程语言
软件的强制自动升级
 

     本文讨论了程序员在软件开发应用及维护过程中对多客户端软件升级的困扰,提出软件自身强制自动升级的的基本思路,并提供在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实现,但文中的原理还是能推广到其他不同的语言中,因此本文对于有大量客户端,且安装后常有软件升级、文件更新等需求的一些类似应用开发有比较大的借鉴意义。

 

 

  推荐精品文章

·2024年9月目录 
·2024年8月目录 
·2024年7月目录 
·2024年6月目录 
·2024年5月目录 
·2024年4月目录 
·2024年3月目录 
·2024年2月目录 
·2024年1月目录
·2023年12月目录
·2023年11月目录
·2023年10月目录
·2023年9月目录 
·2023年8月目录 

  联系方式
TEL:010-82561037
Fax: 010-82561614
QQ: 100164630
Mail:gaojian@comprg.com.cn

  友情链接
 
Copyright 2001-2010, www.comprg.com.cn, All Rights Reserved
京ICP备14022230号-1,电话/传真:010-82561037 82561614 ,Mail:gaojian@comprg.com.cn
地址:北京市海淀区远大路20号宝蓝大厦E座704,邮编:100089