使用 .NET 和后台智能传输服务 API 来编写自动更新应用程序
本文假设您熟悉 C# 和 Visual Basic .NET
下载本文的代码: BITS.exe (363KB)
摘要
.NET Framework 和 Windows 都具有一些非常有趣的 API,这些 API 可用来创建能够自动通过网络进行自我更新的应用程序。 编写能够像 Windows Update 一样自我更新的应用程序有很多好处,其中包括为用户提供了便利,因为这使从维护到网络管理都变得更容易。 自动更新需要注意发现、安全性以及文件替换等因素。 在本文中,作者介绍了 BITS API 以及 .NET Framework 的许多处理自动更新(使用与 Windows Update 相同的功能)的这些方面的功能。
本页内容
困难 | |
BITS 基础知识 | |
BITS、COM 和托管代码 | |
将 BITS 与自动更新应用程序结合起来使用 | |
关于 BITS 的一些考虑事项 | |
.NET Framework 和安全性 | |
强名称和安全绑定 | |
更新打包和替换 | |
文件的替换和提取 | |
AutoUpdateApp.exe 示例应用程序 | |
自动更新应用程序的功能 |
我必须承认,我喜欢 Windows® Update 功能。 在我的计算机开机的时间中,大约有 85% 的时间都连接到了 Internet 上,但是,像许多人一样,我当然不会这么多时间都在使用网络。 Windows XP 利用了这一未用的带宽,将网上可用的最新服务包和修补程序与我计算机上已安装的服务包和修补程序进行比较。 如果它找到了我需要的更新,便会在后台将它们下载下来。 完成后,Windows 会通知我计算机上有需要安装的新软件包。
如果可以选择的话,我希望让客户端上的每一个应用程序都像 Windows 一样允许自动更新。 现在有许多有利的条件以及现成的连接来实现这一功能。 如果要使应用程序自动进行自我更新,必须编写代码来处理发现、下载、安全性以及替换等方面的问题。
为了处理实际的下载,我将介绍 Windows 的一项新功能:后台智能传输服务 (BITS)。 在讨论此功能之后,我将介绍 .NET Framework 中可用来解决自动更新应用程序的安全性和替换问题的功能。
请注意,虽然 BITS 1.5 可重新发布软件可以很好地工作在 Windows 2000 和 Windows XP 上,但是 Microsoft 不打算在 Windows 9x 或 Windows Me 上支持 BITS API。 BITS 有望作为 Windows 的将来版本的一个组件提供。
但是,在开始下面的内容之前,需要指出的重要一点是:要使您的应用程序能够使用本文所介绍的技术,需要在 .NET 下对它进行管理。 BITS 自身是操作系统的一部分,而这里介绍的 .NET Framework 技术可以用于非托管的应用程序的自动更新功能。
困难
为了找到远程服务器上的更新,您的应用程序需要一种查询网络的手段。 这需要网络代码以及使应用程序和服务器可以用来彼此通信的简单协议。 我将在本文的后面继续探讨“发现”这个话题。
接下来您还必须能够下载软件包。 由于 .NET 在网络方面驾轻就熟,因此下载可能看上去不像是一个大问题,但是下载用户所请求的文件仅仅是一个方面。另一个方面是在未经用户同意的情况下下载大型文件。 礼貌的自动更新应用程序仅仅使用残留的带宽来下载更新。 这听起来很不错,但是,您会看到,它带来了一个相当困难的技术问题。 幸运的是,相应的解决方案已存在。
安全可能是首当其冲的一个问题。 请花一点时间想一想 Windows Update 功能。 它的一个主要目的是获取安全修补程序。 想想,如果 Windows Update 自身都不能确认它安装的是否是安全的代码,情况会怎么样? 显而易见,从 Internet 上下载代码并执行该代码的任何应用程序都必须将安全性作为一个首要问题。 因此,我将探讨如何确保自动更新应用程序的安全性。
最后一个需要考虑的因素是使用应用程序自身的新版本来替换应用程序的过程。 此问题之所以引人关注,是因为它要求代码在将自身从系统中删除时能够运行。 处理这一技巧有许多种方法。
关于这些困难,最庆幸的一件事情是在 .NET Framework 与 Windows 之间,所有工具都是现成的,可以立刻用来解决这些问题。
BITS 基础知识
BITS 是 Windows 中新增的一项非常实用的文件传输功能,它通过 HTTP 异步地从远程服务器上下载文件。 BITS 可以专门利用空闲带宽来处理多个用户的多个下载任务。 尽管 BITS 不仅仅限于自动更新应用程序使用,但它是 Windows Update 使用的基础 API。 并且,由于它可用于任何应用程序,因此可用来完成创建自动更新应用程序的过程中所涉及到的大部分非常困难的工作。
下面是基本思想。 应用程序请求 BITS 处理一个或一组文件的下载。 BITS 将该任务添加到自己的队列中,并将该任务与应用程序所运行在的用户上下文关联。 只要用户登录,BITS 就会利用空闲带宽在网络中细细搜寻 (Drizzle) 这些文件。 事实上,BITS 技术的代号名称就是 Drizzle,这个词被证明是 BITS 所执行的工作的一个非常恰当的描述。
那么它到底是怎样工作的呢? 这项技术实际是一项非常复杂的技术。 首先,BITS 是作为一个 Windows 服务实现的,它维护组织成一组优先级队列的任务集合。 优先级包括前台、高、普通和低。 它按照循环法的原则,通过大约五分钟的时间片为同一优先级的每个任务分配带宽。 一旦队列中没有剩余的任务,便立即检查下一个优先级队列中的任务。
前台队列中的任务使用尽可能多的网络带宽,由于这一原因,前台优先级应只供响应用户请求的代码使用。 余下的优先级(高、普通和低)远远比前台优先级更引人关注,因为它们全都是后台优先级,也就是说,它们仅仅使用未用的网络带宽。
为了实现此后台功能,BITS 监视网络数据包,并忽略不属于它的数据包。 余下的数据包被认为是计算机带宽上的活动负载。 BITS 使用活动负载信息以及连接速度和其他某些统计信息来确定应继续下载文件还是暂停(以增加活动用户的吞吐量)。 因此,用户不会遇到带宽问题。
能够立即中断所做的工作对于 BITS 是非常重要的。 在许多情况下,BITS 不得不在仅仅下载了文件的一部分的情况下放弃与网络的连接甚至完全断开连接。 但是,下载了一部分的文件会保存起来,这样,当 BITS 抓住了与网络连接的片刻机会时,便能够从中断的位置继续下载。 这种恢复功能也有一些副作用。
请记住 BITS 是用于从 HTTP 服务器传输文件的。 要使 BITS 能够工作,服务器应与 HTTP 1.1 兼容,或者至少支持 GET 方法中的 Range 头。 这是因为 BITS 需要能够请求文件的一部分。 此外,下载的内容必须是静态内容,例如,标记文件、代码文件、位图或声音。 当请求动态内容(例如,CGI、ISAPI 或 ASP.NET 产生的内容)时,包含 Range 头的 GET 请求没有意义。
当前,BITS 有两个版本: 1.0 和 1.5。 BITS 1.0 随同 Windows XP 一起提供,它具有下列功能: 可与对话框及其他 UI 元素一起使用的可中断后台文件下载、下载优先级、任务完成及出错时的可选通知以及可选的进度通知。 BITS 1.5 随同 Windows .NET Server 一起提供。 除了 BITS 1.0 中包含的功能外,1.5 版还具有可中断的后台文件上载以及使用“基本”、“摘要”、“NTLM”、“协商”(Kerberos) 或“Passport”对连接进行身份验证的功能。 BITS 1.5 还作为与 Windows 2000 及更高版本兼容的可重新发布软件提供(请参阅后台智能传输服务)。
BITS 1.0 中的全部功能对于编写自动更新应用程序已够用,但是使用 BITS 1.5 功能可以执行更复杂的任务,例如,出售更新或处理应用程序与服务器之间的交互。
BITS、COM 和托管代码
BITS API 是作为 COM 对象实现的,正因为此,所以不能编写 .NET Framework 版的 API。 幸运的是,BITS API 非常简单易用。 本文所包含的示例应用程序是使用 C# 编写的,因此为了使用 BITS API,我不得不硬着头皮使用 .NET Framework 的 COM Interop 功能。 我在这里不深入探讨运行库可调用包装 (RCW) 及相关的内容,而仅仅说明我使用此 API 所遵循的过程。
如果我使用的是 C++,那么我的 BITS 代码的开头部分可能如下所示:
IBackgroundCopyManager* pBCM = NULL; hr = CoCreateInstance(__uuidof(BackgroundCopyManager), NULL, CLSCTX_LOCAL_SERVER, __uuidof(IBackgroundCopyManager), (void**) &pBCM); if (SUCCEEDED(hr)) { // Party-on with the pBCM interface pointer }
等效的 C# 代码使用 new 关键字创建 BackgroundCopyManager 对象,然后通过转换获得对 IBackgroundCopyManager 接口的引用,而不是通过调用像 CoCreateInstance 或 QueryInterface 这样的方法。 下面的代码片段是 C# 示例,它的作用是获取 IBackgroundCopyManager 接口:
IBackgroundCopyManager bcm = null; // Create BITS object bcm = (IBackgroundCopyManager)new BackgroundCopyManager();
此代码的简单性有一点误导的作用,因为需要做更多的工作来分别将托管的 BackgroundCopyManager 和 IBackgroundCopyManager 类型与底层的 COM 对象和接口关联。 .NET Framework 通过 RCW 来处理与 COM 对象的交互。 要将托管类型与 RCW 关联,必须使用属性。 图 1 中的代码说明了如何声明 BackgroundCopyManager 类和 IBackgroundCopyManager 接口,以使它们表示 BITS COM 对象。
图 1 中的代码有很多的属性(ComImportAttribute、GuidAttribute、MarshalAsAttribute 等等),这使真正的代码看起来不明显。 事实上,没有真正的代码。 接口和类的定义实际上仅仅是元数据形式的封送占位符,公共语言运行库 (CLR) 用它来直接调用 BITS COM API。
代码示例(请见本文顶部的链接)所包含的 InteropBits.cs 文件提供了与 BITS API 一起使用的所有接口、枚举类型及结构的 C# Interop 代码。 尽管示例仅仅使用了几个方法,但是我提供了完整的实现,因为它有助于您探索此 API 的更多功能。 值得注意的一点是,InteropBits.cs 文件中的代码不是托管 API,而是 COM API 实现之上的 Interop API。 Microsoft 终有一天会发布通过与 .NET Framework 类库的其余部分一致的方法向 .NET Framework 代码公开 BITS 功能的 API。
图 1 中的代码以及 InteropBits.cs 示例文件中的完整实现是手动创建的。 .NET Framework SDK 自带一个名为 TlbImp.exe 的工具。如果您有一个描述所针对的 COM API 的 TLB 文件,可以使用此工具创建类似的代码,并将其编译为托管程序集。 BITS API 不自带 TLB 文件,但是,Platform SDK 中包含一个名为 Bits.idl 的接口定义文件,该文件中有关于接口的描述。
我使用 MIDL.exe 工具(也随同 Platform SDK 一起提供)基于 Bits.idl 创建了一个 TLB 文件。然后使用 TlbImp.exe 创建了一个表示该 TLB 文件的托管程序集。 我使用 ILDasm.exe 将 TlbImp.exe 产生的程序集反汇编成中间语言。 最后,我将中间语言作为准则产生了 C# 代码,并在适当的地方对代码进行了调整,使它更好用更正确。 这种将 COM Interop 与 C# 结合起来使用的方法无疑是很乏味的,但是它可以对最终结果进行非常全面的控制。
将 BITS 与自动更新应用程序结合起来使用
BITS 服务以任务的形式来管理文件下载。 应用程序创建传输任务,然后将一个或多个文件添加到该任务中。 一旦任务的文件列表建立,该任务就会继续,因为任务的初始状态是挂起。 任务用来管理优先级、身份验证以及错误管理等细节。 应用程序可以随时取消任务。
一旦 BITS 完成了任务中所有文件的传输,应用程序便会通过调用某个方法来完成该任务。 Complete 方法将所有文件复制到其最终目的地。 尽管 BITS 文档将任务完成称为“复制”文件,但实际是在目标目录中创建临时的隐藏文件,Complete 方法仅仅重命名隐藏文件并使其可查看。
即便只使用最少的 BITS API(仅仅使用 IBackgroundCopyManager 和 IBackgroundCopyJob 接口),也可以实现自动更新应用程序所需要的一切。 如果需要任务枚举或者完成和错误通知等功能,则必须使用更多的接口。
示例应用程序 AutoUpdater.exe 使用 BITS 来实现更新的发现和下载。 为此,应用程序定义了连续更新名称的模式: 对于本示例,我选择的是 Update1.dll、Update2.dll,依此类推。 尽管这绝不是发现更新的唯一方法,但是它非常适合于 BITS 功能。 为了存储更新状态,示例应用程序维护一个 XML 文件。 该文件中的两段相关信息是下一个更新编号以及描述当前 BITS 下载任务(如果有正在进行的任务)的 GUID。
应用程序每次运行时,都打开该 XML 文件查看下一个更新是否已下载。 如果没有,并且 XML 文件中没有任务 GUID,那么应用程序将启动一个 BITS 任务以下载序列中的下一个更新。 下面的代码显示了启动下载任务所必需的方法:
IBackgroundCopyJob job=null; // Create a bits job to download the next expected update bcm.CreateJob("Application Update", BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out jobID, out job); // Add the file to the job job.AddFile(updateUrl, localLocation); job.Resume(); // start the job in action
对 job.AddFile 的调用传递将下载的文件的位置,以及任务完成时最终文件应存储在其中的完整本地路径和文件名。 请注意任务是在挂起状态下启动的,因此,在 BITS 服务使用任务来执行任何工作之前,任务必须已继续。
如果 XML 文件中有 GUID,则说明 BITS 任务已随着自动更新应用程序上一次启动而启动。 因此该 GUID 传递给 IBackgroundCopyManager.GetJob 方法,以查看安排的前一个下载任务是否已产生结果。 下一步取决于任务的状态。
从图 2 中可以看出,如果任务处于错误状态,那么应用程序将完成任务(将失败的任务从队列中清除),然后继续为同一个更新文件创建新的任务。 通常情况下,导致此错误状态的原因仅仅是该名称的更新尚未存在。 至此已完成了示例应用程序的发现部分。
使用 BITS 轮询更新的最大好处是 BITS 在后台工作。 因此,尽管轮询并不是那么尽如人意,但是 BITS 下载的非强制特征使它非常适于用来发现更新。
如果任务处于已传输状态,那么应用程序将通过调用 IBackgroundCopyJob.Complete 来完成该任务。这会导致文件被放入目标目录中,从而使其可随时更新。 然后应用程序返回,至此,这个难题中有关下载的部分就解决了。
最后,在图 2 中的默认情况下,示例应用程序仅仅返回任务,而不对它进行任何处理。 此任务可能处于两种状态中的某一种: 正在传输状态(意味着任务正在进行)或暂时的错误状态。 这两种情况都被认为是可能成功的情况,因此应用程序不对它进行任何处理。 如果 BITS 任务被置于暂时的错误状态,BITS 服务便会认为此错误有可能恢复,因此将重试。 最后,处于暂时错误状态的任务要么成功,要么被 BITS 置于错误状态。
令人吃惊的是,除了错误恢复逻辑外,这还涉及到 AutoUpdater.exe 示例应用程序的整个发现和下载部分。 至于下载过程的高级功能,其实现看起来与网络通信几乎没有任何相似之处,倒是与文件复制有着更多的共同点。 但是,在生产应用程序中,您可能会发现 BITS API 的其他功能的各种应用,例如,自动式传输通知、任务优先级处理以及上载任务。
关于 BITS 的一些考虑事项
设计 BITS 从未以启动网络连接为目的。 这是最佳考虑,因为您不会希望未连接的应用程序拨叫 ISP 仅仅是为了轮询更新。 但是,在共享 Internet 连接方面,BITS 1.0 和 1.5 都存在局限性。 如果系统 A 共享系统 B 的 Internet 连接,那么当应用程序启动系统 A 上的 BITS 任务时,可能导致系统 B 启动拨号。 这是因为 BITS 服务的当前实现未将连接共享这一情况考虑在内。
这是一个非常棘手的问题,也许 BITS 的将来版本会提供相应的解决办法。 Windows Update 功能也面临同样的问题。 就这一点(在共享连接上启动拨号)的表现而言,使用 BITS 的应用程序与 Windows 没有什么不同。
关于 BITS,最后一个需要关注的问题是任务文件的一致性。 如果下载任务中包含多个文件,那么在所有文件都可用之前,BITS 不会认为任务已完成,也不会认为文件已到达其最终目的地。 但是,BITS 无法知道服务器上的某个文件是否在任务中的其他文件下载后发生更改。 因此,在服务器上进行的更新会影响到客户端上的下载一致性。
有两种方法可以处理这一问题。 第一种是将 BITS 下载分成多个单文件任务,这是我在示例应用程序中采用的方法。 第二种方法是分别将每个任务的所有文件存储在服务器上的每个单独的目录中。 当要对文件进行更新时,将在服务器上为这组新文件创建一个新目录。 这样,之前的某个任务处于中间传输状态的客户端将不会受到这组新文件的影响。 此解决方案所带来的问题是,当服务器端的文件位置改变时,客户端需要一种途径来了解在哪里找到新文件。
如果您发现需要解决 BITS 代码中所存在的问题,没有比 BitsAdmin.exe 更适用的工具了。此工具在 Windows XP CD 的 Support\Tools 子目录中提供。 为了避免运行工具的安装以节省时间,我手动提取 Support.cab 文件中的 .exe。
随着您逐渐开始熟悉 BITS 下载,会发现 BitsAdmin.exe 工具非常实用。 通过此命令行工具中的开关几乎可以访问到 API 的每一项功能。 此外,还可以枚举任务、检查任务状态以及查看详细的日志和错误信息。 可以启动、挂起、继续和完成下载任务。 最后,还可以从 BitsAdmin.exe 中更改任务优先级以及取消任务。
很少有附带的工具能够如此方便地访问 Windows API。 事实上,像批处理文件这样的脚本可以通过 BitsAdmin.exe 工具来充分地使用 BITS。 BITS 是可用于发现和下载更新的一个了不起的解决方案,对于大多数应用程序而言,它的缺点微不足道。 现在让我们来看一看安全性。
.NET Framework 和安全性
为了用在代码中,.NET Framework 引入了一些非常有意思的安全结构。 例如,Code Access Security 就是一个这样的结构。 另一个安全基础结构是与托管程序集的强名称绑定。 为了能够更安全地将代码从服务器传输到客户端,我选择使用此功能。 这里的问题在于自动更新应用程序通过 Internet 与服务器联系并下载文件(包括将在客户端上安装的代码)。 必须对这些新文件加以保护,以防止它们被客户端与服务器之间怀有恶意的某一方所操纵。
此问题的一种解决办法是通过 HTTPS 连接来执行整个数据交换。 BITS 服务能够通过 HTTPS 协议来查找资源。HTTPS 仅仅是发生在加密的 SSL/TLS 通道中的 HTTP 通信。 但是,这种解决方案存在几个缺陷。 首先是导致性能下降。
Web 服务器在发送静态内容(如 HTML 文档或 EXE 文件)方面表现出色。 当 Microsoft Internet 信息服务 (IIS) 收到对静态文件的请求时,可以向 TransmitFile Win32?API 发出一个调用。 TransmitFile 在驱动程序这一级将文件逐个比特地从文件系统发送到网络流中,从而提供了非常卓越的性能。 在通过 HTTPS 引进加密的同时,也大大降低了性能,因为文件的每个字节都必须加密。 这增加了与更新服务器关联的成本。
使用 HTTPS 的另一个问题是它使 Web 服务器成为了安全基础结构中一个非常易于受到攻击的节点。 如果 Web 服务器被攻破,那么客户端将盲目地下载并安装被安装在服务器上的恶意内容。
但是,使用 .NET Framework 的一个非常简单的功能,可以在实现更安全的更新文件传输的同时避免所有这些缺点。 首先,需要一个加密的 SSL/TLS 通道(以使 Web 服务器的吞吐量不会受到影响)。 接下来,由于来自被攻破的服务器的重大安全威胁是对客户端的拒绝服务,因此本解决方案将阻止服务器在客户端上运行恶意代码。 .NET Framework 的这一功能称为强名称绑定,它是程序集加载器的一部分。
强名称和安全绑定
.NET Framework 缓解的一个长期 DLL 问题是弱名称方案,也就是仅使用文件名来标识可重用代码。 这个问题已导致了所有类型的应用程序问题都统称为“DLL 地狱”。 解决方案就是强名称。
强名称包含四部分信息: 文件名、版本号、区域信息以及公钥。 .NET Framework 使用强名称来强制在组件之间实现更严格的绑定。 此框架还使用公钥/密钥对在加载强名称程序集之前先对其进行验证。 对于强名称程序集,.NET Framework 语言编译器(C# 或 Visual Basic® .NET) 直接将公钥生成为 DLL 文件。 除了公钥外,编译器还对文件内容进行哈希计算,并使用私钥(由生成组织安全保管)对结果进行加密。
加密的哈希也称为数字签名。 要检查数字签名,需要将公钥从程序集文件中提取出来并用它来解密哈希。 然后,重新对文件的内容进行哈希计算,并将结果与签名哈希进行比较。 如果程序集的内容发生了任何改变,签名验证都将失败,因为哈希值不匹配。
您可能会猜测使用程序集中的公钥来确认完全相同的程序集似乎不太安全,确实如此。 但是,如果执行验证的代码有一种方法可以确认公钥就是众所周知的或者是所期待的公钥,那么您便拥有了更完整的安全解决方案。 .NET Framework 通过使用公钥的哈希(称为公钥标记或初始公钥)来实现此功能。
将程序集加载到应用程序中时,可以指定公钥标记。 只有当程序集具有强名称,并且强名称的公钥与传递给 Assembly.Load 方法的公钥标记匹配时,.NET Framework 才会加载该程序集。
接下来,我将逐步演示使用强名称命名程序集并使用初始公钥来加载它的过程。 下面的 C# 代码可以用来生成强名称程序集。 该程序集除了展示强名称绑定以外,无法执行其他任何功能:
using System.Reflection; [assembly:AssemblyKeyFile(@"Keys.snk")]
要生成此代码,必须首先创建包含公钥/私钥对的密钥文件 (Keys.snk)。 可以使用 .NET Framework SDK 自带的 Sn.exe 工具从命令行来完成此操作,命令如下:
>sn -k Keys.snk
现在,可以使用 C# 编译器将强名称程序集编译成 .NET Framework 程序集 DLL,命令如下:
>csc -t:library StrongName.cs
所产生的程序集将命名为 StrongName.dll。要找到此强名称产生的公钥标记,可以使用带 /T 开关的 Sn.exe,如图 3 所示。
图 3 提取程序集的公钥标记
图 3 中的示例显示的是公钥标记为 d586e46fd39d13b5(十六进制)的 DLL。 要使用强名称验证来加载此程序集,只需下面的代码:
// Load the update String name = Path.GetFileNameWithoutExtension(patchName); name = "StrongName.dll, PublicKeyToken=d586e46fd39d13b5"); Assembly update = Assembly.Load(name);
此代码验证签名。如果成功,将返回对程序集对象的引用;如果失败,Assembly.Load 方法将引发 FileLoadException。 无论在哪一种情况下,使用 Assembly.Load,此代码都能够验证程序集自生成以来未发生过丝毫改变。
自动更新应用程序可以使用 BITS 下载更新文件,而无需考虑服务器端的安全性。 然后,在客户端上安装并使用更新之前,可以先使用 Assembly.Load 来验证程序集。唯一的先决条件是初始应用程序是使用现有的公钥/密钥对(从公钥标记这个角度而言)安装的。 使用匹配的密钥对生成的任何后期更新都可以先在客户端上经过验证,才能再安装和使用。 其间,用于生成更新的密钥对文件可以存储在任何计算机上,甚至是未连接到网络的计算机上。
这一安全机制非常了不起,但它也带来了两个问题。 例如,可以更新像位图和数据文件这样的非代码文件吗? 是否必须强名称生成所有的 DLL 和 EXE 文件才能使用此机制? 这两个问题的回答分别是“是”和“否”。
更新打包和替换
如果自动更新解决方案不能用于非代码文件,或者如果它要求所有的 DLL 和 EXE 文件都是托管的并且具有强名称,那么它对于我将没有那么大的吸引力。 因此,我需要找到一种当不仅仅包含 .NET Framework 程序集文件的更新时也能够使用 .NET Framework 强名称绑定的方法。 答案是将更新文件像程序集资源一样嵌入到程序集文件中。 这不仅使得任何文件类型都可以通过强名称保护起来,而且还使所有更新任务都采用单文件下载的形式,从而简化了 BITS 任务的创建过程。 一个大文件实际是一个包含许多文件的包。 事实上,在一个包中,可能包含 CAB 文件甚至是 MSI 脚本 — 这种可能性是没有止境的。
所有 .NET Framework 语言编译器都允许将任意文件嵌入到程序集文件中。 我将向您展示的是如何使用 C# 编译器来完成此项工作,但其原理同样适用于 Visual Basic .NET 或托管的 C++。 下面的命令行将 Code.dll、Bitmap.gif 和 Data.xml 等文件生成为 Update2.dll(前述三个文件各作为一个嵌入式资源包含在 Update2.dll 中):
csc /res:Code.dll /res:Bitmap.gif /res:Data.xml /out:Update2.dll /t:library
请注意 C# 编译器非常灵活,无需 .cs 代码文件即可生成 DLL 程序集。 此 DLL 仅仅包含嵌入式资源。 但是,如果您希望 DLL 包含强名称,那么还应包含一个简短的 .cs 文件(包含与 StrongName.cs 类似的代码)。
找到嵌入式资源并将其重新复制到文件系统中所需的代码显示在图 4 中。 此代码使用 Assembly.GetManifestResourceName 和 Assembly.GetManifestResourceStream 方法来找到资源并将其中的字节复制到文件系统中的文件中。 图 4 中的代码不在文件系统中保留原始文件的日期,但是如果要这样做,也可以很容易地对代码进行修改。 请注意提取过程仅仅在 Assembly.Load 验证了容器程序集的签名之后才发生。
文件的替换和提取
在应用程序更新过程中将文件提取到应用程序目录中是一个非常重要的问题,即便文件具有强名称和程序集资源也是如此。 问题的症结在于应用程序在执行过程中将文件复制到其自己的文件上。 更复杂的是,所运行的应用程序可能是像记事本这样的应用程序。 此类应用程序同时运行多个实例是很常见的。
如果您的应用程序是这一类应用程序,那么全能型解决方案几乎是不可能的。 与其规定单一的解决方案,不如来看一看可以用来处理替换问题的功能。 首先,显而易见,在不同于主应用程序的单独进程中运行自动更新功能是可以实现的。 如果您的应用程序是非托管的,并且您希望对它进行扩展以便可以使用本文所介绍的技术,那么这个方法尤其有用。 更新进程可以处理所有 BITS 逻辑以及文件提取。 更新进程甚至可以等到主应用程序的所有实例都已终止,然后再尝试提取文件。
在单独的进程中运行提取代码主要有三个缺点: 首先,它可能使得很难修补自动更新代码本身。 第二,此方法在服务器端更不具优势,因为服务器不太可能关闭。 第三,如果主进程由 Windows Job 对象(不要与 BITS 任务混淆)管理,那么,当主进程退出时,自动更新进程也将自动被 Windows 终止。
AutoUpdateApp.exe 示例应用程序在主应用程序逻辑所在的进程运行自动更新逻辑。 如果您也选择这样做,将会遇到问题,因为进程正在使用的 DLL 和 EXE 文件将被删除和替换。 对于 DLL 或 EXE 被删除的问题,一个不太提倡的技巧是将文件重命名为临时文件名,然后将新文件复制到旧文件名上。 然后可以使用 MoveFileEx Win32 API 来记录要在系统重新启动时删除的文件,或者在应用程序中包含将搜索每个临时文件并将其删除的逻辑。
即便使用重命名技巧,仍然不得不考虑这样一种可能性:当更新正在进行时,用户运行了应用程序的另一个实例,这样它将加载一半旧文件一半新文件。 有两种方法可以解决这种情况。 一种是使用 CLR 中的应用程序域的影像复制功能。
CLR 在名为应用程序域或 AppDomain 的逻辑应用程序容器中运行托管代码。 多个 AppDomain 可以运行在一个 Windows 进程中。 当 AppDomain 创建出来后,可以将这一新域标记为对文件进行影像复制。 这意味着当您的应用程序加载程序集时,程序集文件复制到隐藏的临时目录中,并从影像位置加载。 .NET Framework 中的 ASP.NET 类使用此功能来避开应用程序文件,使其可以被 ASP.NET 代码生成器更新。
AppDomains 还可以用于应用程序重执行。 理想情况下,当有更新可用时,应用程序会提示用户确定是否希望应用此更新。 如果用户的回答是“是”,那么应用程序将仅仅在适当的位置使用新功能运行。 对于某些应用程序而言,在新进程中重新启动已更新的应用程序是可接受的。 在其他情况下,则最好让新代码在执行更新的 EXE 所在的进程中立即开始工作。
使用 AppDomain 可以很容易地重新执行托管 EXE。 执行此功能的代码示例可以在本文所包含的示例代码(见本文顶部的链接)的 RelaunchExe 方法中找到。 RelaunchExe 方法新建一个 AppDomain,然后重新执行可执行文件,并将在第一个位置启动应用程序时所使用的相同命令行参数传递给该文件。
AutoUpdateApp.exe 示例应用程序
本文的代码示例不仅包含自动更新逻辑,而且还生成应用程序的两个更新版本,以便您可以动态地看到整个解决方案。 现在让我们来看一看这个示例。
尽管 Visual Studio? .NET 是功能强大的编码环境,但有时您也不得不创建自己的批生成。 我的项目是很难作为 Visual Studio 项目生成的多版本生成的一个示例。 因此我将示例作为源文件和批文件的集合发布,以便可以从命令行执行生成。
要试用此应用程序,请从本文顶部的链接下载存档文件,并将它解压缩到空目录中。 然后从命令行运行 Build.bat。 要使批文件成功,csc.exe(C# 命令行编译器)和 sn.exe(强名称实用工具)必须都位于路径环境变量中。 csc.exe 位于 .NET Framework 安装区域的以下目录中: C:\Windows\Microsoft.Net\Framework\。Sn.exe 工具位于 .NET Framework SDK 目录中,该目录通常安装在下面两个位置中的某一个位置: C:\Program Files\Microsoft.NET 或 C:\Program Files\Microsoft Visual Studio .NET。
使用 Build.bat 生成项目后,会创建两个重要的目录: app 和 updates。 请注意,将出现一个 sln 文件,以便您可以轻松地使用 Visual Studio .NET 编辑代码文件(但是,应继续使用批处理文件来生成项目)。
第一个目录 app 包含应用程序安装。 将此目录更改为应用程序目录,然后运行 AutoUpdateApp.exe 以试用它。 此应用程序的实际功能是显示它在启动目录中找到的第一个位图(见图 5)。
图 5 自动更新简单应用程序
为了动态地查看更新功能,创建一个名为 updates 的 IIS 虚拟根目录(授予匿名下载权限)。 其目的是使 HTTP://localhost/updates 成为用来发布所有更新的工作位置。 然后将第二个目录 updates 的内容复制到虚拟根目录中。
如果一切都设置妥当,那么您的应用程序现在应该能够找到位于 HTTP://localhost/updates 的 Update1.dll 和 Update2.dll。 从命令行执行和关闭 AutoUpdateApp.exe 若干次。 大约在第二次或第三次启动时,应用程序应该通知您可以应用第一个更新。 第一个更新仅仅将新的 GIF 文件添加到目录中。 第二个更新真正地安装 AutoUpdateApp.exe 的新副本,该副本现在有一项功能可以运行启动目录中的所有 GIF 文件的幻灯片放映。
首次启动 AutoUpdateApp.exe 时,应用程序创建一个名为 UpdateState.xml 的数据文件。此文件包含控制 AutoUpdateApp.exe 的功能的信息,其中包括应用程序在其中查找更新的网络位置。 如果您不希望使用 HTTP://localhost/ 上的位置,可以更改 UpdateState.xml 中的 URL 以使用另一个更新服务器。
自动更新应用程序的功能
现在,有一些很棒的功能可用来编写自动更新的应用程序。 也就是说,Microsoft 开发人员当前正在努力使自动更新更简单,并成为应用程序开发的一个天然部分。 .NET Framework 的 1.0 版已提供了这里介绍的一些固有的基本功能,但没有涉及部署功能(见.NET Zero Deployment: Security and Versioning Models in the Windows Forms Engine Help You Create and Deploy Smart Clients)。 最后,需要说明的一点是,您编写的应用程序安装和更新起来越容易,该应用程序就越成功。
为了发现和替换更新,Microsoft 计划在 Windows 的将来版本中引入内置服务。 BITS 服务将依然是基础的下载机制,但将推出更多的 API 抽象以简化下载和安全保证过程。 此外,还将扩展 BITS,以包括用于管理应用程序更新且简单易用的 UI。
我盼望着有一天,启动应用程序与安装应用程序之间的分隔线完全消失。 但是,至少今天我已经能够编写一安装就将自我更新的应用程序。 这是非常喜人的第一步。
相关文章,请参阅:
Using Windows XP Background Intelligent Transfer Service (BITS) with Visual Studio .NET
.NET Interop: Get Ready for Microsoft .NET by Using Wrappers to Interact with COM-based Applications
Applied Microsoft .NET Framework Programming by Jeffrey Richter (Microsoft Press, 2002)
背景信息,请参阅:
.NET and COM: The Complete Interoperability Guide by Adam Nathan (SAMS, 2002)
Jason Clark 为 Microsoft 和 Wintellect (HTTP://www.Wintellect.com) 提供培训和咨询,并且是开发 Windows NT 和 Windows 2000 Server 的前辈。 他是 Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Press, 2000) 的合著者。 可以通过 JClark@Wintellect.com 与 Jason 联系。