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

自动更新应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (14投票s)

2011年7月29日

CPOL

5分钟阅读

viewsIcon

54664

downloadIcon

3943

解释了 .NET 应用程序如何更新其自身的二进制文件和依赖项

引言

有些应用程序可能需要定期更新自身。通常,设计为高度可扩展的应用程序可以更新少数模块,从而动态获得新的功能和特性,就像插件一样。

.NET 应用程序可以利用将可更新模块加载到单独的 AppDomain 中,并在有可用更新时卸载它们,然后使用更新后的程序集重新加载它们。您可能会遇到的一个问题是,加载的程序集文件会被锁定,这会阻止替换它们。如果您使用单独的 AppDomain 来加载程序集,那么解决方案会更容易。您可以为 AppDomain 启用影子复制功能,该功能实际上会在加载程序集之前创建副本,从而使原始程序集保持解锁状态,您可以在应用程序运行时替换它们。

如果您没有使用单独的 AppDomain,并且想要更新包括应用程序 EXE 在内的程序集,那么您需要做一些技巧性的事情,如本文其余部分所述。

背景

我基本上想运行应用程序,让应用程序从某个远程位置更新自身的 EXE 和其他程序集。这需要以一种不锁定程序集的方式启动应用程序,而这在应用程序启动时是正常的。如果应用程序能够自我更新,那么应用程序可以从中央位置进行分发。我不会在本文中包含任何下载程序集的逻辑。

Using the Code

我找到了以下两种解决方案。

解决方案一

在这里,我使用了另一个应用程序(即 _Starter.exe_)以及主应用程序。_Starter.exe_ 必须存在于应用程序的启动目录中。如果您下载的是代码,请确保在生成后复制 _Startup.exe_。

解决方案一的代码如下

internal static class Program
    {
        private static readonly string DomainName = "MarvinDomain";
        private static readonly string CatchFolder = "AssemblyCatch";

    
        [STAThread]
        private static void Main()
        {
if (AppDomain.CurrentDomain.FriendlyName != DomainName)
{
    string exeFilePath = Assembly.GetExecutingAssembly().Location;
    string applicationFolder = Path.GetDirectoryName(exeFilePath);

    string starterExePath = applicationFolder + "\\Starter.exe";
    //Debugger.Launch(); // You can uncomment to automatically launch VS for debugging
    //Solution one - Using Separate EXE to start application.
    if (File.Exists(starterExePath)) // Check if starter EXE is available
    {
        XmlSerializer serialier = new XmlSerializer(typeof(AppDomainSetup));
        string xmlFilePath = applicationFolder + "\\XmlData.xml";
        using (var stream = File.Create(xmlFilePath))
        {
	serialier.Serialize(stream, AppDomain.CurrentDomain.SetupInformation);
        }

        exeFilePath = exeFilePath.Replace(" ", "*");
        xmlFilePath = xmlFilePath.Replace(" ", "*");
        string args = exeFilePath + " " + xmlFilePath;

        Process starter = Process.Start(starterExePath, args);
    }

在 _Main_ 方法中,应用程序会检查当前 _AppDomain_ 的 _FriendlyName_ 是否与特定名称(即 _MarvinDomain_)匹配。这可以是您想要的任何名称。

如果名称不匹配,应用程序会检查其启动路径中是否存在 _Starter.exe_。如果找到此 EXE,它将启动新进程,该进程将启动 _Starter.exe_。在启动时,它会将两个 _string_ 参数传递给新进程:

  1. 它自身的 EXE 路径
  2. XML 文件路径。

应用程序将当前 _AppDomain_ 的 _AppDomainSetup_ 序列化到 XML 文件中,并将其作为第二个 _string_ 参数传递。

请注意,我已经用星号替换了文件路径中的空格,因为如果存在空格,会生成额外的参数。

现在,当 _Starter.exe_ 运行时,它会在其 _Main_ 方法中从 _string_ 参数接收我们主应用程序的 EXE 路径以及包含序列化的 _AppDomainSetup_ 的 XML 文件的路径。然后,它会创建新的 _AppDomain_ 并为此新创建的域启用影子复制功能。为此,它使用反序列化的 _AppDomainSetup_ 并更改相关属性。如果反序列化的 _AppDomainSetup_ 不可用,它会创建一个新的。然后,它在该新域中运行应用程序,即我们的主应用程序。

启动应用程序的代码

private static readonly string DomainName = "MarvinDomain";
        private static AppDomainSetup setup;

        private static void Main(string[] args)
        {
string executablePath = string.Empty; 
           //  Debugger.Launch();       	// You can uncomment to automatically 
					// launch VS for debugging
if (args.Length > 0)         
{
    executablePath = args[0];
    executablePath = executablePath.Replace("*", " ");
    if (args.Length > 1)
    {
        string xmlFilePath = args[1];
        xmlFilePath = xmlFilePath.Replace("*", " ");
        if (File.Exists(xmlFilePath))
        {
XmlSerializer serializer = new XmlSerializer(typeof(AppDomainSetup));
using (var stream = File.Open(xmlFilePath, FileMode.Open, FileAccess.Read))
{
    setup = serializer.Deserialize(stream) as AppDomainSetup;
}
File.Delete(xmlFilePath);
        }
    }
}
if (File.Exists(executablePath))
{
    AppDomainSetup appDomainShodowCopySetup = AppDomain.CurrentDomain.SetupInformation;

    if (setup != null)
    {
        appDomainShodowCopySetup = setup;
    }

    appDomainShodowCopySetup.ShadowCopyFiles = true.ToString();

    appDomainShodowCopySetup.CachePath = Path.GetDirectoryName
					(executablePath) + "\\AssemblyCatch";

    AppDomain marvinDomain = AppDomain.CreateDomain
				(DomainName, null, appDomainShodowCopySetup);

    marvinDomain.ExecuteAssembly(executablePath);

    AppDomain.Unload(marvinDomain);
}
}
}

当应用程序在新域中运行时,并且该域具有预期的名称,此时我们的主应用程序将执行其正常工作,例如运行 _MainForm_,而不是查找 _Starter.exe_。

要检查演示,请下载二进制文件并单击 _UpgradableApplication.exe_,这是一个 WinForm 应用程序。您会注意到在 _startup_ 文件夹内会创建一个额外的 _catch_ 文件夹,所有程序集都会被复制到该文件夹中。为了确保我们的 _UpgradableApplication.exe_ 没有被锁定,您可以在应用程序运行时删除该文件。

解决方案二

虽然解决方案一有效,但它需要额外的 _Startup.exe_,并且如果您查看 Windows 任务管理器,您会看到 _Starter.exe_ 的名称而不是我们的主 EXE。

如果找不到 _Starter.exe_,演示应用程序将使用解决方案二。因此,要为此解决方案检查演示,您可以将 _Starter.exe_ 重命名为其他名称,将使用第二种方法。第二种方法没有根本性的不同。代码如下:

if (!applicationFolder.EndsWith(CatchFolder))
        {
string copyDirectoryPath = applicationFolder + "\\" + CatchFolder;
if (!Directory.Exists(copyDirectoryPath))
{
    Directory.CreateDirectory(copyDirectoryPath);
}
DateTime now = DateTime.Now;
string dateTimeStr = now.Date.Day.ToString() + now.Month.ToString() 
         + now.Second.ToString() + now.Millisecond.ToString();
string copyExePath = copyDirectoryPath + "\\CopyOf" +
          Path.GetFileNameWithoutExtension(exeFilePath)
         + dateTimeStr + ".exe";
           
File.Copy(exeFilePath, copyExePath,false );

Process.Start(copyExePath);
return;
        }

        Thread mainThread = new Thread(() =>
         {
 // Debugger.Launch();    // You can uncomment to automatically launch VS for debugging
 AppDomainSetup appDomainShodowCopySetup = AppDomain.CurrentDomain.SetupInformation;

 appDomainShodowCopySetup.ShadowCopyFiles = true.ToString();
 appDomainShodowCopySetup.ApplicationBase = applicationFolder.Replace(CatchFolder, "");
 appDomainShodowCopySetup.CachePath = 
	Path.GetDirectoryName(exeFilePath) + "\\" + CatchFolder;
 //Configure shadow copy directories 
 //appDomainShodowCopySetup.ShadowCopyDirectories = "C:\\DllsToBeShadowCopyied";
 AppDomain marvinDomain = AppDomain.CreateDomain
			(DomainName, null, appDomainShodowCopySetup);

 marvinDomain.ExecuteAssembly(exeFilePath);

 AppDomain.Unload(marvinDomain);
         });

        mainThread.Start();
    }
    return;
}

此方法还利用当前 _AppDomain_ 的 _FriendlyName_ 以及应用程序的启动路径。应用程序会检查其启动路径,如果不是来自 _catch_ 路径,则会将自身 EXE 复制到 _AssemblyCatch_ 文件夹,并启动使用新路径 EXE 的新进程。我使用了系统日期时间来为复制的 EXE 文件命名。当新进程启动时,它来自 _catch_ 路径,现在应用程序将创建一个启用影子复制功能的新域,并在该新域中启动应用程序。当应用程序在新域中运行时,它具有有效的域 _Friendly Name_,并且从 _catch_ 路径运行,因此这次它将执行运行 _MainForm_ 的正常工作。

我使用新线程来创建新的 _AppDomain_,因为我在默认 _Main_ 线程上遇到了卸载 _AppDomain_ 的问题。

应用程序开发者如何以及何时获取更新取决于他们自己。应用程序可以定期在后台检查更新,如果发现,它将进行下载。应用程序可以通知用户重新启动应用程序,或者如果可能,它可以自行重新启动并获取新的更改。您可以使用 _AppDomainSetup_ 的 _ShadowCopyDirectories_ 属性指定程序集将从哪些文件夹复制。为了避免重新启动整个应用程序,您还可以设计一个应用程序,它只更新少数模块,这些模块可以位于单独的域中,并在可用更新时被卸载和重新加载。

关注点

在这里,我已经设法为默认应用程序域启用了影子复制。因此,原始应用程序程序集保持解锁状态,应用程序本身可以替换它们。通过对少数模块进行影子复制,您可以确保您的应用程序能够永久运行而无需重新启动,并且仍然能够获得更新。

© . All rights reserved.