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

认证示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (90投票s)

2008 年 1 月 9 日

CPOL

55分钟阅读

viewsIcon

191796

downloadIcon

735

如何准备一个 .NET 应用程序以获得 Windows Vista 认证徽标,包括一个简单但完整的应用程序的源代码(Visual Studio 2005 解决方案)。

Killer Application about box

引言

如果您计划为 .NET 应用程序获取 Windows Vista 认证徽标,本文将对您有所帮助。在这里,您将找到一个简单但完整(可认证)的 VB.NET + C# 桌面应用程序,包括应用程序本身和安装程序的完整源代码,所有这些都打包在一个 Visual Studio 2005 解决方案中。

背景

不久前,我被分配了一项对我来说似乎很艰巨的任务:为我们的一个应用程序获取 Windows Vista 认证徽标。我所拥有的所有工具就是 Visual Studio、应用程序的源代码以及互联网连接。当然,还有必要的资金(如果需要)。

这项工作已经完成,应用程序现已获得认证。虽然“艰巨”这个词可能不太适合形容这个过程,但它绝非易事。问题在于微软提供的信息不足;我不是指所有行政琐事(在 Winqual 注册、获取您组织的数字证书、申请豁免等等;所有这些在 Innovate on Windows VistaWinqual 页面上都有很好的解释),而是指纯粹的技术部分。微软会把测试用例文档甩在你脸上……祝你好运,你自己看着办。

例如,看看测试用例 25:验证应用程序在安装过程中是否正确处理正在使用的文件。您打开 Orca,浏览您为应用程序创建的安装程序,然后……糟糕,找不到 MsiRMFilesInUse 对话框。Visual Studio 创建的安装程序不会创建它。现在该怎么办?

幸运的是,我们有互联网,我们有搜索引擎,还有不少人已经经历过认证过程的斗争。例如,关于 MsiRMFilesInUse 问题的解决方案可以在 这个论坛帖子 中找到。好吧好吧,我说微软不提供任何帮助,现在我又贴了一个 MSDN 博客的链接。我的意思是,微软应该一开始就提供这些信息,在一个更完整的测试用例文档中,或者在一个单独的 FAQ 中。

好了,微软的“好/坏”讨论就到此为止。事实是,在我完成认证工作之后,我认为将我在整个过程中获得的知识分享出去,以使其他面临相同任务的人的生活更轻松一些,这是一个好主意。谁知道呢,“你”明天也许能挽救“我”的生命,所以我想那时你会因为我而感到高兴。

在你继续之前

首先,我假设您已经对认证过程有所了解。我的意思是,我假设您知道,要获得认证,一个应用程序必须通过微软提供的测试用例文档中描述的若干测试用例(可以从 Innovate on Windows Vista 页面下载),您需要购买一个组织数字证书,需要在 Winqual 注册您的应用程序以进行错误报告,并且一旦您的应用程序准备就绪,您需要将其提交给测试机构。除了测试用例,我在这里不会详细解释整个过程的细节。

其次,我假设您的应用程序在结构上与我认证的应用程序相似。“结构相似”是我现在发明的一个不错的流行语,意思是您的应用程序应该具有以下特征:

  • 完全托管的应用程序(仅 .NET 程序集,无非托管代码)。
  • 32 位应用程序。
  • 只安装桌面应用程序。不安装驱动程序或服务。
  • 独立应用程序。不需要网络连接。
  • 始终为每个计算机安装。不支持为每个用户安装。
  • 应用程序数据对所有用户都是通用的。不生成每个用户的数据。
  • 应用程序在安装后不会自动启动。
  • 应用程序在关机后不会重启。
  • 不支持并发用户会话。
  • 不支持远程桌面(终端服务器)执行。

我在这里解释的内容适用于具有上述功能的应用程序。这并不意味着如果您的应用程序在某些方面与我的不同,您就必须立即停止阅读。这仅仅意味着您必须格外小心,因为我将要说的某些内容可能不适用于您,并且/或者您需要到别处去搜索额外的信息。例如,如果您的应用程序支持并发用户会话,您必须确保一个会话的声音不会在另一个会话中被听到。我在这里不讨论这个问题,因为我不需要解决它。

醒目的红色字母免责声明

这很常识,但还是再说一遍:无论您在这里读到什么,在将您的应用程序提交给测试机构之前,您必须将您的应用程序针对所有适用的测试用例进行测试。我不会接受任何类型的投诉,例如,“我因为你说的 XXX,但我的应用程序却是 ZZZ,白白花了 1000 美元进行认证!” 我是人,所以我也会犯错,更不用说我对 Visual Studio 和 .NET 应用程序几乎一无所知。我想帮助您,但我不是上帝。

那么,我们将要做什么?

我们将剖析一个我特意为本文创建的应用程序。该应用程序名为 Killer Application,由虚构的 Capsule Corporation 公司开发。源代码以 Visual Studio 2005 解决方案的形式,可在页面顶部下载。

Killer Application 的作用基本上是没什么用(它由一个父 MDI 窗体组成,带有几个菜单,显示一些数据并允许您创建和读取文本文件)。但是,它将通过所有测试用例,并可能获得 Windows Vista 认证徽标;也就是说,如果您是比尔·盖茨,并且口袋里有 1000 美元的钞票准备花在任何东西上。我创建它是为了模仿我为认证准备的真实应用程序的基本结构。出于这个原因,VB.NET 和 C# 项目混合在一起。

阅读完本文后,您可以将 Killer Application 解决方案用作创建您自己应用程序的框架,或者只是一些源代码片段,或者只是获得一些关于如何做事的想法,然后从头开始编写所有代码;无论哪种最适合您的需求。甚至可能发生的是,您会找到更好的做事方式(真的!)。在这种情况下,如果您能留下评论解释您的发现,那将是很好的。

设置您的环境

让我们开始准备编译 Killer Application 所需的环境(不是测试环境:您不需要 Windows Vista 进行开发过程;事实上,我使用的是 Windows XP)。首先,您当然需要 Visual Studio 2005。我猜 Visual Studio 2008 也可以通过适当的解决方案转换来工作。

其次,我假设您已经获得了您的组织证书。如果没有,您将无法编译解决方案,除非您修改 Visual Studio 中的解决方案后置生成事件(稍后会详细介绍)。我假设凭据和私钥文件名分别是 capsulecred.spccapsulekey.pvk,但当然您可以使用任何您喜欢的名称。

第三,在您主驱动器的根目录(即 C:\vistatools)下创建一个名为 vistatools 的目录。这里我们将存放一些编译应用程序所需的额外文件。

第四,将以下文件复制到 vistatools 目录

  • mt.exe,Microsoft Manifest Tool。它是 Visual Studio 2005 SDK 的一部分,您应该在 C:\Program files\Microsoft Visual Studio 8\SDK\v2.0\Bin 目录中找到它。否则,下载并安装 SDK
  • MsiTran.exe,MSI Transform Tool。它是 Windows Vista SDK 的一部分。
  • signtool.exe,代码签名实用程序。它是 Visual Studio 2005 SDK 和 Windows Vista SDK 的一部分。

(当然,您可以通过在互联网上搜索来单独找到这些文件,但我更倾向于指向“官方”来源。)

第五,您需要从凭据文件和私钥文件创建您组织的数字证书文件。您只需要为所有应用程序执行此操作一次。以下是步骤:

  1. 下载、解压缩并安装 pvkimprt 实用程序
  2. capsulecred.spccapsulekey.pvk 文件复制到 vistatools 目录。
  3. 打开命令提示符,转到 vistatools 目录并执行以下命令:pvkimprt -PFX capsulecred.spc capsulekey.pvk

然后会弹出一个图形界面,要求输入私钥密码。稍后,它会问您是否要导出私钥。请回答“是”。在下一个屏幕上选择默认参数(PKCS #12 格式,允许安全保护),然后为生成的证书文件输入一个新密码(我假设您输入 kaitokun)。最后,当被问到证书文件名时,浏览到 vistatools 目录并选择一个合适的名称(我假设是 capsulekey.pfx)。

完成后,您可以删除 vistatools 目录中的 capsulecred.spccapsulekey.pvk 密钥(当然,但要确保您已将其存储在其他地方!)。目录内容应为:mt.exeMsiTran.exesigntool.execapsulekey.pfx。准备好所有这些,您就可以编译 Killer Application 了。

应用程序结构

在这里,我们将概述 Killer Application:它由什么组成以及它的作用。之后,我们将深入研究源代码和项目设置的细节。Killer Application 解决方案包含四个项目:

  • Killer Application:主应用程序程序集,它是一个 VB.NET 可执行文件,包含所有 GUI 和(非常简单的)应用程序逻辑。
  • KillerApplication.Support:一个 C# DLL,仅包含一个自定义控件。我将其包含进来是为了模仿大多数实际应用程序的典型结构(一个主可执行文件加上一个或多个支持 DLL 文件)。
  • KillerApplication.Install:另一个 C# DLL,其中包含一个具有安装程序自定义操作的类。我们稍后会看到为什么这段代码需要放在自己的程序集中。
  • Killer Application installer:安装程序项目,将为 Killer Application 生成安装程序(MSI)文件。

运行应用程序时,您将看到一个 MDI 窗体,其中包含一个包含三个主要条目的菜单。此菜单允许您执行一些简单的操作,以帮助您执行测试用例(或者至少是这样打算的)。

  • 文件菜单包含三个条目:
    • 打开将打开一个窗体,其中包含控件,允许您读取和显示磁盘上任何位置的文本文件的内容。任何异常都将被捕获并显示其信息。有按钮可以填充文件路径文本框,其中包含三个“有趣”的位置:Windows 目录以及 logouser1logouser2 的个人目录。使用此窗体来执行测试用例 2。
    • 保存将打开一个窗体,其中包含控件,允许您在磁盘上的任何位置创建文本文件。同样,异常将被捕获,并且有按钮可以填充文件路径文本框,其中包含 Windowslogouser1logouser2 目录。使用此窗体来执行测试用例 2 和 3。
    • 退出将——您猜对了——终止应用程序执行。
  • 执行操作菜单包含五个条目:
    • 显示酷控件将打开一个窗体,其中仅包含支持 DLL 中定义的自定义控件。
    • 查看照片将打开一个窗体,仅显示照片。照片文件作为包含文件包含在项目中。此窗体以及以下两个窗体间接执行测试用例 15。
    • 查看文本与查看照片窗体类似,但这次显示的是文本文件的内容而不是照片。
    • 查看数据仅在您已在运行应用程序的计算机上安装 SQL Server 2005 Express 时才有效。它将打开一个窗体,显示包含在项目中的小型 SQL Server 数据库中的数据,该数据库作为包含文件包含在项目中。连接字符串有一个不错的技巧,我们稍后会看到。
    • 崩溃会生成一个 `null` 引用异常。这是快速执行测试用例 32(“验证应用程序是否只处理已知且预期的异常”)的一种方法。
  • 帮助菜单只有一个菜单项:
    • 关于将显示一个简单的关于框。

就是这样。不多,但足以执行所有适用的测试用例。现在让我们深入细节。

设置解决方案

在进行应用程序解剖时,有三个主要关注领域:解决方案/项目设置、源代码和安装程序项目。在本节中,我们将详细介绍第一部分。我将通过列出如果从头开始创建解决方案应该遵循的步骤来做到这一点。当然,所有这些都是我在创建 Killer Application 解决方案时完成的。我相信,即使您正在准备现有解决方案的认证,这些信息也会对您有所帮助。

请注意,当然,您可以根据需要使用任何解决方案和项目名称,并创建或多或少的项目。

1. 创建项目

打开 Visual Studio,创建一个类型为 其他项目类型 -> Visual Studio 解决方案 -> 空解决方案 的新项目。将其命名为 Killer Application。向解决方案添加三个新项目:一个名为 Killer Application 的 VB Windows Forms 应用程序,一个名为 Capsule.KillerApplication.Support 的 C# 类库项目,以及另一个名为 Capsule.KillerApplication.Install 的 C# 类库项目。移除 Visual Studio 默认添加到项目中的 Form1 和 Class1 项。此时不要创建安装程序项目。在主 VB.NET 项目中添加对支持类库的引用。在主 VB.NET 项目中添加一个新类,并将其命名为 `Program`。将以下占位符代码添加到类中:

<STAThread()> _
Public Shared Sub Main(ByVal args() As String)
End Sub

打开主 VB.NET 项目的属性对话框,在“应用程序”选项卡中执行以下更改:

  • 将根命名空间更改为 Capsule.KillerApplication
  • 取消选中 启用应用程序框架 复选框。
  • 将启动对象更改为 `Program`。

请注意,如果您的主应用程序项目是 C# 项目,则会自动创建带有占位符 `Main` 方法的 `Program` 类。您仍然需要更改根命名空间。您可能会问:“为什么我们要费力处理一个 `Main` 方法,而不是将启动代码放在主窗体加载事件中?”正如我们在查看应用程序源代码时将看到的,我们需要在应用程序启动之前检查一系列条件,其中一些条件甚至可能阻止应用程序运行。因此,我们需要在任何窗体被创建之前执行代码。

2. 设置元数据

现在我们将填写每个项目中 AssemblyInfo 文件中的一些应用程序信息。在 C# 项目中,它位于 Properties 文件夹中。在 VB.NET 项目中,它位于 My Project 文件夹中,但在这种情况下,您首先需要激活解决方案资源管理器窗口中的 显示所有文件 图标。对于主应用程序程序集,我们将设置这些数据:

<Assembly: AssemblyTitle("Killer Application")>
<Assembly: AssemblyDescription( _
   "Simple example of an application that could obtain the
        ""Certified for Windows Vista"" logo.")>
<Assembly: AssemblyCompany("Capsule Corporation")>
<Assembly: AssemblyProduct("Killer Application")>
<Assembly: AssemblyCopyright("© Capsule Corporation 2007, 2008")> 

对于支持 DLL,数据将是:

[assembly: AssemblyTitle("Capsule.KillerApplication.Support")]
[assembly: AssemblyDescription("Support DLL for Killer Application")]
[assembly: AssemblyCompany("Capsule Corporation")]
[assembly: AssemblyProduct("Capsule.KillerApplication.Support")]
[assembly: AssemblyCopyright("© Capsule Corporation 2007, 2008")]

对于安装自定义操作 DLL,数据也非常相似;只需更改程序集标题和描述。设置元数据不是严格必需的,但这是个好习惯。Killer Application 在关于框中使用了此数据。

3. 添加清单文件

测试用例 1 指出所有应用程序可执行文件都必须“包含一个嵌入式清单,定义其执行级别”。使用 Visual Studio 2005,没有直接的方法可以将清单文件包含在程序集中。顺便说一句,Visual Studio 2008 使这项任务更容易,但我们将假设我们都是使用 Visual Studio 2005 的可怜用户。幸运的是,有一种间接方法可以做到这一点。在主应用程序程序集(VB.NET 可执行文件)中,添加一个新文本文件并将其命名为 Killer Application.exe.manifest。文件名必须与程序集名称相同,加上 .exe.manifest。确保文件的属性页面中的生成操作设置为 。然后打开文件并粘贴以下内容:

<?xml version="1.0" encoding="utf-8" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

请注意,如果我们的应用程序有更多可执行(*.exe*)项目,我们就必须为每个项目重复此步骤。清单文件的内容始终相同;只有文件名本身会改变。此清单文件将由项目后置生成事件处理,我们现在将看到。

4. 设置后置生成事件

还记得我们以前创建的 vistatools 目录吗?现在是时候使用它了。我们将设置项目的后置生成事件,以便对生成的程序集执行一些有用的操作。在 VB.NET 可执行项目的属性窗口中,选择 编译 选项卡,单击 生成事件 按钮,然后在 后置生成事件命令行框中粘贴以下内容:

%HOMEDRIVE%\vistatools\Mt.exe -manifest "$(ProjectDir)$(TargetFileName).manifest"
     -outputresource:"$(ProjectDir)obj\$(ConfigurationName)\$(TargetFileName);1"
%HOMEDRIVE%\vistatools\signtool.exe sign
    /f %HOMEDRIVE%\vistatools\capsulekey.pfx /p kaitokun /v
         /t http://timestamp.verisign.com/scripts/timstamp.dll
    "$(ProjectDir)obj\$(ConfigurationName)\$(TargetFileName)"

注意:实际上,只有两个命令要执行。为了提高可读性,我将它们分成了单独的行。在 Visual Studio 的后置生成事件窗口中,命令必须在每一行上。

在第一个命令中,我们使用清单工具将清单文件嵌入到生成的程序集中。第二个命令将签名程序集,从而满足测试用例 5(“验证应用程序安装的可执行文件和文件是否已签名”)。请注意,您必须将 capsulekey.pfx 文件名和 kaitokun 密码更改为您实际的值。如果您还没有组织证书,但仍想编译该应用程序,只需删除此行,或者更好的是,通过在其前面添加 `rem` 命令来禁用它。

请注意,我们以 $(ProjectDir)obj\$(ConfigurationName) 目录中创建的文件为目标,而不是在目标生成目录(通常是 bin\ConfigName)中创建的文件。这是因为,在生成安装程序时,要打包的可执行文件实际上将从 obj 目录中获取。这是我们希望“带有清单”和签名的文件,而不是用于开发机器上调试和测试的文件。

现在,我们来看看支持程序集。对于 KillerApplication.SupportKillerApplication.Install,打开属性窗口,选择 生成事件 选项卡,然后在 后置生成事件命令行框中粘贴以下内容(再次注意,这是一个命令分为四行):

%HOMEDRIVE%\vistatools\signtool.exe sign /f
    %HOMEDRIVE%\vistatools\capsulekey.pfx /p kaitokun /v
    /t http://timestamp.verisign.com/scripts/timstamp.dll
    "$(ProjectDir)obj\$(ConfigurationName)\$(TargetFileName)"

是的,它与可执行文件的情况完全相同,但没有清单部分。同样,如果您还没有组织证书,请暂时删除或注释掉该命令。第一版后置生成事件(两个命令)必须设置在所有生成 EXE 文件的项目中。第二版(一个命令)必须设置在所有生成 DLL 文件的项目中。

5. 添加您的代码和数据

这就是项目设置所需的所有内容(安装程序项目除外,我们稍后会对其进行剖析)。现在您需要将应用程序代码和辅助数据(图像、文本、数据集等)添加到项目中。您可以使用任何您需要的代码和数据……除了,当然,您需要做一些特殊的事情,我们现在将看到。

我的数据在哪里?

在我们深入研究源代码的细节之前,我们将看看另一个需要注意的主题:应用程序数据。也就是说,所有属于项目但不是代码的应用程序文件。

Visual Studio 允许您将这些类型的文件添加到您的项目中。只需右键单击项目,然后选择 添加新项添加现有项。然后在该项的属性页面中,确保 生成操作设置为 内容。Killer Application 在主项目的 data 文件夹中有三个这样的文件:一张照片、一个文本文件和一个数据库文件,如下图所示:

Killer Application content files

问题是:应用程序生成后,这些文件去哪里了?答案取决于您如何生成应用程序。

  • 如果您直接从 Visual Studio 构建或运行应用程序,则内容文件将被复制到与可执行文件生成在同一目录中(即,项目目录下的 bin\ConfigName 目录)。
  • 创建安装程序时(我们稍后会看到),您可以指示编译器将所有内容文件打包在一起,并在安装时将它们复制到目标计算机上的任何所需目录。

在这两种情况下,原始目录结构都得以保留。这意味着,对于 Killer Application,在生成解决方案时,将有一个 bin\Release\Killer Application.exe 文件以及 bin\Release\Data\Texts\Agreement.txt,以及两个更多文件(照片和数据库)在其原始相对路径中。

Killer Application folder generated by VS

现在是时候看看测试用例 15 了:“验证应用程序是否按默认设置安装到正确的文件夹。” 对于应用程序范围内的数据,正确文件夹是 `%ALLUSERSPROFILE%` 变量(在 .NET 术语中也称为 CommonApplicationDataCommonAppDataFolder)指向的文件夹。在 Windows XP 中,这通常是 C:\Documents and Settings\All Users,在 Windows Vista 中是 c:\ProgramData。如果您稍微作弊一下滚动到底部(或者更好的是,查看 Killer Application 解决方案),您会发现 Killer Application 安装程序创建了一个名为 Killer Application 的目录,并将所有项目内容文件放在这里。

Killer Application folder in installer project

因此,考虑到所有这些,您可能会认为使用单一代码库在所有情况下访问应用程序数据会很好,无论数据放在哪里。您说得对。以下是我在 Killer Application 中实现这一点的方式:

  1. 在 `Program` 类中创建一个名为 `DataDirectory` 的 static string 变量。
  2. 启动时,检查应用程序可执行目录中是否存在一个名为 Data 的目录。如果存在,则将 `DataDirectory` 设置为该目录的路径。否则,将 `DataDirectory` 设置为 %ALLUSERSPROFILE%\Killer Application\Data
  3. 当您需要从代码中访问应用程序数据时,通过将 `Program.DataDirectory` 的值与文件的相对路径(不包括 data 部分)组合来获取正确路径。例如:Path.Combine(Program.DataDirectory, "Texts\Agreement.txt")

这里还有一些额外的技巧,但我们将在查看源代码时看到它们。

源代码(终于)

现在我们真正准备好查看 Killer Application 的源代码了。我们将首先查看启动顺序,然后查看 Killer Application 主窗口上的菜单项的作用,最后我们将检查安装程序支持项目中包含的自定义操作。

启动

我们将从源代码分析开始,即 Killer Application 在启动时执行的代码。在 Visual Studio 中打开 `Program.cs` 文件,查找 `Main` 方法,您会发现以下内容:

1. 将 Main 包装在 Try-Catch-Log 块中

您看到 `Main` 方法如下时可能会感到惊讶:

<STAThread()> _
Public Shared Sub Main(ByVal args() As String)
    Try
        _Main(args)
    Catch ex As Exception
        Dim text As String = _
            String.Format("Unexpected exception in Killer Application:{0}({1}){0}{2}", _
            Environment.NewLine, ex.GetType().Name, ex.Message)
        EventLog.WriteEntry("Application Error", text, EventLogEntryType.Error, 1000)
        Throw
    End Try
End Sub

测试用例 32 说:“验证应用程序是否只处理已知且预期的异常。” 那么,为什么我们在这里做相反的事情呢?难道我们不应该让意外异常自行处理吗?

问题在于您可以在测试用例的“验证”部分读到的内容:“对于上面的每个可执行文件,必须同时有一个源列为 Application Error 的错误消息和一个源列为 Windows Error Reporting 的信息消息,才能通过此测试用例。” 事实是,信息消息已生成,但事件日志中没有错误消息的痕迹。因此,我们必须手动生成它,而这正是这段奇怪代码的作用。记录完成后,`Throw` 语句将未修改地重新抛出异常,因此一切都很好,我们就通过了测试用例。

请注意,在 ` catch ` 块的末尾,我们必须使用 `Throw` 而不是 `Throw ex`。前者将重新抛出原始异常并保留原始调用堆栈,而后者将生成一个新异常,导致调用堆栈丢失(并且还会导致测试用例 32 失败,我承认我不知道为什么)。

其余初始化代码在 `_Main` 方法中。

2. 设置未处理异常模式

在执行任何其他操作之前,需要以下代码:

Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException)

否则,在未处理异常的情况下,您将看到这个丑陋的错误窗口,而不是漂亮的 WER 窗口。

Killer Application not valid crash window

您猜对了,如果发生这种情况,测试用例 32 将失败。

3. 检查远程桌面执行

测试用例 9 说:“验证应用程序是否通过远程桌面正常启动和执行。” 但是,如前所述,Killer Application 不支持远程桌面执行。测试用例如何通过?答案在于小字。测试用例描述中有一条注释说:“如果应用程序不支持远程桌面,它必须向用户弹出一条指示此消息的消息,并将一条消息写入 Windows NT 事件日志才能通过此测试用例。” 而这正是拯救我们生活的地方。

If SystemInformation.TerminalServerSession OrElse _
        Command.ToLower().Contains("failremote") Then
    MessageBox.Show("Terminal Server execution is not allowed.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
    Dim evlog As New EventLog_
        ("Application", Environment.MachineName, "Killer Application")
    evlog.WriteEntry("Terminal Server execution was attempted. _
            User was notified and application terminated.", _
         EventLogEntryType.Information)
    Return
End If

`failremote` 命令行开关是我用来测试此功能而不必实际设置终端服务器连接的技巧。在实际应用程序中可以删除它。顺便说一句,这段代码的功劳归功于 Amitava 先生

4. 检查现有应用程序实例

测试用例 8 说:“验证应用程序是否通过快速用户切换正常启动和执行。” 同样,这是 Killer Application 不支持的一项功能。同样,小字拯救了我们:“如果应用程序不支持并发用户会话,它必须向用户弹出一条指示此消息的消息,并将一条消息写入 Windows NT 事件日志才能通过此测试用例。”

因此,我们将执行一些与远程桌面执行类似但稍微复杂一些的操作。我们将首先检查 Killer Application 是否已被另一个用户运行;如果是,我们将显示错误消息,创建相应的事件日志,然后终止。如果不是,我们将检查 Killer Application 是否已被我们自己运行;如果是,我们将激活已运行实例的主窗口。

我们将需要一些“高级”代码来实现这一点。在支持项目中,您可以找到 `AlreadyRunningChecker` 类(它应该在主项目中,但我 tinha C# 的代码,懒得转成 VB)。该类有两个 static 方法:`ActivateProcessMainWindow`,它将激活给定进程 ID 的主窗口(通过使用非托管 API);以及 `GetSameNameProcess`,它将返回 Killer Application 已运行实例的进程 ID(通过使用检测)。借助这个类,我们可以通过以下方式正确检查其他应用程序实例的存在:

Dim pid As Long = AlreadyRunningChecker.GetSameNameProcess(False)
If pid <> 0 OrElse Command.ToLower().Contains("failmultiuser") Then
    MessageBox.Show("This application is already being run by another user.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
    Dim evlog As New EventLog_
        ("Application", Environment.MachineName, "Killer Application")
    evlog.WriteEntry( _
        "Multiple user execution was attempted. _
        User was notified and application terminated.", _
         EventLogEntryType.Information)
    Return
End If

pid = AlreadyRunningChecker.GetSameNameProcess(True)
If pid <> 0 Then
    AlreadyRunningChecker.ActivateProcessMainWindow(pid)
    Return
End If

我从互联网上的某个地方获得了 `AlreadyRunningChecker` 类的大部分代码,但我记不清是哪里了。抱歉。

5. 设置数据目录路径

我们之前已经谈过:应用程序数据文件(所有不是代码的应用程序文件)根据应用程序是从 Visual Studio 内部运行还是使用生成的 MSI 文件安装,位于不同的位置。我们将使用全局变量 `Program.DataDirectory` 来存储实际的数据文件路径。以下代码将设置此变量的内容:

DataDirectory = ConfigurationManager.AppSettings("DataDirectory")
If String.IsNullOrEmpty(DataDirectory) Then
    DataDirectory = Path.Combine(Application.StartupPath, "Data")
    If Not Directory.Exists(DataDirectory) Then
        DataDirectory = Path.Combine(Environment.GetFolderPath( _
        Environment.SpecialFolder.CommonApplicationData), "Killer Application\Data\")
    End If
Else
    DataDirectory = Path.Combine(Application.StartupPath, DataDirectory)
End If
If Not DataDirectory.EndsWith("\") Then DataDirectory += "\"

这段代码的作用如下:

  1. 检查配置文件中的 appSettings 部分是否存在键 `DataDirectory` 并且其值不为空。如果存在,则将 `DataDirectory` 设置为其值。(相对路径是指应用程序可执行文件的路径)
  2. 否则,检查应用程序可执行文件所在的目录中是否存在 Data 目录。如果存在,则将 `DataDirectory` 设置为该目录的路径。
  3. 否则,将 `DataDirectory` 设置为公共应用程序数据文件夹加上 Killer Application\Data

2 将适用于应用程序在 Visual Studio 中运行或构建时,3 将适用于应用程序安装时,1 保留给特殊用途,您希望在已安装的应用程序中使用一组不同的数据(例如,在生产计算机上进行调试)。在设置完 `DataDirectory` 字段后,还需要做一件额外的事情。Killer Application 的一个数据文件是一个数据库文件,通过类型化数据集访问。如果您查看设置文件,您会看到使用的连接字符串如下:

Data Source=.\SQLEXPRESS;AttachDbFilename=
    "|DataDirectory|Database\KillerApplicationDatabase.mdf";
Integrated Security=True;User Instance=True

此连接字符串假定本地安装了 SQL Server 2005 Express。但是请看 AttachDbFilename 键:它指向应用程序数据文件所在的目录。这个值可以更改吗?是的,这就是我们现在要做的:

AppDomain.CurrentDomain.SetData("DataDirectory", DataDirectory)

SQL Server 引擎使用的 `DataDirectory` 值是应用程序域范围的设置,可以使用 `AppDomain` 类的 `SetData` 方法进行设置。默认情况下,此设置未设置(即其值为 `null`),因此 SQL Server 引擎假定应用程序可执行目录是其值。由于数据库文件可能位于多个不同的位置,因此我们需要适当地设置此设置,以便 SQL Server 能够找到数据库文件。

此外,如果您讨厌全局变量,可以直接使用此应用程序域设置而不是 `Program.DataDirectory` 变量来获取数据文件路径。只需使用以下代码获取其值:`AppDomain.CurrentDomain.GetData("DataDirectory")`。

但是,此方法存在一个问题。使用此连接字符串,每当您尝试编辑类型化数据集(例如,添加新的 TableAdapter)时,Visual Studio 都会抱怨找不到数据库文件。这是因为 Visual Studio 始终假定 `DataDirectory` 设置的默认值,因此,在编辑数据集时,您需要对连接字符串进行以下修改:

AttachDbFilename="|DataDirectory|Data\Database\KillerApplicationDatabase.mdf";

我确定肯定有更好的解决方案,但我已经习惯了这个,所以就保留了这个方案。

6. 记录应用程序执行情况

这实际上不是必需的,但我将其作为修改数据目录中文件的示例(理论上,这在卸载应用程序时是一个问题,我们稍后会看到原因以及如何解决)。简单地说,一行文本包含当前时间和用户名将被追加到数据目录根目录下的一个名为 log.txt 的文件中(该文件不是解决方案的一部分,也不是安装包的一部分,它在第一次写入时创建)。

Dim log As String = String.Format("Application run by {0} on {1}{2}", _
    Environment.UserName, DateTime.Now, Environment.NewLine)
File.AppendAllText(Path.Combine(DataDirectory, "log.txt"), log)

7. 运行应用程序

这里没有什么不寻常的,我们只是将控制权转交给应用程序的主窗口。

Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Application.Run(New FormMain())

打开和保存文件

测试用例 2 说:“验证最低权限用户无法修改其他用户的文档或文件”,测试用例 3 说:“验证最低权限用户无法将文件保存到 Windows 系统目录。” 好消息是,您不需要做任何特殊的事情来满足这些测试用例,因为操作系统会相应地授予或拒绝对文件和文件夹的访问。您只需要确保在尝试读取或写入您(实际上是用户)无权访问的地方时,正确控制将引发的异常。

Killer Application 中的文件菜单将帮助您处理这些测试用例。它包含打开保存两个条目,用于创建和打开文本文件。任何生成的异常都将被捕获并显示其相关信息。

保存对话框窗口如下所示:

Killer Application save file dialog

有一个文本框,您必须在此输入将创建文本文件的路径。三个按钮允许您使用三个“有趣”的位置填充此文本框:Windows 系统目录,以及 logouser1logouser2 用户的家目录(这些用户必须根据测试用例规范文档的说明进行创建)。您还可以通过单击“...”按钮出现的目录树来选择任何目录。最后,创建文件按钮将在选定的目录中创建一个名为 KILLERAPP.TXT 的文件,其中包含固定文本。

请注意,我们没有使用 `SaveFileDialog` 控件,这会使我们的工作更轻松。这是故意的,因为文件保存和打开对话框控件甚至不允许用户浏览任何未经授权的目录,因此使这些测试用例变得微不足道。真正的挑战(嗯,实际上也不是什么大挑战)是在通过代码处理文件时通过测试用例,这就是为什么这个自定义保存对话框如此丑陋。

这是附加到创建文件按钮单击事件的代码:

Try
    File.WriteAllText(Path.Combine(txtPath.Text, "KILLERAPP.TXT"), _
        "Congratulations! You have successfully created a text file _
            with Killer Application.")
    MessageBox.Show(Me.MdiParent, "Text file created successfully.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Information)

Catch ex As UnauthorizedAccessException
    MessageBox.Show(Me.MdiParent, "Sorry, you don't have the necessary _
            permissions for creating a file here.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Warning)

Catch ex As Exception
    Dim text As String = String.Format("Ooops. Unexpected error:{0}{0}({1}){0}{2}", _
        Environment.NewLine, ex.GetType().Name, ex.Message)
    MessageBox.Show(Me.MdiParent, text, "Killer Application", _
        MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try

尝试在未经授权的地点写入文件时,我们会收到一个 `UnauthorizedAccessException`,我们必须适当地处理它(在这种情况下,我们只是将其转换为人类可读的错误消息)。在这个简单的应用程序中,我们盲目地捕获任何其他可能的异常并简单地显示它,在实际应用程序中,我们可能会进行更精确的异常处理。

保存对话框的兄弟是打开对话框,其细节我在此不予显示,因为它与保存对话框非常相似。它只是增加了一个文本框来输入要打开的文件名(可以填写为 KILLERAPP.TXT)和一个显示文件内容的文本框;至于代码,`File.WriteAllText` 调用被更改为 `File.ReadAllText`。

有了所有这些,您可以按照以下步骤来执行测试用例 2 和 3:

  1. logouser1 身份登录 Windows,运行 Killer Application,然后选择 文件 -> 保存
  2. 尝试在 Windows 目录中创建文件,并确保您不能。
  3. logouser2 目录中,情况与上面相同。
  4. 尝试在 logouser1 目录中创建文件,您应该能够无错误地完成。
  5. 关闭“保存”对话框,选择 文件 -> 打开,并确保您可以打开您创建的文件(名为 KILLERAPP.TXT)。
  6. 退出 Killer Application,以 logouser2 身份登录 Windows,运行 Killer Application,然后选择 文件 -> 打开
  7. 确保您无法从 logouser1 目录打开 KILLERAPP.TXT 文件。
  8. 尝试在 logouser1 目录中创建文件,您应该会收到错误。

此处有一个注意事项。如 此处所述,您可能会发现您实际上可以写入 Windows 目录。如果发生这种情况,则意味着您的可执行文件没有有效的清单。检查您是否忘记在项目中包含清单文件,以及/或是否已适当设置项目后置生成事件以使用 mt.exe 将清单文件包含在程序集中。

显示数据(并崩溃)

Killer Application 主窗口上的执行操作菜单包含三个与处理数据文件相关的条目:查看照片查看文本查看数据

关于应用程序数据文件,值得说的一切都已经说过了:我们已经看到测试用例 15 迫使我们在安装应用程序时将所有应用程序数据文件放在指定目录中,我们已经看到了如何在开发+调试时同时拥有这些数据文件,并且我们看到了在涉及 SQL Server 2005 Express 数据库文件时该怎么做。这些菜单条目允许我们看到这些概念的实际应用。

例如,查看 `FormViewPhoto`。它使用以下代码加载照片文件:

Dim photoPath As String = Path.Combine(Program.DataDirectory, "Photos\KaitoCute.jpg")
pictureBox.Load(photoPath)

至于 `FormViewText`,它使用以下方式加载文本文件:

Dim textPath As String = Path.Combine(Program.DataDirectory, "Texts\Agreement.txt")
textBox.Text = File.ReadAllText(textPath)

……等等。如果您有更多数据文件,那么如何访问它们:取项目中的文件路径,去掉开头的“Data”,将其与位于 `Program.DataDirectory` 的根数据目录路径组合起来,您就完成了。对于 SQL Server 数据库,这通过连接字符串和 DataDirectory 应用程序域设置进行处理。

还有一个额外的菜单项,崩溃,它只会强制产生一个 `null` 引用异常。这将允许您轻松地执行测试用例 32(“验证应用程序是否只处理已知且预期的异常”),但这只是为了方便,并且不能免除您实际使用 threadhijacker 使您的应用程序崩溃。

安装程序的自定义操作

在源代码解剖的最后,但同样重要的一点是,我们将看看 KillerApplication.Install 项目。该项目包含一个类(外加几个包含辅助代码的类),`Installer`,其中包含安装程序的自定义操作。

自定义操作是在应用程序安装和/或卸载时执行的代码片段。它们对于执行 Windows Installer 技术提供的标准功能之外的任务很有用(主要是复制文件、创建注册表项以及创建程序菜单和桌面快捷方式)。自定义操作定义在一个 .NET 类中,该类必须具有 `RunInstaller` 属性,并且必须添加到安装程序项目上的自定义操作编辑器中。我们稍后会看到更多内容。

在继续之前,让我们回答一个我们很久以前问过自己的问题:为什么这些自定义操作必须放在单独的程序集中?为什么它们不能是主程序集或支持程序集的一部分?答案是,这会导致应用程序无法干净地卸载。

更具体地说,如果您将自定义操作放在应用程序本身的某个程序集中,会发生什么?一件非常糟糕的事情:在应用程序卸载后,您会发现应用程序安装的文件夹仍然存在,并且其中只有一个文件:是的,包含自定义操作的程序集。虽然没有一个测试用例明确指出应用程序必须执行干净卸载(尽管有人可以从测试用例 23 推断出来),但这是常识;没有人希望在卸载后,应用程序会在用户的硬盘上留下垃圾。

这个问题怎么解决?很简单:将自定义操作的代码放在一个单独的程序集中,并将此程序集安装在应用程序文件夹以外的任何位置。在我的例子中,我选择了公共文件文件夹(我们稍后将看到如何做到这一点),并且效果很好:自定义操作已正确执行,应用程序已干净卸载。

我承认我对它为何会这样运作了解不多,并且是通过反复试验才找到这个解决方案的。欢迎提出关于替代方法的建议。

话虽如此,让我们看看我们在项目中使用了哪些自定义操作以及它们的作用。请注意,为了方便起见,`Installer` 类定义了两个文本常量来存储应用程序名称,以及一个属性来告诉我们应用程序的安装位置:

private const string APPNAME="Killer Application";
private const string APPFILE="KILLER APPLICATION.EXE";

private string ApplicationDataPath
{
    get { return Path.Combine(Environment.GetFolderPath
            (Environment.SpecialFolder.CommonApplicationData), APPNAME); }
}

1. 安装

首先,我们需要一个在应用程序安装时执行的自定义操作。在安装时,我们需要执行两项任务:

  1. 为我们的应用程序在 Windows 事件日志中创建事件源。
  2. 给予普通用户对应用程序数据文件目录的访问权限。这是必要的,因为安装程序是以管理员特权执行的,因此创建的数据文件目录属于管理员。

完成此操作的代码如下:

public override void Install(System.Collections.IDictionary stateSaver)
{
    if(!EventLog.SourceExists(APPNAME))
    {
        EventLog.CreateEventSource(APPNAME, "Application");
    }

    string userGroupName=FindUserForSid.GetNormalUsersGroupName();
    AclManager manager=new AclManager(ApplicationDataPath, userGroupName, "F");
    manager.SetAcl();

    base.Install(stateSaver);
}

`FindUserForSid.GetNormalUsersGroupName` 调用将为我们获取标准用户组的名称,该名称取决于 Windows 的语言(例如,在英语中是 `Users`,在西班牙语中是 `Usuarios`)。我从 pinvoke.net 获取了这个类的代码(当然,将管理员组 SID 更改为用户组 SID)。`AclManager` 类更改给定用户组的目录权限。我从 Rick Strahl 的博客获取了它。

2. 回滚

测试用例 23 说:“验证应用程序是否回滚安装并将计算机恢复到之前的状态。” 为此,我们需要一个回滚自定义操作来撤销安装过程中执行的任何操作。在这种情况下,我们只需要删除事件日志源,因为任何已安装的文件都将由标准安装程序代码自动删除。

public override void Rollback(System.Collections.IDictionary savedState)
{
    if(EventLog.SourceExists(APPNAME))
    {
        EventLog.DeleteEventSource(APPNAME);
    }
    base.Rollback(savedState);
}

3. 卸载

在卸载我们的应用程序时需要三个操作:

  1. 删除数据文件目录。

    数据文件目录在安装时创建,因此安装程序应该自动删除它,所以我们不必担心它。好吧,事实是这仅适用于安装程序最初创建的包含文件。如果您在此目录中创建新文件,这些文件将在应用程序卸载后保留。Killer Application 实际上在数据目录中创建了一个新文件,用于记录应用程序执行情况(请参阅启动顺序),因此我们手动删除此目录。

  2. 删除事件源。

    这里没有什么神秘之处;请注意,即使我们删除了事件源,生成的事件仍然会保留。

  3. 删除 prefetch 目录中的应用程序文件。

    Windows XP 和 Vista 有一个名为 Prefetch 的目录,其中包含最近使用的应用程序的快捷方式,以缩短应用程序启动时间。这些快捷方式可以安全地手动删除,这也是我们对 Killer Application 的预取文件所做的,从而实现完全干净的卸载。

    这是执行所有这些操作的代码。请注意,如果由于某种原因删除数据目录或预取文件失败(如果卸载过程中涉及的文件夹在资源管理器中打开,则可能会发生这种情况),用户会收到警告,以便至少他可以手动删除这些文件。

    protected override void OnAfterUninstall(System.Collections.IDictionary savedState)
    {
        //* Delete data files
    
        string path=ApplicationDataPath;
        if(Directory.Exists(path))
        {
            try
            {
                Directory.Delete(path, true);
            }
            catch(Exception ex)
            {
                MessageBox.Show(string.Format(
    @"Error when trying to delete the program data folder:
    ({0}) {1}
    
    Uninstall process will continue, but the folder will not be deleted.
    The folder path is:
    {2}", ex.GetType().Name, ex.Message, path), APPNAME + " uninstaller",
    MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }
    
        //* Delete the event source
    
        if(EventLog.SourceExists(APPNAME))
        {
            EventLog.DeleteEventSource(APPNAME);
        }
    
        //* Delete file from Prefetch folder
    
        string prefetchPath=Path.Combine
            (Environment.ExpandEnvironmentVariables("%windir%"), "Prefetch");
    
        try
        {
            string[] files=Directory.GetFiles(prefetchPath, APPFILE+"*.*");
            foreach(string file in files)
            {
                File.Delete(file);
            }
        }
        catch
        {
            string prefetchDir=Path.Combine
                (Environment.ExpandEnvironmentVariables("%windir%"), "Prefetch");
            MessageBox.Show(@"Some "+APPNAME+" files may remain in the
                "+prefetchPath+" directory. " +
                "You can delete these files manually.",
                APPNAME + " uninstaller", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    
        base.OnAfterUninstall(savedState);
    }
    

至此,我们完成了 Killer Application 源代码的解剖。现在让我们进入安装程序项目。

安装程序

准备应用程序源代码和调整应用程序项目设置只是我们在认证道路上需要完成工作的一部分。在我们的应用程序完全符合认证要求之前,还有最后一项重要工作要做:为我们的应用程序创建和调整安装程序。

为了获得认证,我们的应用程序必须使用 Windows Installer 技术进行安装(Click Once 部署也接受,但我们在这里不讨论此可能性)。这意味着我们必须生成一个 MSI 格式的安装程序文件,其中包含适当打包的应用程序,包括所有安装逻辑(安装时显示的 GUI、自定义操作以及将在 Windows 注册表中生成的必要元数据,以便 Windows 知道如何正确卸载应用程序)。

好消息是我们可以使用 Visual Studio 创建这样的安装程序,但当然,如果我们想要一个完全符合测试用例的安装程序,我们就需要稍微作弊一下。而如何实现这一点,就是我们现在将要看到的。

第一步:创建安装程序项目

安装程序项目是应用程序解决方案的一部分,但在解释如何创建解决方案时,我特意没有提到它,因为我想让安装程序在本篇文章中有一个单独的部分。所以,这就是我为 Killer Application 创建安装程序项目所做的事情:

  1. 加载 Killer Application 解决方案后,在 Visual Studio 文件菜单中,选择 添加 -> 新建项目
  2. 在“添加新项目”对话框中,选择 其他项目类型 -> 设置和部署 -> 设置项目。在项目名称文本框中,输入 Killer Application installer
  3. 右键单击解决方案资源管理器中的新安装程序项目,选择 属性。在 配置 下拉列表中选择 所有配置,然后在 输出文件名 文本框中,输入 bin\Killer Application 1.0.msi

至此,您已经为我们的应用程序创建了一个空的安装程序项目。现在我们需要进行一些初步调整。

第二步:调整项目属性

当解决方案资源管理器中选中安装程序项目时,项目属性窗口将显示一些我们可以修改的配置值。其中一些默认值是合适的,其他一些可以填写但也可以留空,最后还有一些值必须正确设置,如果我们希望事情正常工作的话。这些值如下:

  • InstallAllUsers 必须设置为 True,因为我们的应用程序始终是为每个计算机安装,而不是为每个用户安装。
  • Localization 必须设置为相应的值,该值必须与您的应用程序旨在获得认证的 Windows 语言匹配。对于 Killer Application,它是 西班牙语(西班牙),就像我帮助认证的应用程序一样,在您的应用程序中,您必须适当地设置它。
  • Product name:将其更改为应用程序名称,在本例中为 Killer Application
  • Title 也是如此,在本例中为 Killer Application 1.0
  • 确保 Version 与应用程序版本号匹配,在本例中默认值 1.0.0 即可。

还有一个额外的设置需要更改,但它不在项目属性窗口中。您需要打开用户界面编辑器(它是解决方案资源管理器文件夹中的一个图标),选择 安装文件夹 窗口,然后在属性窗口中,将 InstallAllUsersVisible 设置为 False。这是使工作正常运行所需的最低设置,但您可能还想设置其他有用属性的值,如 DescriptionManufacturerManufacturerUrlSupportUrl。应用程序安装后,此信息可以在 Windows 控制面板中显示,更具体地说是在 添加/删除程序 窗口中。

第三步:添加要安装的文件

到目前为止,我们的安装程序应用程序是相当无用的,因为它不安装任何东西。我们需要告诉 Visual Studio 哪些文件必须打包到 MSI 文件中,这通过文件系统编辑器(可从解决方案资源管理器访问)来完成。那么,屏住呼吸,开始吧:

  1. 移除 用户桌面 文件夹(除非您想在应用程序安装时在用户桌面上创建一个应用程序图标,在 Killer Application 中我没有这样做)。
  2. 右键单击 应用程序文件夹,选择 添加 -> 项目输出。为 Killer Application 选择主要输出。
  3. 为 `KillerApplication.Support` 重复上述步骤,选择其主要输出。
  4. 右键单击 用户程序菜单,选择 添加 -> 文件夹,将新文件夹命名为 Killer Application
  5. 返回 应用程序文件夹,右键单击 Killer Application 的主要输出,选择 创建快捷方式。将快捷方式命名为 Killer Application
  6. 剪切您创建的快捷方式,并将其粘贴到 用户程序菜单 - Killer Application 文件夹中。
  7. 右键单击文件系统根目录(标记为 目标计算机上的文件系统),选择 添加特殊文件夹 - 公共文件文件夹
  8. 右键单击 公共文件文件夹,选择 添加 -> 项目输出。为 `KillerApplication.Install` 选择主要输出。
  9. 右键单击文件系统根目录(标记为 目标计算机上的文件系统),选择 添加特殊文件夹 - 自定义文件夹。将新文件夹命名为 Killer Application
  10. 选择您创建的自定义文件夹,打开属性窗口,将 DefaultLocation 设置为 [CommonAppDataFolder]\Killer Application,将 Property 设置为 COMMONAPPDATAFOLDER
  11. 右键单击自定义文件夹,选择 添加 -> 项目输出。为 Killer Application 选择内容文件。

呼。好吧,您可以在 Killer Application 安装程序项目中看到这项工作的成果。请记住,我们已经看到文件系统编辑器应该是什么样子的第一部分。

Killer Application folder in installer project

关于此点的最后说明。第一次编译安装程序项目时,您将在结果窗口中看到以下内容:

警告:两个或多个对象具有相同的目标位置('[targetdir]\capsule.killerapplication.support.dll'

现在查看安装程序项目中的 检测到的依赖项 文件夹,您将看到已添加对 KillerApplication.Support.dll 的引用。这发生是因为 Visual Studio 检测到支持 DLL 是主项目的依赖项,因此将其添加到了要打包的文件列表中;但我们已经在文件系统编辑器中将支持项目的项目输出添加到应用程序文件夹时完成了此操作;因此我们有两个副本。解决方案:右键单击依赖项文件夹中的文件引用,然后选择 排除

第四步:添加自定义操作

我们在 `KillerApplication.Install` 程序集中为安装程序自定义操作创建了代码,但除非我们明确指示,否则安装程序不会将此代码视为安装自定义操作。为了实现这一点,我们需要执行以下操作:

  1. 在解决方案资源管理器中,选择自定义操作编辑器。
  2. 右键单击 Install 文件夹,选择 添加自定义操作
  3. 在项目中选择项 窗口中,选择 Common Files 文件夹,然后选择 `KillerApplication.Install` 的主要输出。
  4. 将要求为自定义操作输入名称,输入 “创建事件源并授予用户对数据目录的权限”
  5. Rollback 文件夹重复上述步骤,这次为自定义操作命名 “安装错误时删除事件源”
  6. Uninstall 文件夹重复上述步骤,这次将自定义操作命名为 “删除事件源和数据文件”

实际上,您可以为自定义操作赋予任何您喜欢的名称,但使用有意义的名称以保持清晰是一个好主意。完成时,自定义操作编辑器应如下所示:

Killer Application installer custom actions editor

第五步:使您的应用程序仅限 XP2+ 或 Vista(可选)

您可能希望您的应用程序需要最低版本的 Windows 操作系统才能运行。我们可以指示安装程序,如果检测到较低版本,则拒绝工作,以下是所需步骤:

  1. 在解决方案资源管理器中,选择启动条件编辑器。
  2. 右键单击 Launch Conditions 文件夹,选择 添加启动条件
  3. 将要求输入名称,输入 Windows XP SP2 或更高版本。”
  4. 选择您刚刚创建的条件,打开属性窗口,并将 Condition 属性设置为以下值:(VersionNT=501 AND ServicePackLevel>=2) OR VersionNT>501。将 Message 属性设置为以下文本:Killer Application 需要 Windows XP SP2 或更高版本。推荐使用 Windows Vista。

这将使您的应用程序只能在 Windows XP SP2 或更高版本(Windows 2003、Windows Vista、Windows 2008 以及将出现的新版本)上安装。如果您想更严格,并使您的应用程序仅与 Vista 或更高版本兼容,请将条件设置为:VersionNT>=600。有关操作系统版本条件的更多详细信息,请参阅 此处

第六步:添加转换文件

我们将由我们的安装项目生成的 MSI 文件将缺少一些重要数据,这些数据是获得认证所必需的。更具体地说:

  • 测试用例 18 说:“验证应用程序是否创建了卸载注册表项和值。” 但是,如果您用 Orca 打开 MSI,您会发现 InstallLocation 键丢失了。
  • 测试用例 25 说:“验证应用程序在安装过程中是否正确处理正在使用的文件。” 但是,再次用 Orca 打开 MSI,您会发现 MsiRMFilesInUse 不存在。

为了解决这些问题,我们需要使用 Orca 创建两个转换文件,并将它们应用于 MSI 文件。更具体地说:

  1. 根据 这个 MSDN 论坛条目(搜索 Derek Sanderson 的已接受答案)中的说明,创建一个用于测试用例 25 问题的转换文件。将其命名为 AddMsiRmFilesInUse.mst 并保存在 Killer Application 安装程序项目文件夹中。
  2. 再次使用 Orca 创建一个用于测试用例 18 问题的转换文件,这次使用 Simon Williams 这篇文章(搜索安装/卸载部分)中提供的数据。将其命名为 VistaPatch2.mst 并保存在 Killer Application 安装程序项目文件夹中。
  3. 右键单击解决方案资源管理器中的安装程序项目,选择 添加 -> 文件,浏览到安装程序项目文件夹,然后选择 AddMsiRmFilesInUse.mstVistaPatch2.mst 文件。
  4. 一旦这些文件被添加到项目中,在解决方案资源管理器中选择它们,然后在属性页面中将 Exclude 设置为 True(我们不希望这些文件被打包到 MSI 文件中)。

这些转换文件将在生成 MSI 文件时对其进行修补。要实现这一点,当然我们需要一个后置生成事件,这就是我们现在要创建的。在解决方案资源管理器中选择安装程序项目,打开属性页面,然后在后置生成事件框中粘贴以下内容:

%HOMEDRIVE%\vistatools\MsiTran.exe -a "$(ProjectDir)VistaPatch2.mst" "$(BuiltOuputPath)"
%HOMEDRIVE%\vistatools\MsiTran.exe -a "$(ProjectDir)AddMsiRMFilesInUse.mst"
    "$(BuiltOuputPath)"

请注意,为了能够创建转换文件,您需要用 Orca 打开 MSI 文件……而我们还没有生成它。为了解决这个“先有鸡还是先有蛋”的问题,请先编译安装程序项目而不带转换(然后删除或注释掉后置生成事件命令),以便您得到一个初始 MSI 文件来处理。警告:您可以在本页下载的 Killer Application 解决方案中包含的 AddMsiRMFilesInUse.mst 的所有 UI 消息都是西班牙语。您应该用您自己的语言创建自己的转换文件。

第七步:添加 NoImpersonate 补丁

生成的 MSI 文件存在一个微妙的问题。如果您尝试通过直接运行它(而不是运行 setup.exe)来安装您的应用程序,您将收到一个烦人且奇怪的 意外错误 2869 窗口,并且整个安装过程将停止。这与自定义操作与出色的、超级的、令人惊叹的 Vista 用户访问控制的交互有关。

这个问题的解决方案在 Hunter555 的这个博客条目 中。您需要在安装程序项目目录中创建一个名为 NoImpersonate.js 的文件。将该博客条目中的脚本代码粘贴到文件中,然后像处理清单文件一样进行:将其添加到安装程序项目,并将其 Exclude 属性设置为 True。该脚本将在 MSI 文件生成后对其进行适当修改,使其在直接执行时不会产生任何错误。为了实现这一点,我们需要在安装程序项目的后置生成事件中添加以下命令:

cscript.exe "$(ProjectDir)NoImpersonate.js" "$(BuiltOuputPath)"

cscript.exe 是 Windows 脚本主机,并且已经包含在操作系统中。

第八步:调整 setup.exe(可选)

如果计划分发 Visual Studio 与 MSI 文件一起生成的 setup.exe 文件,则只需要执行本节将要介绍的调整。理论上,只分发 MSI 文件就足够了,但以防万一,我还是将 setup.exe 与 MSI 文件一起提交给了测试机构。

随便。如果您计划分发 setup.exe 文件,则需要在安装程序项目中执行以下附加步骤:

  1. 创建清单文件

    测试用例 13 说:“验证应用程序的安装程序是否包含嵌入式清单”。所以让我们开始吧:打开您喜欢的文本编辑器,在安装程序项目目录中创建一个名为 setup.exe.manifest 的文件。此文件的内容必须如下:

    <?xml version="1.0" encoding="utf-8" ?>
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
      <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
        <security>
          <requestedPrivileges>
            <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
          </requestedPrivileges>
        </security>
      </trustInfo>
    </assembly>

    (嗯,我的文本编辑器支持 UTF-8 编码。如果您的不支持,请相应地修改 XML 声明中的编码属性。)

    现在右键单击解决方案资源管理器中的安装程序项目,选择 添加 -> 文件,浏览到安装程序项目文件夹,然后选择您刚刚创建的 setup.exe.manifest 文件。在解决方案资源管理器中选择文件,然后在属性页面中将 Exclude 设置为 True。请注意,此文件与我们在应用程序主可执行文件中嵌入的文件不同,因为它请求的执行级别是 requireAdministrator。确实,我们需要管理员权限才能安装我们的应用程序(这是自定义操作执行所需的功能)。

  2. 扩展后置生成事件

    我们已经创建了一个不错的清单文件,现在我们需要创建一个将处理它的命令。所以将以下两个命令添加到安装程序的后置生成事件框中(请注意,一如既往,为了易于阅读,命令被拆分到两/三行中):

    %HOMEDRIVE%\vistatools\Mt.exe -manifest "$(ProjectDir)setup.exe.manifest"
        -outputresource:"$(ProjectDir)$(Configuration)\setup.exe;1"
    %HOMEDRIVE%\vistatools\signtool sign /f  %HOMEDRIVE%\vistatools\capsulekey.pfx
        /p kaitokun /v /t http://timestamp.verisign.com/scripts/timstamp.dll
        "$(ProjectDir)$(Configuration)\setup.exe"

    请注意,我包含了对 setup.exe 文件进行签名的代码。我认为这不必要(测试用例文档中未提及),但无论如何,签名此文件并无坏处,如果您计划执行 此类高级操作,它甚至可能很有用。

第九步:开派对!

信不信由你,但我们完成了。如果您遵循了这篇相当长的文本中解释的所有步骤,您将拥有一个漂亮且 100% Vista 可认证的应用程序以及一个同样不错的安装程序。您可以喝一两杯啤酒(请记住:如果您喝酒,请不要编码)。总而言之,这是完成所有工作后 Killer Application 解决方案的解决方案资源管理器外观:

Killer Application complete solution explorer
KillerApp_contentfiles.png

重新审视测试用例

为了完成这篇文章(是的,我发誓这是最后一节),我们将采用逆向方法来处理测试用例。我们已经解剖了一个示例应用程序,并适当地提到了涉及的测试用例。现在我们将列出测试用例,并提及我们为满足它们所做的工作。请记住:不要相信我,我很糟糕,所以请在将您的应用程序提交给测试机构之前,将您的应用程序针对所有适用的测试用例进行测试。

简单部分

有几个案例不适用,也就是说,您甚至不需要关心它们。请记住,对于结构上与 Killer Application 相似(这是一个很好的词,我必须更多地使用它)的应用程序来说,这是真实的;如果您实际上需要检查其中任何一个案例,请务必仔细检查。

  • 测试用例 4。验证应用程序安装程序不包含 16 位安装程序,不使用或依赖 16 位代码或组件,并且在 x64 版本的 Windows 上不尝试安装任何非 64 位驱动程序,无论应用程序是 Win32 应用程序还是 64 位原生应用程序。

    (.NET 应用程序中没有 16 位内容,我们也不安装驱动程序)
  • 测试用例 6。验证应用程序安装的所有内核模式驱动程序是否已签名。

    (我们不安装驱动程序)
  • 测试用例 10。验证驱动程序和服务是否在安全模式下启动。

    (我们不安装驱动程序或服务)
  • 测试用例 14。验证应用程序是否使用已安装用户的令牌启动。

    (我们的应用程序在安装后不会启动)
  • 测试用例 16。验证 ClickOnce 应用程序是否使用有效的 Authenticode 证书签名。

    (我们的应用程序不使用 ClickOnce 进行部署)
  • 测试用例 17。验证 ClickOnce 应用程序是否仅将数据存储在已安装用户的文件夹中,并且在安装期间不写入 WRP 注册表项。

    (我们的应用程序不使用 ClickOnce 进行部署)
  • 测试用例 31。验证应用程序在指定 AppVerifier 检查下不会中断到调试器。

    (我们的应用程序是完全托管的)

现在,让我们看看我们真正关心的测试用例。

重要的测试用例

请注意,每个测试用例只有一两句话,因为我们只是引用了文章其余部分中已详细介绍的概念。

  • 测试用例 1。验证应用程序的所有可执行文件都包含一个嵌入式清单,用于定义其执行级别。

    我们在生成 EXE 文件的项目中手动包含了一个清单文件,并设置了项目生成后事件,使用 mt.exe 在生成文件后将其嵌入可执行文件中。

  • 测试用例 2。验证低特权用户无法修改其他用户的文档或文件。

    每当我们写入文件时,我们会捕获可能的 UnauthorizedAccessException 并执行适当的纠正措施,例如告知用户缺少适当的权限。

  • 测试用例 3。验证低特权用户无法将文件保存到 Windows 系统目录。

    与测试用例 2 相同,但一个设计良好的应用程序不应尝试写入 Windows 目录。

  • 测试用例 5。验证已安装的应用程序可执行文件和文件已签名。

    我们使用生成后事件,通过 signtool.exe 和我们的组织证书对所有生成的 EXE 和 DLL 文件进行签名。

  • 测试用例 7。验证应用程序正确检查操作系统版本。

    .NET Framework 会为我们处理,我们无需执行任何特殊操作。

  • 测试用例 8。验证应用程序在快速用户切换下能够正常启动和执行。

    在启动代码中,我们检查应用程序是否已被另一个用户运行,如果是,我们显示错误消息,在 Windows 事件日志中写入消息,并终止执行。

  • 测试用例 9。验证应用程序在远程桌面下能够正常启动和执行。

    在启动代码中,我们检查应用程序是否正在通过远程桌面运行,如果是,我们显示错误消息,在 Windows 事件日志中写入消息,并终止执行。

  • 测试用例 11。验证应用程序安装程序使用 Windows Installer。

    是的,Visual Studio 创建的安装程序依赖于 Windows Installer 技术。

  • 测试用例 12。验证应用程序的 MSI 安装程序不会从内部一致性评估器 (ICE) 收到任何错误。

    执行测试用例步骤,您将看到不会收到任何错误。但请注意,您可能会在测试用例规范指定的 ICE 范围内收到警告(非错误),但这不影响认证。

  • 测试用例 13。验证应用程序的安装程序包含嵌入式清单。

    如果您将 Visual Studio 生成的 setup.exe 文件与 MSI 文件一起分发,则此项适用。我们以与应用程序主可执行文件相同的方式(参见测试用例 1)手动将清单包含在此文件中。

  • 测试用例 15。验证应用程序默认安装到正确的文件夹。

    我们通过将应用程序数据文件安装到公共应用程序数据文件夹来实现这一点。在启动时,我们会获取并存储这些数据文件的正确路径。

  • 测试用例 18。验证 Windows Installer 包包含 Manufacturer、ProductCode、ProductLanguage、ProductName、ProductVersion(主版本和次版本)以及 UpgradeCode 属性标签,并且它们不为空。

    Visual Studio 生成的 MSI 文件确实包含这些属性。

  • 测试用例 19。验证应用程序创建卸载注册表项和值。

    Visual Studio 生成的 MSI 文件包含这些属性,但 InstallLocation 除外。为解决此问题,我们使用生成后事件,通过 msitran.exe 在 MSI 文件生成后对其应用转换。

  • 测试用例 20。验证应用程序不尝试写入或替换任何 WRP 注册表项或文件。

    Visual Studio 生成的安装程序不会尝试做这种不好的事情。无论如何,这里有一个应用程序可以为您解析 AppVerifier 日志,并告诉您是否有任何内容会导致此测试用例失败。

  • 测试用例 21。验证应用程序不使用嵌套安装自定义操作。

    我们使用自定义操作,但它们不属于被禁止的类型。

  • 测试用例 22。验证应用程序不向 Windows Installer 的标准表添加自定义列,并且任何自定义表或属性都不会以 'msi' 作为前缀。

    这相当繁琐。不过,Visual Studio 生成的安装程序不会添加这些不好的自定义列、表或属性。

  • 测试用例 23。验证应用程序回滚安装并将其恢复到之前的状态。

    我们添加了一个回滚自定义操作,以便在安装过程失败时删除安装时创建的 Windows 事件日志源。请注意,在执行测试用例步骤时,必须使用 FailInstallFromDeferredCustomAction.msm 模块而不是 FailInstallFromCommitCustomAction.msm 模块。

  • 测试用例 24。验证应用程序在安装过程中不强制重启。

    安装后不会强制重启,这里不需要特殊操作。

  • 测试用例 25。验证应用程序在安装过程中正确处理正在使用的文件。

    Visual Studio 生成的 MSI 文件中没有 MsiRMFilesInUse 对话框的踪迹。为解决此问题,我们再次使用生成后事件,通过 msitran.exe 在 MSI 文件生成后对其应用转换。

  • 测试用例 26。验证应用程序可以从命令行静默安装。

    我们的应用程序将通过此测试用例,因为我们强制进行每个计算机安装;如果我们进行每个用户安装,则该测试用例将失败。如果您确实需要,肯定会有解决每个用户安装下通过此测试的变通方法,但这需要您自己去寻找,我的朋友。

  • 测试用例 27。验证应用程序的 Windows Installer ComponentID 表不包含空值。

    Visual Studio 生成的安装程序中的指定表没有 null 值。

  • 测试用例 28。验证应用程序的 Windows Installer 包不对每个组件包含多个 COM 服务器。

    同样,我们的安装程序将通过此测试用例,无需我们采取任何措施。

  • 测试用例 29。验证应用程序的 Windows Installer 包不对每个组件包含多个快捷方式。

    同上。

  • 测试用例 30。验证应用程序已实现 Restart Manager 感知。

    AmitavaSimon Williams 解释了如何使 .NET 应用程序 Restart Manager 感知。基本上,您需要捕获 WM_QUERYENDSESSION WM_ENDSESSION Windows 消息,并执行适当的操作,例如保存用户数据以供以后恢复。

    您可能已经注意到,Killer Application 代码中没有关于这方面的内容。事实上,虽然捕获会话结束消息并妥善准备关机当然是良好的实践,但实际上并不需要这样做来认证应用程序。简而言之,实际上不需要采取任何特殊措施即可通过此测试用例。

  • 测试用例 32。验证应用程序只处理已知和预期的异常。

    我们将应用程序的 Main 方法的执行包含在一个 try-catch 块中,以便在发生意外异常时生成 Windows 事件日志中的必需条目。在进行日志记录后,通过 Throw 命令(而不是 Throw exThrow new Exception)重新抛出原始异常。

    此外,在应用程序启动时,我们将未处理异常模式设置为 UnhandledExceptionMode.ThrowException,以便在发生异常时实际出现 WER 窗口。

最后说明

我希望所有这些废话对您有所帮助。请记住,我所解释的是获得认证的一种方法,而不是唯一的方法。如果您认为有更好的方法来完成这项工作,欢迎在评论区提出。

历史

  • 2008 年 1 月 9 日:第一个版本
© . All rights reserved.