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

使用 Visual Studio 2010 的安装程序和补丁程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2010年11月8日

CPOL

18分钟阅读

viewsIcon

69303

downloadIcon

2266

本指南介绍如何替换 Visual Studio 内置的 ClickOnce 技术,并使用 Visual Studio 工具控制安装/更新过程。

引言

我的目标是创建一个解决方案,能够首次安装 .NET 应用程序,并在其生命周期内保持更新。我之前尝试过使用 ClickOnce 来完成这些任务,但发现它很麻烦,而且在证书和签名方面遇到了不少问题,导致我之前的安装无法更新(因此无法更新),尤其是在多个开发人员处理同一个解决方案时。我决心创建一个安装程序和更新程序,它不会每次运行时都要求管理员权限,并且能够处理大多数实际任务,同时允许我为最终用户保持简单的安装。它必须与所有常用的 Windows 技术兼容,并允许我发布生产版本和 BETA 版本更新。我是一名自学程序员,并且完全期望从我的错误中学习,所以如果你发现任何错误,请随时指出。我已经对此进行了广泛的测试,并相信我已经找到了所有/任何 bug。

贡献者鸣谢(我使用了这些代码片段/库)

版本控制

我希望能够通过修补程序实用程序来处理应用程序版本,因此我的 AssemblyInfoUtil.cs 类的学分归功于 CodeProject 上的这篇文章,它创建了一个修改版本号并在我进行修补时重新编译的类。我的程序使用这些版本号。

压缩和密码保护

我需要能够压缩并将输出至少半保护地发布到网上,所以我使用了来自这里的 ICSharp 库。

安装程序退出时自动启动应用程序

我需要能够“更新”安装程序并保持应用程序流程的连续性,所以我选择使用一个脚本来自动启动应用程序。从这篇CodeProject 文章中,我找到了一个链接指向这个MSDN 博客,我学会了如何在 Visual Studio 安装程序末尾添加一个复选框来自动启动程序。当从我的启动器以 /passive 模式运行时,它会立即关闭并执行更新后的启动器,没有任何用户反馈,而在首次安装时则会有用户反馈。

使用选项

  • 选项 1 - 即时部署
    • 下载 Zip 文件
    • 在 IIS 或您选择的 Web 服务器上创建一个网站,关闭 Web 缓存。
      • 如果您没有本地访问权限(例如 \\web1\d$\websites),您需要修改代码以使用 FTP。
    • 修改启动器窗体,添加 Logo、版权信息等。
    • 将您的项目引用添加到 SelfUpdatingProgram
    • 修改 SelfUpdatingProgram Program.cs 文件,使其启动您的项目而不是 Form1.cs
    • 编辑 SelfUpdatingSettings 中的 Updater.settings 文件,确保所有设置都与您的设置匹配。
      • 您的应用程序可执行文件将是 SelfUpdatingProgram.EXE,除非您更改了项目名称。
      • 公司名称将是您的子文件夹和文件将驻留的文件夹名称(通常在 C: 盘上,但会根据其系统进行调整)。
    • 以 Release 模式重新生成整个解决方案,然后生成 Setup 解决方案。
      • 修改 Setup 的属性,设置公司名称、支持、开发者等信息。
      • 在 Setup 区域的“查看文件夹”中,相应地重命名快捷方式链接。
      • 注意:为了在初始部署后更新 SETUP,您必须单击 setup 项目,进入 Properties (F4),然后增加其中一个版本号。
      • 当提示时,选择 YES 更新 Product Code。
      • 每次对 Launcher 程序进行更改后,您都必须手动重新生成您的 setup 项目。
    • SelfUpdatingPatcher 设置为 StartupProject 并运行它。
    • 设置您的起始版本,然后点击 Production。
    • 验证所有文件是否已正确部署到网站(如果驱动器是本地驱动器,您可以从 Patcher 进行此操作)。
    • 基本设置可以通过直接链接到生产 MSI 文件(唯一未压缩的文件)来实现。
      • 注意:运行 .msi 文件时,它不会检查并协助下载所需组件(例如 .NET)。
    • 高级设置可用于将 setup.exe.msi 文件打包成 IEXPRESS 包,并使用此包作为您的永久安装链接。
      • 警告:如果您在 64 位计算机上执行此操作,则该包在 32 位计算机上将无法正常工作。反之,如果您在 32 位计算机上执行此操作,则它可以在两者上工作。
      • 在命令提示符下,键入 IEXPRESS.exe 并按照说明进行操作,将起始文件设置为 setup.exe
      • 创建 IEXPRESS 包后,删除新创建的 EXE 文件,编辑创建的 .SED 文件,并将 setup.exe 行修改为“setup.exe /passive”。
      • 再次运行 IEXPRESS 加载 SED,但跳过修改,只构建您的输出(如果未修改到 SED 中,则会忽略 /passive)。
      • 您的安装现在将在用户点击运行时自动执行(文章末尾有更多截图和说明)。
  • 选项 2 - 自行实现
    • 下载源包,逐行创建模仿项目,进行必要的更改以实现完全自定义。
    • 我建议从头开始创建一个新解决方案并实现各个部分,而不是尝试修改下载的包。
    • 完成后,请参考选项 1 并使用新解决方案。

项目布局

SelfUpdatingProgram

  • 此项目是您将要运行的实际软件。
  • 注意:对我来说,这个项目是一个 MDI 父窗体,它包含对我想包含的其他项目的多个引用。
    • 此级别或更高级别的所有相关项目都必须以“Release Mode”编译,否则修补程序将找不到它们。
    • 此项目下无需提供详细信息,它就像您的主项目一样简单,或者说它执行您的主项目。
      • 无需“替换此项目”,只需添加对现有项目的引用,并将 Program.cs 指向启动它。
      • 通过 DLL 添加到此项目的项目将在您重新编译它们并重新生成此项目时自动更新。
        • 添加的 DLL 将是自更新的,即如果您添加了一个来自解决方案的 bin/release 目录中的 DLL,那么每次您在 release 模式下编译该解决方案时,它都会更新引用,并在下次 Patcher 运行时推送新文件。

SelfUpdatingLauncher

  • 此项目将包含在安装文件中,并在修补后启动您的软件项目。您应用程序使用的所有文件将存储在用户“Program Files”或“Program Files<x86>”驱动器的根目录下。应该注意的是,默认实现不检查“可用”空间,并且在错误消息方面非常慷慨。有许多文章对此进行了广泛的介绍。默认的窗体设置效果很好,并且允许很多自定义。我见过一些 XP 机器上显示透明边框不正确,但仍然不算太差。左侧的默认按钮设置是“options”按钮,再次点击会停止自动关闭,再点击一次会停止自动启动。X 按钮表示关闭窗体。

SelfUpdatingPatcher

  • 此项目负责更新您的版本号、压缩您的文件、添加密码保护,然后上传。盒子的宽度可能需要调整以匹配您的版本控制方案。

SelfUpdatingSettings

这是一个通用的项目,我可以在所有其他项目中重用类和设置。

SelfUpdatingSetup

这是一个几乎永远不会更改的安装项目,但如果更改了,它将被作为补丁下载,它只包含启动器的输出、所需的 .NET 版本以及任何需要安装在 GAC 中的组件。

如果您必须更新 Setup 文件(例如,更新了需要放在 GAC 中的组件、修复 bug 或 .NET 升级),您还必须增加 setup 项目的版本号。要做到这一点,请单击 setup 项目,然后按 F4 进入属性。将版本号增加一,并在提示时选择 Product Code。启动器将检测到更新的 Setup 文件,Setup 文件将启动,但除非版本号增加,否则它不会更新。

要更新 Launcher,您需要包含一个新的 Setup 文件。对 Launcher 进行更改,单击 SelfUpdatingSetup ,然后按 F4。单击 Version 字段并将版本号增加 1,例如,如果它显示为 1.0.0,则将其更改为 1.0.1。

然后会提示您更新 Product Code,如下所示。

然后,您需要重新生成 Setup 项目,以便 Patcher 能够识别更改。

其他注意事项

您将需要一个 Web 服务器来托管补丁,我正在使用 Windows Server 2008 中的 IIS,但任何服务器都可以。这里的关键设置是关闭缓存,否则客户端将需要相当长的时间才能看到您的更新。您还会注意到我正在使用直接路径上传我的补丁,您可能需要将其替换为 FTP 到您的 IIS 文件夹(CodeProject 上有很多示例,但我强烈建议即使是 FTP 也使用 WebClient),我选择直接路径是因为我们的服务器被阻止访问外部,所以我只能在我在办公室或连接 VPN 时才能上传文件。

为了方便使用,我建议将所有项目放在同一个父文件夹中(您将在 SelfUpdatingSettings 中看到相关的设置要求)。

如果您自行实现了 Setup 或 Launcher,则必须将 EnableLaunchApplication.js 包含在 Setup 文件夹中,并编辑它以包含您的 Launcher Program.EXE 的新名称(如有必要,请参考上述文章)。

创建项目

此时,您可能已经有一个项目了,只需将 Launcher、Patcher 和 Settings 添加到其中即可。稍后我们将创建 Setup。

Settings 项目

  • 注意:我添加了一个文件夹和 ICSharpCode 二进制文件的副本以供将来使用,以确保我始终使用相同版本的二进制文件。
  • 单击 Add New Project,然后选择 Class Library Project。
  • 删除默认的类。
  • 选择新项目,然后单击 Add -> New Item -> Settings file。将其命名为适当的名称(我的名称是 Updater.settings)。
  • 将 Access Modifier 设置为 Public,并在 Application Scope 下添加这些字符串设置。
    • ApplicationExecutable  -> SelfUpdatingProgram.EXE
    • UriRepositoryDefault  ->  http://xxx.xxx.xxx/SUP/Repository/
    • ProjecctReleaseFolder  ->  ..\..\..\SelfUpdatingProgram\bin\Release
    • SetupReleaseFolder  ->  ..\..\..\SelfUpdatingSetup\Release
    • AppSubFolderBeta  ->  Beta
    • AppSubFolderProd  ->  Prod
    • AppSubFolderSetup  ->  Setup
    • ZipFilePassword  ->  [good password here]
    • VersionFile  ->  ..\..\..\SelfUpdatingProgram\Properties\AssemblyInfo.cs
    • RepositoryDefault -> //web1/d$/websites/website/Repository/  (应替换为您的直接连接或带 FTP 的代码块)
    • RepositoryExistsPath  ->  //web1/d$/websites/website/
    • SoftwareManifestFileName  ->  software_manifest.xml
    • Company Name -> Sup Inc (这是用于存储您的文件的文件夹名称,显然不能包含非法字符)
    • MSBuild_WorkingDirectory -> MSBuild 对于在不加载 Patcher 两次的情况下合并您的版本更改是必不可少的。
Click to enlarge image
  • 添加必需的类(最好放在名为 Classes 的文件夹下)。
    • AssemblyInfoUtil.cs, ExecuteModeEnum.cs, CheckForUpdate.cs, 和 StaticClasses.cs(见下文)。

AssemblyInfoUtil.cs

此类仅从 SelfUpdatingProgram 加载程序集版本信息,并可选地将新版本信息写入文件,然后执行新编译的文件。它主要在 Patcher Program 中使用。在类实例化时,它将使用 Settings 作为默认值,或者您可以根据需要进行覆盖。一旦它有了文件名,它会检查是 VB 还是 CS,然后读取文件的行,寻找修改版本的行。它将这些版本存储在类的四个 public int 变量中。它包含一个 Writeline 方法,该方法执行与读取相同的代码,并带有可选的“Write”更改的开关。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Diagnostics;

namespace SelfUpdatingSettings.Classes
{
    public class AssemblyInfoUtil
    {
        /// <summary>
        /// Loads the AssemblyUtil.cs file from the Updater.sessings 
        /// location to read and then optionally update the version number
        /// </summary>
        /// <param name="fullFileLocation">Default Blank - 
        /// Sets the location of the version file</param>
        public AssemblyInfoUtil(string fullFileLocation = "")
        {
            if (fullFileLocation == string.Empty)
                fullFileLocation = Updater.Default.VersionFile;
            projectFullFileLocation = fullFileLocation;

            // Make sure file exists
            if (!System.IO.File.Exists(projectFullFileLocation))
            {
                MessageBox.Show("Invalid File Name for Assembly File: 
		\r\n" + projectFullFileLocation, "Failed to Load Versioning Assembly", 
		MessageBoxButtons.OK);
                return;
            }

            // Determine if assembly is in VB
            if (Path.GetExtension(projectFullFileLocation).ToLower() == ".vb")
                isVB = true;

            ReadFile();
        }

        private string versionStr = string.Empty;
        public string VersionStr
        {
            get
            {
                return version1.ToString() + "." + version2.ToString() + 
		"." + version3.ToString() + "." + version4.ToString();
            }
        }

        private int incParamNum = 0;

        private int version1 = -1;
        public int Version1
        {
            get { return version1; }
            set
            {
                version1 = value;
                fileisDirty = true;
            }
        }
        private int version2 = -1;
        public int Version2
        {
            get { return version2; }
            set
            {
                version2 = value;
                fileisDirty = true;
            }
        }
        private int version3 = -1;
        public int Version3
        {
            get { return version3; }
            set
            {
                version3 = value;
                fileisDirty = true;
            }
        }
        private int version4 = -1;
        public int Version4
        {
            get { return version4; }
            set
            {
                version4 = value;
                fileisDirty = true;
            }
        }

        public bool fileisDirty = false;

        public string projectFullFileLocation = string.Empty;
        public bool isVB = false;

        /// <summary>
        /// Processes the file line by line to read the version information
        /// </summary>
        private void ReadFile()
        {
            // Read file
            StreamReader reader = new StreamReader(projectFullFileLocation);
            String line;
            try
            {
                while ((line = reader.ReadLine()) != null)
                {
                    line = ProcessLine(line, false);
                }
            }
            catch (Exception exd)
            {
                MessageBox.Show(exd.Message + "\r\n" + exd.StackTrace, 
			"Failed to Load Versioning Information");
            }
            reader.Close();
        }

        /// <summary>
        /// Processes the file line by line an replaces the 
        /// version information with the settings class.
        /// After it is complete it deletes the old assembly, 
        /// writes a new one, then runs the MSBuild compiler on the default project.
        /// </summary>
        public void WriteFile()
        {
            if (fileisDirty)
            {
                // Read file
                StreamReader reader = new StreamReader(projectFullFileLocation);
                StreamWriter writer = new StreamWriter(projectFullFileLocation + ".out");
                String line;

                while ((line = reader.ReadLine()) != null)
                {
                    line = ProcessLine(line, true);
                    writer.WriteLine(line);
                }
                reader.Close();
                writer.Close();
                File.Delete(projectFullFileLocation);
                File.Move(projectFullFileLocation + ".out", projectFullFileLocation);
                using (Process p = new Process())
                {
                    p.StartInfo = new ProcessStartInfo("MSBuild.exe");
                    p.StartInfo.WorkingDirectory = 
			Path.GetFullPath(Updater.Default.MSBUILD_WorkingDirectory);
                    p.StartInfo.Arguments = "/t:Rebuild /p:Configuration=Release";
                    p.Start();
                    p.WaitForExit(50000);
                }
            }
        }

        /// <summary>
        /// Reads a line of the file and optionally will change the version 
        /// information to match the public version settings
        /// </summary>
        /// <param name="line">The incoming file line in string</param>
        /// <param name="performChange">Whether or not to perform a change 
        /// on the line version parameter</param>
        /// <returns>Returns a new line with the changed version</returns>
        private string ProcessLine(string line, bool performChange)
        {
            if (isVB)
            {
                line = ProcessLinePart(line, "<Assembly: AssemblyVersion(\"", false);
                line = ProcessLinePart(line, "<Assembly: AssemblyFileVersion(\"", false);
            }
            else
            {
                line = ProcessLinePart(line, "[assembly: AssemblyVersion(\"", false);
                line = ProcessLinePart(line, "[assembly: AssemblyFileVersion(\"", false);
            }
            return line;
        }

        /// <summary>
        /// Actually processes the line part that patches Assembly Version Information
        /// </summary>
        /// <param name="line">The line in question</param>
        /// <param name="part">The assembly part reading/writing the version</param>
        /// <param name="performChange">Wether or not to commit changes</param>
        /// <returns>Returns a new line with the changed version</returns>
        private string ProcessLinePart(string line, string part, bool performChange)
        {
            int spos = line.IndexOf(part);
            // Make sure line isn't commented out
            int failIndex = line.IndexOf("//");
            if (spos >= 0 && failIndex == -1)
            {
                spos += part.Length;
                int epos = line.IndexOf('"', spos);

                versionStr = line.Substring(spos, epos - spos);
                StringBuilder str = new StringBuilder(line);
                str.Remove(spos, epos - spos);
                str.Insert(spos, VersionStr);
                line = str.ToString();

                string[] version = versionStr.Split((".").ToCharArray());
                foreach (string s in version)
                {
                    if (version1 == -1)
                    {
                        version1 = int.Parse(s);
                        continue;
                    }
                    if (version2 == -1)
                    {
                        version2 = int.Parse(s);
                        continue;
                    }
                    if (version3 == -1)
                    {
                        version3 = int.Parse(s);
                        continue;
                    }
                    if (version4 == -1)
                    {
                        version4 = int.Parse(s);
                        continue;
                    }
                }
            }
            return line;
        }
    }
}        

ExecuteModeEnum.cs

用于控制执行模式(Production 或 Beta)的参数。通用性使我可以在所有项目中重用我的设置项目并访问执行模式。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SelfUpdatingSettings.Classes
{
    public enum ExecuteModeEnum
    {
        //None = 0, // Force a selection
        //Test = 1, // Not Used
        Beta = 2,
        Production = 3
    }
} 

StaticClasses.cs

一小组 static 类,用于处理参数传递和查找默认硬盘。您可以在此处添加其他大小要求检查和其他功能。参数传递功能允许您的安装程序添加一个指向“Program Beta”的快捷方式,并将启动器作为 BETA 运行,然后启动程序作为 BETA 运行。它还会传递您可能用于自定义解决方案的任何其他参数。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace SelfUpdatingSettings.Classes
{
    public static class StaticClasses
    {

        private static string installPath = string.Empty;
        public static string InstallPath
        {
            get
            {
                if (installPath == string.Empty)
                {
                    // obtain the default program files folder based 
                    // upon whether 32 or 64-bit architecture is in play
                    if (8 == IntPtr.Size || (!String.IsNullOrEmpty
		    (Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432"))))
                    {
                        installPath = Environment.GetEnvironmentVariable
					("ProgramFiles(x86)");
                        if (installPath == null)
                        {
                            installPath = "C:\\Program Files (x86)";
                        }
                    }
                    else
                    {
                        installPath = Environment.GetEnvironmentVariable("ProgramFiles");
                        if (installPath == null)
                        {
                            installPath = "C:\\Program Files";
                        }
                    }
                    installPath = Path.GetPathRoot(installPath) + 
				Updater.Default.CompanyName + "\\";
                }
                return StaticClasses.installPath;
            }
        }

        // convert a string array into a string with the specified 
        // separator between each value
        public static string getStringArrayValuesAsString(string[] stringArray, 
		string separator, bool omitEmptyGuid)
        {
            StringBuilder sb = new StringBuilder();
            bool hasEmptyGuid = false;
            foreach (string s in stringArray)
            {
                if (s == Guid.Empty.ToString())
                    hasEmptyGuid = true;
                if (omitEmptyGuid && s == Guid.Empty.ToString())
                {
                    continue;
                }
                if (sb.Length > 0)
                {
                    sb.Append(separator);
                }
                sb.Append(s);
            }
            if (!omitEmptyGuid &&
                !hasEmptyGuid)
            {
                sb.Append(separator);
                sb.Append(Guid.Empty.ToString());
            }
            return sb.ToString();
        }
    }
}

CheckForUpdate.cs

这个类负责 Updater 的繁重工作,可以从正在运行的程序中实例化一个“Check For Update”按钮/操作,但在本实现中它仅在应用程序加载时使用。您需要为该类添加对 ICSharpCode.SharpZipLib.dll 的引用(包含在 Resources 目录中)。

  • 此类中的重要公共属性
    • SetupUpdateNeeded (bool) 告知您是否有新的 Setup 文件。
    • MainUpdateNeeded (bool) 告知您是否需要更新其他文件。
    • UpdateNeeded (DataTable) 一个列出要更新文件的表。
    • FileCount (int) 需要下载的文件数量。
  • 此类中的重要公共方法
    • 类加载,读取传递的 executeMode 以及 StaticClass InstallPath,并设置变量以匹配下载文件的路径、保存文件的位置、启动器路径以及您选择的 ExecutionMode 的 Setup 文件。然后它找到一个合适的临时文件夹来存放文件,并创建 DataTable 来存放需要更新的文件,并执行下载清单文件的方法。(包含文件和哈希字符串的 XML 文件)。关于这个类的要点是,由于我们正在启动的“实际”程序此时没有运行,因此很容易将现有文件与清单进行比较。
    • DownLoadFile (返回 bool)对 UpdateNeeded 表中的每一行执行以处理下载。这允许您在启动器中灵活地控制进度条。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using System.Data;
using System.Net;
using System.Security.Cryptography;

namespace SelfUpdatingSettings.Classes
{
    public class CheckForUpdate
    {
        /// <summary>
        /// Instantiate the Class to check for an update, 
        /// it loads a table with files to be updated.
        /// </summary>
        /// <param name="e">The mode of update</param>
        public CheckForUpdate(ExecuteModeEnum e)
        {
            // Determine what we need to look for (Production, Beta, or Test)
            execMode = e;

            // Setup URLs to download proper manifest
            companyDir = StaticClasses.InstallPath;
            switch (execMode)
            {
                case ExecuteModeEnum.Beta:
                    mainUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderBeta + "/";
                    downloadUri = Updater.Default.UriRepositoryDefault + "/";
                    setupUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderBeta + "/" + 
				Updater.Default.AppSubFolderSetup + "/";
                    launcherPath = companyDir + Updater.Default.AppSubFolderBeta + "\\";
                    setupFilePath = companyDir + Updater.Default.AppSubFolderBeta + 
				"\\" + Updater.Default.AppSubFolderSetup + "\\";
                    break;
                default:
                    mainUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderProd + "/";
                    downloadUri = Updater.Default.UriRepositoryDefault + "/";
                    setupUri = Updater.Default.UriRepositoryDefault + 
				Updater.Default.AppSubFolderProd + "/" + 
				Updater.Default.AppSubFolderSetup + "/";
                    launcherPath = companyDir + Updater.Default.AppSubFolderProd + "\\";
                    setupFilePath = companyDir + Updater.Default.AppSubFolderProd + 
				"\\" + Updater.Default.AppSubFolderSetup + "\\";
                    break;
            }
            manifestFileName = Updater.Default.SoftwareManifestFileName;

            // Set working temporary Path
            tempPath = Path.GetTempPath();
            tempPath = tempPath + System.Guid.NewGuid().ToString() + "\\";
            tempDirInfo = new DirectoryInfo(tempPath);
            if (tempDirInfo.Exists)
            {
                tempDirInfo.Delete(true);
            }
            tempDirInfo.Create();

            // Setup updateNeeded table with files needed to update
            updateNeeded.TableName = "UpdateNeededFiles";
            updateNeeded.Columns.Add("sourcePathName", typeof(string));
            updateNeeded.Columns.Add("sourceFileName", typeof(string));
            updateNeeded.Columns.Add("zippedPathName", typeof(string));
            updateNeeded.Columns.Add("zippedFileName", typeof(string));

            // Download Manifest and check for Setup Update Needed
            DownloadManifest();
        }

        private ExecuteModeEnum execMode = ExecuteModeEnum.Production;
        private bool setupUpdateNeeded = false;
        public bool SetupUpdateNeeded
        {
            get { return setupUpdateNeeded; }
        }

        private bool mainUpdateNeeded = false;
        public bool MainUpdateNeeded
        {
            get { return mainUpdateNeeded; }
        }

        public bool checkSuccess = false;
        public Exception CheckException = null;

        private int filecount = 0;
        public int Filecount
        {
            get { return filecount; }
        }

        private DataTable updateNeeded = new DataTable();
        public DataTable UpdateNeeded
        {
            get { return updateNeeded; }
        }

        private string companyDir = string.Empty;
        public string CompanyDir
        {
            get { return companyDir; }
        }

        private string downloadUri = string.Empty;
        public string DownloadUri
        {
            get { return downloadUri; }
        }

        private string setupFilePath = string.Empty;
        public string SetupFilePath
        {
            get { return setupFilePath; }
        }

        private string launcherPath = string.Empty;
        public string LauncherPath
        {
            get { return launcherPath; }
        }

        private string mainUri = string.Empty;
        private string setupUri = string.Empty;
        private string manifestFileName = string.Empty;
        private string tempPath = string.Empty;
        private DirectoryInfo tempDirInfo;

        /// <summary>
        /// Download and Process the entries in the manifest file for the application
        /// </summary>
        private void DownloadManifest()
        {
            try
            {
                string fileName = tempPath + manifestFileName;
                /// WebClient is a very efficient way to retrieve the manifest file
                using (WebClient client = new WebClient())
                {
                    Uri uri =
                        new Uri(mainUri + "/" + manifestFileName);
                    // Make sure temporary manifest File does not exist, 
                    // if it does dump it
                    if (System.IO.File.Exists(fileName))
                    {
                        System.IO.File.Delete(fileName);
                    }
                    client.DownloadFile(uri, fileName);
                }
                // turn manifest into a dataset
                DataSet maniDS = new DataSet();
                maniDS.ReadXml(fileName);
                filecount = maniDS.Tables["Files"].Rows.Count;

                DataRow updateRow;
                foreach (DataRow row in maniDS.Tables["Files"].Rows)
                {
                    string zippedFileName = (string)row["zippedfilename"];
                    string zippedPathName = (string)row["zippedpathname"];
                    string md5HashedValue = (string)row["md5HashedValue"];
                    string sourceFileName = (string)row["sourceFileName"];
                    string sourcePathName = (string)row["sourcePathName"];
                    string sourceFullPath = companyDir + 
			sourcePathName.Substring(1) + "\\" + sourceFileName;
                    // Check the file for an update
                    bool fileNeeded = downloadOfFileNeeded
				(sourceFullPath, md5HashedValue);
                    if (fileNeeded)
                    {
                        if (!mainUpdateNeeded)
                            mainUpdateNeeded = true;
                        // Program needs to know an update is needed 
                        // and the files needed to update
                        updateRow = updateNeeded.NewRow();
                        updateRow["zippedfileName"] = zippedFileName;
                        updateRow["zippedPathName"] = zippedPathName;
                        updateRow["sourcefilename"] = sourceFileName;
                        updateRow["sourcepathname"] = sourcePathName;
                        updateNeeded.Rows.Add(updateRow);

                        if (zippedFileName.EndsWith(".msi"))
                        {
                            // Program needs to let updater know to stop and 
                            // update installer
                            setupUpdateNeeded = true;
                            setupFilePath += zippedFileName;
                        }
                    }
                }
            }
            catch (Exception exd)
            {
                checkSuccess = false;
                CheckException = exd;
            }
        }

        /// <summary>
        /// Process individual file and compare length has to stored length hash
        /// </summary>
        /// <param name="fileName">The filename of the file being checked 
        /// (full path + name of local copy)</param>
        /// <param name="hashString">The stored has value to compare the 
        /// local copy with</param>
        /// <returns></returns>
        private bool downloadOfFileNeeded(string fileName, string hashString)
        {
            bool downloadFile = true;
            if (System.IO.File.Exists(fileName))
            {
                using (FileStream oStream = System.IO.File.OpenRead(fileName))
                {
                    byte[] obuffer = new byte[oStream.Length];
                    oStream.Read(obuffer, 0, obuffer.Length);
                    byte[] hashValue = 
			new MD5CryptoServiceProvider().ComputeHash(obuffer);
                    string hashX = BitConverter.ToString(hashValue);
                    downloadFile = (hashString.ToLower() != hashX.ToLower());
                    oStream.Close();
                }
            }
            return downloadFile;
        }

        /// <summary>
        /// Perform a download of the specified file
        /// </summary>
        /// <param name="uriFileName">The full url + file you need to download</param>
        /// <param name="zipFileName">The zipped up file name</param>
        /// <param name="targetFileName">The target unzipped File Name</param>
        /// <param name="subDirPath">The path you are going to put the file in</param>
        /// <returns>True if Successful, False if UnSuccessful</returns>
        public bool DownloadFile(string uriFileName, 
		string zipFileName, string targetFileName, string subDirPath)
        {
            // Used to verify download
            bool completeDownload = false;
            try
            {
                using (WebClient client = new WebClient())
                {
                    Uri uri = new Uri(uriFileName);

                    if (!System.IO.Directory.Exists(subDirPath))
                    {
                        System.IO.Directory.CreateDirectory(subDirPath);
                    }

                    if (System.IO.File.Exists(targetFileName))
                    {
                        System.IO.File.Delete(targetFileName);
                    }

                    if (System.IO.File.Exists(zipFileName))
                    {
                        System.IO.File.Delete(zipFileName);
                    }

                    client.DownloadFile(uri, zipFileName);
                    if (uriFileName.EndsWith(".zip"))
                    {
                        unzipFileToFolder(zipFileName, targetFileName);
                    }
                }
                if (System.IO.File.Exists(targetFileName))
                {
                    completeDownload = true;
                }
                else
                {
                    this.CheckException = new Exception
			("File Did Not Exist after Download");
                }
            }
            catch (Exception exd)
            {
                this.CheckException = exd;
                completeDownload = false;
            }
            return completeDownload;
        }

        /// <summary>
        /// Unzip a file to the specified folder, 
        /// optionally deleting the zipped file when finished
        /// </summary>
        /// <param name="zippedFileName">The Zipped FileName (including path)</param>
        /// <param name="targetFileName">The Target File Name Unzipped</param>
        /// <param name="deleteZippedFile">
        /// Should the file be deleted after unzipped</param>
        private void unzipFileToFolder(string zippedFileName, 
		string targetFileName, bool deleteZippedFile = true)
        {
            using (ZipInputStream s = new ZipInputStream
			(System.IO.File.OpenRead(zippedFileName)))
            {
                // obtain password to all zip files
                s.Password = Updater.Default.ZipFilePassword;
                ZipEntry theEntry;
                string tmpEntry = string.Empty;
                while ((theEntry = s.GetNextEntry()) != null)
                {
                    FileStream streamWriter = System.IO.File.Create(targetFileName);
                    int size = 2048;
                    byte[] data = new byte[size];
                    while (true)
                    {
                        size = s.Read(data, 0, data.Length);
                        if (size > 0)
                        {
                            streamWriter.Write(data, 0, size);
                        }
                        else
                        {
                            break;
                        }
                    }
                    streamWriter.Close();
                }
            }
            if (deleteZippedFile)
            {
                if (System.IO.File.Exists(zippedFileName))
                {
                    System.IO.File.Delete(zippedFileName);
                }
            }
        }
    }
}

Patcher 项目

  • 单击 Add New Project,选择 Windows Forms Project。
  • 创建一个新的 Patcher 窗体并将其链接到 Program.cs
  • 确保您添加了对 SelfUpdatingSettings 的项目引用。
    • Initialize 确保目录存在,然后创建清单数据表、清单文件、MD5 哈希,压缩文件并上传到 Repository。
      • 代码执行几乎与 CheckForUpdate 类相反,但有一些附加功能。
    • 允许选择 Production 和 Beta 版本,并在上传之前使用 msbuild 重新编译所有项目,以支持版本更改。
      • 如果项目未设置为 Release,则不会包含在此上传中。
      • 提醒 - 如果您有需要在客户端计算机上包含的外部库,您可能需要编辑它们的属性以设置为 Copy Local 和/或将它们包含在 Setup Dependencies 中。
  • 注意:我在 MSBUILD 中添加了一个文件夹,并将 MSBUILD 文件从其默认位置复制到此文件夹,以便 patcher 可以将 assemblyInfo 重建到我的产品的版本控制中。如果需要,版本控制可以扩展到主项目之外。

Patcher 项目的代码后台

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Security.Permissions;
using System.IO;

using SelfUpdatingSettings;
using SelfUpdatingSettings.Classes;
using System.Drawing.Drawing2D;
using System.Security.Cryptography;
using ICSharpCode.SharpZipLib.Zip;

namespace SelfUpdatingPatcher
{
    public partial class PatcherDialogBox : Form
    {
        public PatcherDialogBox()
        {
            InitializeComponent();

            try
            {
                // Ensure application is only run local to web by just 
                // checking for local access to directory
                DirectoryInfo r = new DirectoryInfo(Updater.Default.RepositoryExistsPath);
                if (r.Exists)
                {
                    DialogResult = System.Windows.Forms.DialogResult.OK;
                }
                else
                {
                    MessageBox.Show("Repository Cannot Run Outside of Servers, 
		    need local access to " + Updater.Default.RepositoryExistsPath,
                        "Failed to Run", MessageBoxButtons.OK);
                    this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
                }
            }
            catch (Exception exd)
            {
                MessageBox.Show("Repository Cannot Run Outside of Servers, 
		need local access to " + Updater.Default.RepositoryExistsPath,
                    "Failed to Run", MessageBoxButtons.OK);
                MessageBox.Show(exd.StackTrace, exd.Message, MessageBoxButtons.OK);
                this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
            }
        }

        #region Form-centric events

        private void PatcherDialogBox_Load(object sender, EventArgs e)
        {
            // Refuse to open form if there is a problem
            if (this.DialogResult == System.Windows.Forms.DialogResult.Cancel)
            {
                this.Close();
            }
            try
            {
                initFileTable();
                obtainSettingValues();
                webBrowser.Visible = false;
                webBrowser.SetBounds(listResults.Bounds.X, listResults.Bounds.Y, 
			listResults.Bounds.Width, listResults.Bounds.Height);
            }
            catch (Exception ex)
            {
                listResults.Items.Add(ex.Message + "\r\n" + ex.StackTrace.ToString());
            }
            versionUtil = new AssemblyInfoUtil(Updater.Default.VersionFile);
            version1Tbox.Value = versionUtil.Version1;
            version2Tbox.Value = versionUtil.Version2;
            version3Tbox.Value = versionUtil.Version3;
            version4Tbox.Value = versionUtil.Version4;
            version1Tbox.ValueChanged += versionTbox_ValueChanged;
            version2Tbox.ValueChanged += versionTbox_ValueChanged;
            version3Tbox.ValueChanged += versionTbox_ValueChanged;
            version4Tbox.ValueChanged += versionTbox_ValueChanged;
        }

        private void PatcherDialogBox_Paint(object sender, PaintEventArgs e)
        {
            Rectangle BaseRectangle =
                new Rectangle(0, 0, this.Width - 1, this.Height - 1);

            Brush Gradient_Brush =
                new LinearGradientBrush(
                BaseRectangle,
                Color.DarkOrange, Color.LightSlateGray,
                LinearGradientMode.Vertical);
            e.Graphics.FillRectangle(Gradient_Brush, BaseRectangle);
        }

        private void PatcherDialogBox_Resize(object sender, EventArgs e)
        {
            // Invalidate, or last rendered image will just be scaled
            // to new size
            this.Invalidate();
        }

        #endregion

        #region Button-centric events

        private void btnCancel_Click(object sender, EventArgs e)
        {
            DialogResult = System.Windows.Forms.DialogResult.Cancel;
            this.Close();
        }

        private void btnBeta_Click(object sender, EventArgs e)
        {
            listResults.Items.Add("Clicked Beta to Push");
            btnBeta.BackColor = Color.LightGreen;
            btnBeta.Refresh();
            btnProduction.BackColor = DefaultBackColor;
            btnProduction.Refresh();
            makeListResultsVisible();
            versionUtil.WriteFile();
            executePushToRepository(ExecuteModeEnum.Beta);
        }

        private void btnProduction_Click(object sender, EventArgs e)
        {
            listResults.Items.Add("Clicked Production to Push");
            DialogResult dialogResult = MessageBox.Show
		("Confirm patching of PRODUCTION?", "Patcher Question", 
		MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);
            if (dialogResult == DialogResult.OK)
            {
                btnBeta.BackColor = DefaultBackColor;
                btnBeta.Refresh();
                btnProduction.BackColor = Color.LightGreen;
                btnProduction.Refresh();
                makeListResultsVisible();
                versionUtil.WriteFile();
                executePushToRepository(ExecuteModeEnum.Production);
                if (MessageBox.Show(this, 
		"Beta will now be pushed to ensure uniformity.", 
		"Pushing Beta", MessageBoxButtons.OKCancel) ==
                         System.Windows.Forms.DialogResult.OK)
                    btnBeta_Click(null, null);
            }
            else
            {
                listResults.Items.Add("Production Push not Confirmed");
            }
        }

        private void btnViewRepositorySite_Click(object sender, EventArgs e)
        {
            Cursor.Current = Cursors.WaitCursor;
            if (btnViewRepositorySite.Tag == null)
            {
                listResults.Visible = false;
                webBrowser.Visible = true;
                webBrowser.SetBounds(listResults.Bounds.X, 
		listResults.Bounds.Y, listResults.Bounds.Width, 
		listResults.Bounds.Height);
                webBrowser.Navigate(Updater.Default.UriRepositoryDefault);
                btnViewRepositorySite.Text = "Hide &Repository Site";
                btnViewRepositorySite.Tag = "Hide";
                btnViewRepositorySite.BackColor = Color.PaleTurquoise;
                btnViewRepositorySite.Refresh();
                btnViewTempFolder.BackColor = DefaultBackColor;
                btnViewTempFolder.Refresh();
            }
            else
            {
                makeListResultsVisible();
            }
            Cursor.Current = Cursors.Default;
        }

        private void btnViewTempFolder_Click(object sender, EventArgs e)
        {
            btnViewRepositorySite.BackColor = DefaultBackColor;
            btnViewRepositorySite.Refresh();
            btnViewTempFolder.BackColor = Color.PaleTurquoise;
            btnViewTempFolder.Refresh();
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.InitialDirectory = pathTempCompany;
            openFileDialog.Filter = "All files (*.*)|*.*";
            openFileDialog.ShowDialog();
        }

        #endregion

        #region Form-level variables

        private string appFolder = string.Empty;
        private string companyName = string.Empty;
        private string repositoryFolder = string.Empty;
        private string launcherFolder = string.Empty;
        private string setupFolder = string.Empty;
        private string softwareManifestFileName = string.Empty;
        private DirectoryInfo projReleaseDirectoryInfo;
        private DirectoryInfo projSetupDirectoryInfo;
        private DirectoryInfo projLauncherDirectoryInfo;
        private DirectoryInfo repositoryInfo;
        private string pathTempCompany = string.Empty;
        private AssemblyInfoUtil versionUtil = null;

        private DataTable fileTable = new DataTable("Files");

        #endregion

        /// <summary>
        /// Mainline Logic to invoke upon clicking of a Production/Beta button
        /// </summary>
        /// <param name="chosenVersionToPush">Execution Mode</param>
        private void executePushToRepository(ExecuteModeEnum chosenVersionToPush)
        {
            Cursor.Current = Cursors.WaitCursor;
            try
            {
                switch (chosenVersionToPush)
                {
                    case ExecuteModeEnum.Beta:
                        appFolder = Updater.Default.AppSubFolderBeta;
                        repositoryFolder = Updater.Default.RepositoryDefault + 
				@"\" + Updater.Default.AppSubFolderBeta;
                        break;
                    case ExecuteModeEnum.Production:
                        appFolder = Updater.Default.AppSubFolderProd;
                        repositoryFolder = Updater.Default.RepositoryDefault + 
				@"\" + Updater.Default.AppSubFolderProd;
                        break;
                }

                initFileTable();
                DataSet dataSet = new DataSet("SoftwareManifest");

                // Make sure external Repository exists
                if (!repositoryInfo.Exists)
                    repositoryInfo.Create();

                // walk through the chosen app folder
                // and zip the files therein to the appropriate temp folder

                if (projReleaseDirectoryInfo.Exists)
                {
                    if (projSetupDirectoryInfo.Exists)
                    {
                        // Make sure temp folder is empty so we don't undo a previous push
                        DirectoryInfo di = new DirectoryInfo(pathTempCompany);
                        if (di.Exists)
                        {
                            foreach (DirectoryInfo prevDirectoryInfo 
				in di.GetDirectories())
                            {
                                if (prevDirectoryInfo.Exists)
                                    prevDirectoryInfo.Delete(true);
                            }
                        }
                        // zip the files in the main application folder
                        zipFilesInFolder(projReleaseDirectoryInfo.FullName, 
			pathTempCompany + "\\" + appFolder, 
			projReleaseDirectoryInfo, true, true);
                        listResults.Items.Add(" ");

                        // zip the files in the setup Folder (msi will be skipped)
                        zipFilesInFolder(projSetupDirectoryInfo.FullName, 
			pathTempCompany + "\\" + appFolder + "\\" + setupFolder, 
			projSetupDirectoryInfo, true, true);
                        listResults.Items.Add(" ");

                        // create and zip the software manifest file
                        string manifestFile = pathTempCompany + "\\" + 
				appFolder + "\\" + softwareManifestFileName;
                        if (File.Exists(manifestFile))
                        {
                            File.Delete(manifestFile);
                        }
                        listResults.Items.Add("Creating Software Manifest File:  " + 
			softwareManifestFileName);
                        listResults.Items.Add(" ");
                        dataSet.Tables.Add(fileTable);
                        dataSet.WriteXml(manifestFile);

                        // Write files to repository folder (cleaned above)
                        UpdateRepository(repositoryInfo);
                        listResults.Items.Add("Program Completed...");
                        listResults.Items.Add(" ");
                    }
                    else
                    {
                        listResults.Items.Add("ProjSetupFolder| " + 
			projSetupDirectoryInfo.FullName + " does not exist!");
                    }
                }
                else
                {
                    listResults.Items.Add("ProjReleaseFolder| " + 
			projReleaseDirectoryInfo.FullName + " does not exist!");
                }

            }
            catch (Exception ex)
            {
                listResults.Items.Add(ex.Message);
                listResults.Items.Add(ex.StackTrace.ToString());
            }
            listResults.Refresh();
            listResults.TopIndex = listResults.Items.Count - 1;
            Cursor.Current = Cursors.Default;
        }

        /// <summary>
        /// Clear Repository, Transfer Directorys, then Transfer Files
        /// </summary>
        /// <param name="repositoryInfo">Repository DirectoryInfo</param>
        private void UpdateRepository(DirectoryInfo repositoryInfo)
        {

            // 1) Create directories in Repository and wipe out any existing data
            listResults.Items.Add("Clearing Existing Repository: " + 
			repositoryInfo.FullName);
            DirectoryInfo updateDir = new DirectoryInfo(repositoryFolder);
            if (updateDir.Exists)
            {
                listResults.Items.Add("  Deleting Directory and SubDirectories: " + 
			updateDir.FullName);
                updateDir.Delete(true);
            }
            listResults.Items.Add(" ");

            listResults.Items.Add("Transferring Directories");
            DirectoryInfo workingDir = new DirectoryInfo(pathTempCompany);

            TransferDirectories(workingDir, workingDir.FullName);
            TransferFiles(workingDir, workingDir.FullName);
        }

        /// <summary>
        /// Move the entire directories and files to the target Repository
        /// </summary>
        /// <param name="directoryInfo">Source DirectoryInfo</param>
        /// <param name="baseDir">The To Directory Target</param>
        private void TransferDirectories(DirectoryInfo directoryInfo, string baseDir)
        {
            DirectoryInfo workingDirectoryInfo;
            string workingDir = string.Empty;
            foreach (DirectoryInfo di in directoryInfo.GetDirectories())
            {
                workingDir = Updater.Default.RepositoryDefault + 
			di.FullName.Remove(0, baseDir.Length + 1);
                workingDirectoryInfo = new DirectoryInfo(workingDir);
                if (!workingDirectoryInfo.Exists)
                    workingDirectoryInfo.Create();
                TransferDirectories(di, baseDir);
                listResults.Items.Add("  Transferred Directory: " + di.FullName);
                TransferFiles(di, baseDir);
                listResults.Items.Add(" Transferred Files from " + di.FullName);
                listResults.Items.Add(" ");
                listResults.Refresh();
                listResults.TopIndex = listResults.Items.Count - 1;
            }
        }

        /// <summary>
        /// Transfer files from the DirectoryInfo to the baseDirectory Repository
        /// </summary>
        /// <param name="directoryInfo">The From Directory to copy files</param>
        /// <param name="baseDir">The To Directory target</param>
        private void TransferFiles(DirectoryInfo directoryInfo, string baseDir)
        {
            FileInfo workingFile;
            string workingDir = string.Empty;
            foreach (FileInfo f in directoryInfo.GetFiles())
            {
                workingFile = new FileInfo(f.FullName);
                workingDir = workingFile.FullName.Remove(0, baseDir.Length + 1);
                workingDir = Updater.Default.RepositoryDefault + workingDir;
                workingFile.CopyTo(workingDir);
            }
        }

        /// <summary>
        /// Create File Table with the appropriate columns
        /// </summary>
        private void initFileTable()
        {
            fileTable = new DataTable("Files");
            fileTable.Columns.Add(new DataColumn("sourcePathName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("sourceFileName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("zippedPathName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("zippedFileName", typeof(string)));
            fileTable.Columns.Add(new DataColumn("md5HashedValue", typeof(string)));
        }

        /// <summary>
        /// Make the listResults listbox visible and set the Text, Tag, 
        /// and BAckColor of various buttons as needed
        /// </summary>
        private void makeListResultsVisible()
        {
            webBrowser.Visible = false;
            webBrowser.Navigate(string.Empty);
            listResults.Visible = true;
            btnViewRepositorySite.Text = "View &Repository Site";
            btnViewRepositorySite.Tag = null;
            btnViewRepositorySite.BackColor = DefaultBackColor;
            btnViewRepositorySite.Refresh();
            btnViewTempFolder.BackColor = DefaultBackColor;
            btnViewTempFolder.Refresh();
        }

        /// <summary>
        /// Obtain and Manipulate the program's settings from misc. values
        /// </summary>
        private void obtainSettingValues()
        {
            string pathTempFolder = string.Empty;
            try
            {
                listResults.Items.Clear();

                // By putting the setup in the beta/prod folders 
                // it allows you to process a "Beta" setup file without
                // touching users in production
                setupFolder = Updater.Default.AppSubFolderSetup;

                // This is the xml file managing your patches
                softwareManifestFileName = Updater.Default.SoftwareManifestFileName;

                // local path to your release project
                projReleaseDirectoryInfo = new DirectoryInfo
				(Updater.Default.ProjecctReleaseFolder);

                // local path to your Setup Project
                projSetupDirectoryInfo = new DirectoryInfo
				(Updater.Default.SetupReleaseFolder);

                // local path to your repository
                repositoryInfo = new DirectoryInfo(Updater.Default.RepositoryDefault);

                // create folders and zip files
                // as needed in a temporary folder

                pathTempFolder = Path.GetTempPath();
                DirectoryInfo di = new DirectoryInfo(pathTempFolder);

                pathTempCompany = pathTempFolder + Updater.Default.CompanyName;
                di = new DirectoryInfo(pathTempCompany);
                if (di.Exists)
                {
                    di.Delete(true);
                }
                di.Create();
            }
            catch (Exception ex)
            {
                listResults.Items.Add(ex.Message + "\r\n" + ex.StackTrace.ToString());
            }
        }

        /// <summary>
        /// Zip all of the files in the source folder to the 
        /// specified temporary folder deriving MD5 hash values along the way, 
        /// saving them to a datatable for later usage in outputting the manifest XML file.
        /// </summary>
        /// <param name="sourceFolderFullName">Source Folder Full Name</param>
        /// <param name="tempFolder">Temporary Folder Path</param>
        /// <param name="directoryInfo">DirectoryInfo object</param>
        /// <param name="omitVsHostExecutables">Whether to omit unneeded 
        /// Visual Studio files</param>
        /// <param name="dontZipInstallers">Whether or not to zip the MSI file</param>
        private void zipFilesInFolder(string sourceFolderFullName, 
		string tempFolder, DirectoryInfo directoryInfo, 
		bool omitVsHostExecutables, bool dontZipInstallers)
        {
            string targetSubFolderName = directoryInfo.FullName.Substring
		(sourceFolderFullName.Length);
            while (targetSubFolderName.StartsWith("\\"))
            {
                targetSubFolderName = targetSubFolderName.Substring
				(1, targetSubFolderName.Length - 1);
            }
            string targetFolderName = tempFolder + "\\" + targetSubFolderName;
            while (targetFolderName.EndsWith("\\"))
            {
                targetFolderName = targetFolderName.Substring
				(0, targetFolderName.Length - 1);
            }
            // zip each file/sub-folder in the present folder
            foreach (FileInfo fi in directoryInfo.GetFiles())
            {
                // Get MD5 Hash
                string md5Hash = string.Empty;
                using (FileStream oStream = System.IO.File.OpenRead(fi.FullName))
                {
                    byte[] obuffer = new byte[oStream.Length];
                    oStream.Read(obuffer, 0, obuffer.Length);
                    byte[] hashValue = 
			new MD5CryptoServiceProvider().ComputeHash(obuffer);
                    md5Hash = BitConverter.ToString(hashValue).ToLower();
                    oStream.Close();
                }

                // if .vshost executables are allowed
                // or the filename does not contain "vshost"
                // zip or copy the file as needed
                if (!omitVsHostExecutables || !fi.Name.ToLower().Contains("vshost"))
                {
                    // if MSI installers are not to be zipped
                    // then copy them unmolested to the target folder
                    if (dontZipInstallers && fi.Name.ToLower().EndsWith(".msi"))
                    {
                        string tgtFileName = targetFolderName + "\\" + fi.Name;
                        if (File.Exists(tgtFileName))
                        {
                            File.Delete(tgtFileName);
                        }
                        FileInfo tgtFileInfo = new FileInfo(tgtFileName);
                        if (!tgtFileInfo.Directory.Exists)
                        {
                            Directory.CreateDirectory(tgtFileInfo.Directory.FullName);
                        }
                        fi.CopyTo(tgtFileName);

                        addFileRow(tgtFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), fi.Name,
                            tgtFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), tgtFileInfo.Name,
                            md5Hash, 0);
                    }
                    //otherwise
                    else
                    {
                        listResults.Items.Add("Zipping file " + fi.FullName);
                        string zipFileName = targetFolderName + "\\" + fi.Name + ".zip";
                        listResults.Items.Add("    into " + zipFileName + "...");
                        listResults.Items.Add(" ");
                        listResults.Refresh();
                        listResults.TopIndex = listResults.Items.Count - 1;
                        if (File.Exists(zipFileName))
                        {
                            File.Delete(zipFileName);
                        }
                        FileInfo zipFileInfo = new FileInfo(zipFileName);
                        if (!zipFileInfo.Directory.Exists)
                        {
                            Directory.CreateDirectory(zipFileInfo.Directory.FullName);
                        }
                        using (ZipOutputStream oZipStream = 
			new ZipOutputStream(File.Create(zipFileName)))
                        {
                            oZipStream.Password = Updater.Default.ZipFilePassword;
                            oZipStream.PutNextEntry(new ZipEntry(fi.Name));

                            FileStream oStream = File.OpenRead(fi.FullName);
                            byte[] obuffer = new byte[oStream.Length];
                            oStream.Read(obuffer, 0, obuffer.Length);

                            addFileRow(zipFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), fi.Name,
                                zipFileInfo.Directory.FullName.Substring
				(pathTempCompany.Length), zipFileInfo.Name,
                                md5Hash);

                            oZipStream.Write(obuffer, 0, obuffer.Length);
                            oZipStream.Finish();
                            oZipStream.Close();
                            oStream.Close();
                        }
                    }
                }
            }
            foreach (DirectoryInfo di in directoryInfo.GetDirectories())
            {
                zipFilesInFolder(sourceFolderFullName, tempFolder, di, 
			omitVsHostExecutables, dontZipInstallers);
            }
        }

        private void addFileRow(string sourcePathName, 
		string sourceFileName, string zippedPathName, string zippedFileName,
            string md5hashedValue,  int insertLoc = -1)
        {
            DataRow dataRow = fileTable.NewRow();
            dataRow["sourcePathName"] = sourcePathName;
            dataRow["sourceFileName"] = sourceFileName;
            dataRow["zippedPathName"] = zippedPathName;
            dataRow["zippedFileName"] = zippedFileName;
            dataRow["md5hashedValue"] = md5hashedValue;

            if (insertLoc > -1)
                fileTable.Rows.InsertAt(dataRow, insertLoc);
            else
                fileTable.Rows.Add(dataRow);
        }

        /// <summary>
        /// Update AssemblyInfoUtil class Version Numbers
        /// </summary>
        private void versionTbox_ValueChanged(object sender, EventArgs e)
        {
            if (!versionUtil.fileisDirty)
                versionUtil.fileisDirty = true;
            if (version1Tbox.Value != versionUtil.Version1)
                versionUtil.Version1 = (int)(version1Tbox.Value);
            if (version2Tbox.Value != versionUtil.Version2)
                versionUtil.Version2 = (int)(version2Tbox.Value);
            if (version3Tbox.Value != versionUtil.Version3)
                versionUtil.Version3 = (int)(version3Tbox.Value);
            if (version4Tbox.Value != versionUtil.Version4)
                versionUtil.Version4 = (int)(version4Tbox.Value);
        }
    }
}

Launcher 项目

  • 单击 Add New Project,选择 Windows Forms Project。
  • 创建一个新的 Launcher 窗体并将其链接到 Program.cs
    • Initialize 存储任何传递的参数,以便您可以将它们传递给您的实际程序。
    • 实例化并使用之前的 CheckForUpdate 类来管理所需单个文件的下载(而不是每次都修补所有文件)。
    • 这些文件在后台工作线程中处理,以确保主 UI 线程不会完全无法响应最终用户,并且为调试提供了很大的灵活性。

Launcher 窗体的代码后台

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Windows.Forms;

using Microsoft.Win32;

using SelfUpdatingSettings.Classes;
using SelfUpdatingSettings;

namespace SelfUpdatingLauncher
{
    public partial class SelfUpdatingLauncher : Form
    {
        public SelfUpdatingLauncher(string[] passingArgs)
        {
            InitializeComponent();
            launchArgs = passingArgs;
            foreach (string arg in launchArgs)
            {
                if (arg.ToLower() == "/beta")
                {
                    execMode = ExecuteModeEnum.Beta;
                }
            }
        }

        private ExecuteModeEnum execMode = ExecuteModeEnum.Production;
        private bool launchSetup = false;
        private CheckForUpdate updateChecker = null;
        private string[] launchArgs;
        private bool closeOnExit = true;
        private BackgroundWorker bg;
        /// <summary>
        /// The Number of Seconds to wait until updating starts.
        /// </summary>
        private int timeout = 1;

        private void Form_Load(object sender, EventArgs e)
        {
            this.TransparencyKey = this.BackColor;
            BackgroundWorker bgStarter = new BackgroundWorker();
            bgStarter.RunWorkerCompleted += new RunWorkerCompletedEventHandler
					(bgStarter_RunWorkerCompleted);
            bgStarter.DoWork += new DoWorkEventHandler(bgStarter_DoWork);
            bgStarter.ProgressChanged += new ProgressChangedEventHandler
					(bgStarter_ProgressChanged);
            bgStarter.WorkerReportsProgress = true;
            bgStarter.RunWorkerAsync(timeout);
        }

        #region Start Program Worker

        private void bgStarter_DoWork(object sender, DoWorkEventArgs e)
        {
            // If we error out that's okay, we aren't 
            // returning anything to the completed Event
            BackgroundWorker bgStarter = (BackgroundWorker)sender;
            for (int i = ((int)e.Argument); i >= 0; i--)
            {
                bgStarter.ReportProgress(0,"Starting Program in " + 
				i.ToString() + " second(s).");
                Thread.Sleep(999);
            }
            bgStarter.ReportProgress(0, "Checking for Update.");
        }

        private void bgStarter_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            updateStatus((string)e.UserState, e.ProgressPercentage);
            listBoxProgress.TopIndex = listBoxProgress.Items.Count - 1;
        }

        private void bgStarter_RunWorkerCompleted
		(object sender, RunWorkerCompletedEventArgs e)
        {
            closeButton.Enabled = true;
            updateChecker = new CheckForUpdate(execMode);
            updateStatus(updateChecker.Filecount.ToString() + " file(s) reviewed, " 
		+ updateChecker.UpdateNeeded.Rows.Count.ToString() + 
		" files found to update.", 0);
            if (updateChecker.MainUpdateNeeded |
                updateChecker.SetupUpdateNeeded)
            {
                progressBar1.Minimum = 0;
                progressBar1.Maximum = updateChecker.UpdateNeeded.Rows.Count;
                updateStatus("Update Starting...", 0);

                bg = new BackgroundWorker();
                bg.DoWork += new DoWorkEventHandler(downloadFile_DoWork);
                bg.RunWorkerCompleted += new RunWorkerCompletedEventHandler
					(downloadFile_Completed);
                bg.ProgressChanged += new ProgressChangedEventHandler
					(downloadFiles_Progress);
                bg.WorkerSupportsCancellation = true;
                bg.WorkerReportsProgress = true;
                bg.RunWorkerAsync(updateChecker.UpdateNeeded);
            }
            else
            {
                updateStatus("Update Not Required...", 0);
                DoClose();
            }
        }

        #endregion

        #region Fileworkers
        private void downloadFiles_Progress(object sender, ProgressChangedEventArgs e)
        {
            updateStatus((string)e.UserState, e.ProgressPercentage);
            listBoxProgress.TopIndex = listBoxProgress.Items.Count - 1;
        }

        private void downloadFile_Completed(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Result is bool)
            {
                if ((bool)e.Result)
                {
                    return;
                }
            }
            if (e.Error == null)
            {
                // Setup Launch will happen on form close if needed
                launchSetup = updateChecker.SetupUpdateNeeded;
            }
            else
            {
                MessageBox.Show(e.Error.Message + "\r\n" + e.Error.StackTrace, 
			"Failed to Update Files!", MessageBoxButtons.OK);
            }
            DoClose();
        }

        private void downloadFile_DoWork(object sender, DoWorkEventArgs e)
        {
            bg = (BackgroundWorker)sender;
            DataTable dt = (DataTable)e.Argument;
            DataRow row;
            for (int i = 0; i < dt.Rows.Count; i++)
            {
                if (bg.CancellationPending)
                {
                    e.Result = true;
                    break;
                }
                row = dt.Rows[i];
                string zippedFileName = (string)row["zippedfilename"];
                string zippedPathName = (string)row["zippedpathname"];
                string sourceFileName = (string)row["sourceFileName"];
                string sourcePathName = (string)row["sourcePathName"];

                string sourceFullPath = updateChecker.CompanyDir + 
		sourcePathName.Substring(1) + "\\" + sourceFileName;
                string zippedPath = updateChecker.CompanyDir + 
		sourcePathName.Substring(1) + "\\" + zippedFileName;
                string zipFullPath = updateChecker.DownloadUri + 
		zippedPathName + "/" + zippedFileName;
                zipFullPath = zipFullPath.Replace("//\\", "//");
                string subdirPath = updateChecker.CompanyDir + sourcePathName.Substring(1);
                if (updateChecker.DownloadFile(zipFullPath, zippedPath, 
			sourceFullPath, subdirPath))
                {
                    bg.ReportProgress(i + 1, sourceFullPath + " updated Successfully.");
                }
                else
                {
                    bg.ReportProgress(i + 1, sourceFullPath + " failed to update!");
                    bg.ReportProgress(i + 1, updateChecker.CheckException.Message + 
			"\r\n" + updateChecker.CheckException.StackTrace);
                }
            }
        }
        #endregion

        private void updateStatus(string statusUpdate, int percentageUpdate)
        {
            listBoxProgress.Items.Add(statusUpdate);
            labelSecondsBeforeUpdate.Text = statusUpdate;
            listBoxProgress.Refresh();
            if (percentageUpdate > 0)
            {
                progressBar1.Value = percentageUpdate;
            }
        }

        private void DoClose()
        {
            updateStatus("Finished Processing...", progressBar1.Maximum);
            if (closeOnExit)
            {
                this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
                this.Close();
            }
        }

        private void Event_FormClosed(object sender, FormClosedEventArgs e)
        {
            // First click of optionsButton sets the Tag to true and 
            // tells the program to not "autoclose" at the end
            // Second click of optionsButton sets the Tag to false and 
            // tells the program to not launch when form is closed
            bool auto = false;
            if (optionsButton.Tag == null)
            {
                auto = true;
            }
            else
            {
                auto = (bool)optionsButton.Tag;
            }
            if (auto)
            {
                try
                {
                    using (Process p = new Process())
                    {
                        // If no arguments were passed to start application 
                        // it was launched from the setup and does not need 
                        // to launch the Setup again
                        if (launchSetup &&
                            launchArgs.Length > 0)
                        {
                            p.StartInfo = new ProcessStartInfo
					(updateChecker.SetupFilePath);
                            p.StartInfo.Arguments = "/passive";
                        }
                        else
                        {
                            p.StartInfo.FileName = updateChecker.LauncherPath + 
					Updater.Default.ApplicationExecutable;
                            p.StartInfo = new ProcessStartInfo
				(updateChecker.LauncherPath + 
				Updater.Default.ApplicationExecutable,
                                StaticClasses.getStringArrayValuesAsString
				(launchArgs, " ", false));
                            p.StartInfo.WorkingDirectory = updateChecker.LauncherPath;
                        }
                        p.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
                        p.Start();
                    }
                }
                catch (Exception exd)
                {
                    MessageBox.Show(exd.Message + "\r\n" + exd.StackTrace, 
			"Failed to Launch", MessageBoxButtons.OK);
                }
            }
        }

        #region Buttons
        private void btnClose_Click(object sender, EventArgs e)
        {
            // Make sure worker is initialized
            if (bg != null)
            {
                // Check if it's busy, and interrupt it if it is
                if (bg.IsBusy)
                    bg.CancelAsync();

                // Need to wait until it drops out
                while (bg.IsBusy)
                    Thread.Sleep(999);
            }

            this.DialogResult = System.Windows.Forms.DialogResult.Cancel;
            this.Close();
        }

        private void optionsButton_Click(object sender, EventArgs e)
        {
            if (optionsButton.Tag == null)
            {
                updateStatus("User Requested Manual Close, 
			downloading will continue.", 0);
                closeOnExit = false;
                optionsButton.Tag = true;
            }
            else if ((bool)optionsButton.Tag)
            {
                updateStatus("User Requested Auto-Launch turned off, 
			downloading will continue.", 0);
                optionsButton.Tag = false;
            }
        }

        #endregion
    }
}

Installer 类的代码后台(与 Setup 协同工作,以便在卸载时清理目录,如前所述。我们使用的实际程序不包含在 Launcher 中,因此在卸载时我们需要手动清理它。)

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;

using System.Diagnostics;
using System.IO;
using SelfUpdatingSettings.Classes;


namespace SelfUpdatingLauncher
{
    [RunInstaller(true)]
    public partial class SelfUpdatingInstaller : System.Configuration.Install.Installer
    {
        public SelfUpdatingInstaller()
        {
            InitializeComponent();
        }

        private void SelfUpdatingInstaller_AfterUninstall
			(object sender, InstallEventArgs e)
        {
            try
            {
                DirectoryInfo d = new DirectoryInfo(StaticClasses.InstallPath);
                if (d.Exists)
                {
                    d.Delete(true);
                }
            }
            catch { }
        }
    }
}

设置项目

  • 将 Launcher 设置为启动项目。
  • 右键单击并选择 Add new project。
  • 在 Windows, Other Project Types, Setup and Deployment, Visual Studio Installer 下。
  • 为其命名,它将默认为 File System 文件夹。
  • 右键单击您的新 Setup 项目,然后单击 "Add->Project Output"。
  • 选择 Primary Output,并确保在顶部下拉列表中选择了您的 Launcher 项目。
  • 它应该会自动添加对 ICSharp、.NET 和 SelfUpdatingSettings.dll 的依赖关系。
  • 回到创建 Setup 项目时自动打开的 File System 窗口。
  • 单击 User's Program Folder 并选择 properties,将 Always Create 更改为 true。
  • 右键单击 User's Program Folder 的空白区域,然后选择 Create New Shortcut。
  • 双击 Application Folder 并选择 Project Output。
  • 转到新快捷方式的 Properties,然后在 arguments 中添加 /prod,修改您可能需要的任何其他属性,如 Icon 和 Name。
  • 使用相同的方法创建另一个快捷方式,但在 arguments 中放入 /beta。名称也应表明这是 Beta 版本。
  • 可选地,重复此过程以在 User's Desktop 部分将快捷方式放在桌面上。
  • 再次右键单击 Setup Project,然后选择 View -> Custom Actions。
  • 在 Custom Actions 窗口中,右键单击 Custom Action's 再次,然后单击 Add。
  • 选择 Application Folder,然后双击 Project Output。
  • 右键单击您的 SelfUpdatingLauncher Project,然后选择 Add -> New Item。
  • 从列表中选择 Installer Class 并将其命名为适当的名称(我的名称是 SelfUpdatingInstaller)。
  • 双击新创建的 Installer Class,进入 properties,然后进入 events,并为 After Uninstall 和 Committed 添加事件。
  • 转到新创建的事件。
    • AfterUninstall 删除自定义目录及其所有子文件夹/文件,这些文件是我们手动下载的。
    • Committed 在安装完成后(可选)启动 LauncherProgram
  • 选择 Setup Project 并转到 properties。
  • 确保 Remove Previous Versions 设置为 TRUE
  • 确保 Install All Users 设置为 TRUE
  • 设置其他相关选项(Name、URLs、支持等)。
  • PostBuildAction设置为
    • cscript.exe "$(ProjectDir)EnableLaunchApplication.js" "$(BuiltOuputPath)"
  • EnableLaunchApplication.js 从源包复制出来,将执行程序修改为与您的 launcher 项目名称匹配,并将此文件放在 Setup .vdproj 文件所在的同一个文件夹中。

IExpress Setup

Windows XP/Vista/7 都包含一个名为 IEXPRESS 的方便的打包 Setup 的工具。右键单击您的 Setup 项目,然后选择 Open Folder in Windows Explorer,并将 setup.exe.msi 文件复制到一个可以从命令提示符访问的文件夹中。

非常重要:如果您想能够在 32 位机器上运行这个新的 Setup 文件,它必须在 32 位环境中创建,它仍然可以在 64 位机器上工作,并且不会影响您的应用程序,只影响 Setup 文件。

单击 Start, Run, cmd,然后导航到正确的文件夹。

注意:如果您在 Setup 程序的 Release 目录中创建了 SED 和安装文件,那么 SED 和相关文件也会被推送到您的补丁中(非必需)。

点击 Next 创建一个新的 Self Extraction Directive 文件。再次点击 Next 选择 Extract files and run an installation command。为您的包选择一个标题,然后点击 Next。选择是否提示用户,然后再次点击 Next。确定是否显示许可证,然后再次点击 Next。点击 Add 并将 setup.exe.msi 文件添加到您的包中。

点击 Next,然后在 Install Program 下拉框中选择 setup.exe

选择安装时窗口的显示方式(我使用默认值),然后再次点击 next。决定安装完成后是否显示消息,然后点击 next。(我选择 No Message,因为我正在追求无人值守安装)。在下一个屏幕上为您的包选择一个名称和位置,然后点击 Next。要选择一个位置,请点击 Browse 并导航到您要保存的位置,然后输入一个新名称,我使用了 SUPInstaller

选择安装完成后如何处理重启(我想不到有任何需要重启的原因,所以我将选项更改为 "No restart" 并点击 next。再次点击 next 保存 Self Extraction Directive (SED) 文件,然后再次点击 next 创建包。点击 Finish。现在从您的命令提示符中,键入 notepad SUPInstaller.SED 并将 AppLaunched=setup.exe 行修改为 AppLaunched=setup.exe /passive,以便进行完全被动的安装。注意,通过运行 setup.exe 程序而不是 MSI,所有适当的组件(如正确的 .NET 版本)都将被下载和处理(这些会提示用户)。

再次从命令提示符运行 iexpress,但这次打开一个现有的 SED(您刚刚修改过的),然后点击 next。然后再次点击 next 重新创建包(这次带有我们的 /passive 选项),然后点击 Finish。您可以将此 Setup 文件发布到更容易访问的站点,因此不必向最终用户显示补丁文件的加载位置。并且您可以永久使用此 Setup 文件而无需重新创建 IEXPRESS 包,因为我们的程序将在首次加载后自动使用最新的 Setup 进行修补。

闭运算

所以,我希望这能帮助很多人,并且我能够回馈一些我从 CodeProject 等网站获得的知识,这些网站为我的编程需求提供了宝贵的时间节约和非凡的学习资源。

历史

  • 2010年11月7日:初始版本
© . All rights reserved.