65.9K
CodeProject 正在变化。 阅读更多。
Home

自动更新的有效方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (22投票s)

2017年9月11日

CPOL

7分钟阅读

viewsIcon

79536

downloadIcon

1153

一种提供静默自动更新且无需服务器端代码的简单方法

商业版本 

CodeProject 2017年9月最佳C++文章 一等奖

引言

许多软件开发者需要更新他们的软件并将这些更新应用到所有现有用户。本文提供了一种我们开发的方法,该方法允许完全透明的自动更新,用户无需任何操作(例如启动新版本等)。它之所以独特,是因为它不需要任何服务器端代码。

问题

我一直在寻找一种方法,让我们的软件只在我们的网站上的版本更新时才下载更新,同时又避免运行服务器端组件。我们的目标是让我们的 AutoUpdate 类在不先下载的情况下确定网站上的版本是否确实是新版本。我们也想要一个干净流畅的解决方案,它不需要安装新版本,而是能让产品(即 software.exe)透明地用新版本(也叫 software.exe)替换自身。我了解到,有很多步骤需要执行,也有很多场景需要解决,但最终的解决方案是稳健的,并且涵盖了所有场景。

构建块

获取给定可执行文件的版本。我们使用 GetFileVersionInfo() 来获取信息。我们开发了自己的函数,将可执行文件路径转换为我们自己的版本信息结构,它

typedef struct
{
    int Major;
    int Minor;
    int Revision;
    int SubRevision;
} SG_Version;

所以 SG_GetVersion 如下所示

BOOL AutoUpdate::SG_GetVersion(LPWSTR ExeFile, SG_Version *ver)
{
    BOOL result = FALSE;
    DWORD dwDummy;
    DWORD dwFVISize = GetFileVersionInfoSize(ExeFile, &dwDummy);
    LPBYTE lpVersionInfo = new BYTE[dwFVISize];
    GetFileVersionInfo(ExeFile, 0, dwFVISize, lpVersionInfo);
    UINT uLen;
    VS_FIXEDFILEINFO *lpFfi;
    VerQueryValue(lpVersionInfo, _T("\\"), (LPVOID *)&lpFfi, &uLen);
    if (lpFfi && uLen)
    {
        DWORD dwFileVersionMS = lpFfi->dwFileVersionMS;
        DWORD dwFileVersionLS = lpFfi->dwFileVersionLS;
        delete[] lpVersionInfo;
        ver->Major = HIWORD(dwFileVersionMS);
        ver->Minor = LOWORD(dwFileVersionMS);
        ver->Revision = HIWORD(dwFileVersionLS);
        ver->SubRevision = LOWORD(dwFileVersionLS);
        result = TRUE;
    }
    ReplaceTempVersion();
    return result;
}

在文件名中添加下一个版本。接下来,我们需要一个函数来生成包含下一个版本的文件名。例如,如果我们的软件是“program.exe”,下一个版本是 1.0.0.3,那么这个函数将生成以下名称并将其存储在我们的类的成员变量 m_NextVersion 中:“program.1.0.0.3.exe”。

void AutoUpdate::AddNextVersionToFileName(CString& ExeFile, SG_Version ver)
{
    CString strVer;
    ver.SubRevision += 1;    // For the time being we just promote the subrevision in one but of course
                            // we should build a mechanism to promote the major, minor and revision
    ExeFile = GetSelfFullPath();
    ExeFile = ExeFile.Left(ExeFile.GetLength() - 4);
    ExeFile += L"."+strVer;
    ExeFile += L".exe";
    m_NextVersion = ExeFile;
}

从网站下载新版本

在研究了几种从 URL 下载文件的方法后,这是我们发现的最佳方法。请注意,我们正在使用 DeleteUrlCacheEntry()。如果您的软件每 2 小时检查一次更新,并且只有在 6 小时后才放置更新,那么除非您删除缓存,否则您的浏览器可能不会下载新文件,因为它将使用现有文件的名称。这在 QA 阶段尝试不同场景时也很重要。

    DeleteUrlCacheEntry(URL); // we need to delete the cache so we always download the real file
    HRESULT hr = 0;
    hr = URLDownloadToFile(
        NULL,   // A pointer to the controlling IUnknown interface (not needed here)
        URL,
        ExeName,0,              // Reserved. Must be set to 0.
        &pCallback);           //  A callback function is used to ensure asynchronous download


然后回调函数是这样的

我们定义了一个名为 MyCallback 的类。在调用 URLDownloadToFile() 之前,我们定义了这个函数的本地成员

    MyCallback pCallback;

该类定义如下

using namespace std;

class MyCallback : public IBindStatusCallback
{
public:
    MyCallback() {}

    ~MyCallback() { }

    // This one is called by URLDownloadToFile
    STDMETHOD(OnProgress)(/* [in] */ ULONG ulProgress, /* [in] */ ULONG ulProgressMax, /* [in] */ ULONG ulStatusCode, /* [in] */ LPCWSTR wszStatusText)
    {
        // You can use your own logging function here
        // Log(L"Downloaded %d of %d. Status code", ulProgress, ulProgressMax, ulStatusCode);
        return S_OK;
    }

    STDMETHOD(OnStartBinding)(/* [in] */ DWORD dwReserved, /* [in] */ IBinding __RPC_FAR *pib)
    {
        return E_NOTIMPL;
    }

    STDMETHOD(GetPriority)(/* [out] */ LONG __RPC_FAR *pnPriority)
    {
        return E_NOTIMPL;
    }

    STDMETHOD(OnLowResource)(/* [in] */ DWORD reserved)
    {
        return E_NOTIMPL;
    }

    STDMETHOD(OnStopBinding)(/* [in] */ HRESULT hresult, /* [unique][in] */ LPCWSTR szError)
    {
        return E_NOTIMPL;
    }

    STDMETHOD(GetBindInfo)(/* [out] */ DWORD __RPC_FAR *grfBINDF, /* [unique][out][in] */ BINDINFO __RPC_FAR *pbindinfo)
    {
        return E_NOTIMPL;
    }

    STDMETHOD(OnDataAvailable)(/* [in] */ DWORD grfBSCF, /* [in] */ DWORD dwSize, /* [in] */ FORMATETC __RPC_FAR *pformatetc, /* [in] */ STGMEDIUM __RPC_FAR *pstgmed)
    {
        return E_NOTIMPL;
    }

    STDMETHOD(OnObjectAvailable)(/* [in] */ REFIID riid, /* [iid_is][in] */ IUnknown __RPC_FAR *punk)
    {
        return E_NOTIMPL;
    }

    // IUnknown stuff
    STDMETHOD_(ULONG, AddRef)()
    {
        return 0;
    }

    STDMETHOD_(ULONG, Release)()
    {
        return 0;
    }

    STDMETHOD(QueryInterface)(/* [in] */ REFIID riid, /* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject)
    {
        return E_NOTIMPL;
    }
};

SG_Run 函数

SG_Run 函数是我们找到的启动新进程的最佳方法。有多种方法,例如 ShellExecute(),但这是最高效的方法。

BOOL SG_Run(LPWSTR FileName)
{
    wprintf(L"Called SG_Run '%s'", FileName);
    PROCESS_INFORMATION ProcessInfo; //This is what we get as an [out] parameter

    STARTUPINFO StartupInfo; //This is an [in] parameter

    ZeroMemory(&StartupInfo, sizeof(StartupInfo));
    StartupInfo.cb = sizeof StartupInfo; //Only compulsory field

    if (CreateProcess(FileName, NULL,
        NULL, NULL, FALSE, 0, NULL,
        NULL, &StartupInfo, &ProcessInfo))
    {
        //WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
        CloseHandle(ProcessInfo.hThread);
        CloseHandle(ProcessInfo.hProcess);
        wprintf(L"Success");
        return TRUE;
    }
    else
    {
        wprintf(L"Failed");
        return FALSE;
    }

}

解决方案

在开始编码之前,请记住确保您的项目有版本字符串。要添加版本字符串,请在解决方案资源管理器中右键单击项目名称,然后选择“添加”->“资源”。 

流程

该解决方案可以用以下步骤描述

假设产品的当前版本(software.exe)是 1.0.0.1,我们希望它下载并用更新的版本 1.0.0.2 替换自身。我们将 1.0.0.2 版本的 software.exe 命名为 software.1.0.0.2.exe。我们将此版本放在我们的网站上,可以免费下载,无需与服务器交互或登录。例如:http://www.ourproduct.com/downloads/software.1.0.0.2.exe
  1. Software.exe 应获取其自身版本,并定期尝试下载其下一个版本(如果可能)。如果不存在更新版本,则不会下载任何内容。
     
  2. 当存在新版本时,它将被下载到一个临时文件(例如 _software.exe)。
     
  3. 将检查 _software.exe,以确保其版本字符串与服务器上的原始文件名(即 1.0.0.2)匹配,如果不匹配,它将被删除并忽略。
     
  4. software.exe 现在将 _software.exe 作为一个新进程启动。
     
  5. software.exe 退出。
     
  6. _software.exe 将自身复制到 software.exe,这在 software.exe 退出后是可能的。
     
  7. _software.exesoftware.exe 作为一个新进程启动。
     
  8. _sotware.exe 退出。
     
  9. software.exe 删除 _software.exe。
     

要求

我们需要将我们的软件链接到以下库

urlmon.libversion.lib

请记得使用语言下拉菜单设置代码片段的语言。

使用“var”按钮将变量或类名包装在 <code> 标签中,例如 this

神奇之处

我在服务器上放置了此程序的相同副本,但有一个更改:本文中的代码版本字符串是 1.0.0.1,我服务器上的版本是 1.0.0.2。如果您下载源代码并编译它,您将获得 SG_AutoUpdate.exe。如果您运行它,它应该会在几秒钟内自动更新。

原始可执行文件外观如下

运行后,您将看到同一个文件,但实际上它是一个不同的文件。这是新版本

当然,您可以使用您自己的网站。

目前,下载链接是使用此常量构建的

    CString m_DownloadLink = _T("http://forensics.tips/");

AutoUpdate.h 第 43 行。

您可以将其替换为任何其他地址。以下是测试此机制的说明:

  1. 构建源代码并将 SG_AutoUpdate.exe 保存在某处(或暂时重命名)。
  2. 转到项目的资源,找到版本字符串,并将其更改为1.0.0.2
  3. 再次构建源代码。
  4. 您将获得最新版本,名为 SG_AutoUpdate.exe。将其上传到您的服务器并重命名为 SG_AutoUpdate.1.0.0.2.exe
  5. 所有名称都区分大小写。
  6. 现在,回到您备份的副本,删除您刚刚上传的文件,然后将备份重命名回 SG_AutoUpdate.exe。删除所有其他文件。您应该只有一个名为 SG_AutoUpdate.exe 的文件,并且如果将鼠标悬停在其上,您应该会看到“之前”图像,版本应显示:“1.0.0.1。”
  7. 双击它,几秒钟后您会发现它已更新到1.0.0.2。您可以检查版本字符串以进行验证。

但让我们确保新版本不会被再次下载,也不会导致无休止的“假阳性”更新。

现在再次运行当前的 SG_AutoUpdate.exe。您会发现它报告服务器上没有更新。我们完成了。

我录制了一个小视频展示了整个过程。请仔细查看文件资源管理器中的“程序版本”列。

 

更新(支持 UAC)

根据 David Zaccanti 的提问,此自动更新方法支持需要提升权限的应用程序,即提示用户批准管理员权限。我已经更新了源代码和服务器上的新版本(程序更新到的版本),因此它们都需要管理员权限。当您编译源代码并运行版本 1.0.0.1 时,它将显示 UAC 提示,但在静默更新到版本 1.0.0.2 后,不会出现提升提示,因为提升的进程可以使用 CreateProcess() 启动另一个提升的进程,而无需提升提示。

待完成...

此代码中还有一些增强功能,但可以进行开发

- 添加 Log() 函数,允许您将所有“wprintf”调用重定向到一个日志文件中,该文件由程序的所有变体在自动更新过程中共享。

- 允许自动更新到下一个版本之后的版本,例如,从 1.0.0.1 到 1.0.0.5。

- 在项目的生成后部分添加自动版本更新程序,这将允许当前版本的自动升级。

Michael Haephrati
商业自动更新解决方案的创建者
haephrati@gmail.com
© . All rights reserved.