自动更新应用程序






4.88/5 (14投票s)
解释了 .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_ 参数传递给新进程:
- 它自身的 EXE 路径
- 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_ 属性指定程序集将从哪些文件夹复制。为了避免重新启动整个应用程序,您还可以设计一个应用程序,它只更新少数模块,这些模块可以位于单独的域中,并在可用更新时被卸载和重新加载。
关注点
在这里,我已经设法为默认应用程序域启用了影子复制。因此,原始应用程序程序集保持解锁状态,应用程序本身可以替换它们。通过对少数模块进行影子复制,您可以确保您的应用程序能够永久运行而无需重新启动,并且仍然能够获得更新。