使用AppDomain的影子副本在运行时覆盖exe文件

4
在以下示例应用程序中,我创建了一个新的AppDomain并启用了影子复制来执行它。然后,我试图从新的AppDomain中删除(替换)原始主exe文件。但是,我会收到一个“访问被拒绝”的错误。有趣的是,在启动程序后,可以在Windows资源管理器中重命名主exe文件(但无法删除它)。
影子复制能否用于运行时覆盖主exe文件?
static void Main(string[] args)
{
    // enable comments if you wanna try to overwrite the original exe (with a 
    // copy of itself made in the default AppDomain) instead of deleting it

    if (AppDomain.CurrentDomain.IsDefaultAppDomain())
    {
        Console.WriteLine("I'm the default domain");
        System.Reflection.Assembly currentAssembly = System.Reflection.Assembly.GetExecutingAssembly();
        string startupPath = currentAssembly.Location;

        //if (!File.Exists(startupPath + ".copy"))
        //    File.Copy(startupPath, startupPath + ".copy");

        AppDomainSetup setup = new AppDomainSetup();
        setup.ApplicationName = Path.GetFileName(startupPath);
        setup.ShadowCopyFiles = "true";

        AppDomain domain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup);
        domain.SetData("APPPATH", startupPath);

        domain.ExecuteAssembly(setup.ApplicationName, args);

        return;
    }

    Console.WriteLine("I'm the created domain");
    Console.WriteLine("Replacing main exe. Press any key to continue");
    Console.ReadLine();

    string mainExePath = (string)AppDomain.CurrentDomain.GetData("APPPATH");
    //string copyPath = mainExePath + ".copy";
    try
    {
        File.Delete(mainExePath );
        //File.Copy(copyPath, mainExePath );
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error! " + ex.Message);
        Console.ReadLine();
        return;
    }

    Console.WriteLine("Succesfull!");
    Console.ReadLine();
}

你想要达到什么目的?覆盖exe文件应该被拒绝,因为该文件正在使用中。 - Iain Ballard
是的,我想覆盖exe。来自MSDN的:“影子复制使得在应用程序域中使用的程序集可以在不卸载应用程序域的情况下更新”。这仅适用于dll而不适用于exe吗? - Mauro Ganswer
那个影子副本是相反的——先复制,然后运行副本,以便可以替换原始文件。来自MSDN的说明:“当应用程序域配置为影子复制文件时,应用程序路径中的程序集将被复制到另一个位置并从该位置加载。复制品被锁定,但原始程序集文件未被锁定,因此可以进行更新。”如果您正在进行实验、尝试修改运行时行为或进行原地更新,或者其他操作,请告知,这将有助于更好地解决问题。 - Iain Ballard
看看MEF吧 - 你可以很容易地使用它来热插拔插件代码。通过简单更改源exe文件来更改正在运行的应用程序是不可能的。请参阅此问题:https://dev59.com/PW_Xa4cB1Zd3GeqPzEE1 - Iain Ballard
没有任何一种运行代码可以仅通过更改底层文件来替换。执行本地代码是映射在内存中的,而不是从磁盘上运行。一些虚拟机(如Erlang的)可以处理重新加载,但大多数系统不能。影子复制允许您在副本正在运行时替换原始文件。要在.Net中热交换代码,您必须有一个主机exe(它本身不会更改),并具有一个进程来根据需要加载和交换代码。MEF并没有默认处理所有这些问题,但对于有经验的开发人员来说并不太困难。我将尝试挖掘一些示例代码。 - Iain Ballard
显示剩余2条评论
3个回答

10

您可以通过在单个应用程序中使用多个AppDomain来实现自更新应用程序。诀窍是将应用程序可执行文件移动到临时目录并复制回您的目录,然后在新的AppDomain中加载复制的可执行文件。

static class Program
{
    private const string DELETED_FILES_SUBFOLDER = "__delete";

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [LoaderOptimization(LoaderOptimization.MultiDomainHost)]
    [STAThread]
    static int Main()
    {
        // Check if shadow copying is already enabled
        if (AppDomain.CurrentDomain.IsDefaultAppDomain())
        {
            // Get the startup path.
            string assemblyPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
            string assemblyDirectory = Path.GetDirectoryName(assemblyPath);
            string assemblyFile = Path.GetFileName(assemblyPath);

            // Check deleted files folders existance
            string deletionDirectory = Path.Combine(assemblyDirectory, DELETED_FILES_SUBFOLDER);
            if (Directory.Exists(deletionDirectory))
            {
                // Delete old files from this folder
                foreach (var oldFile in Directory.EnumerateFiles(deletionDirectory, String.Format("{0}_*{1}", Path.GetFileNameWithoutExtension(assemblyFile), Path.GetExtension(assemblyFile))))
                {
                    File.Delete(Path.Combine(deletionDirectory, oldFile));
                }
            }
            else
            {
                Directory.CreateDirectory(deletionDirectory);
            }
            // Move the current assembly to the deletion folder.
            string movedFileName = String.Format("{0}_{1:yyyyMMddHHmmss}{2}", Path.GetFileNameWithoutExtension(assemblyFile), DateTime.Now, Path.GetExtension(assemblyFile));
            string movedFilePath = Path.Combine(assemblyDirectory, DELETED_FILES_SUBFOLDER, movedFileName);
            File.Move(assemblyPath, movedFilePath);
            // Copy the file back
            File.Copy(movedFilePath, assemblyPath);

            bool reload = true;
            while (reload)
            {
                // Create the setup for the new domain
                AppDomainSetup setup = new AppDomainSetup();
                setup.ApplicationName = assemblyFile;
                setup.ShadowCopyFiles = true.ToString().ToLowerInvariant();

                // Create an application domain. Run 
                AppDomain domain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup);

                // Start application by executing the assembly.
                int exitCode = domain.ExecuteAssembly(setup.ApplicationName);
                reload = !(exitCode == 0);
                AppDomain.Unload(domain);
            }
            return 2;
        }
        else
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            MainForm mainForm = new MainForm();
            Application.Run(mainForm);
            return mainForm.ExitCode;
        }
    }
}

你是将新文件复制到已删除的目录还是程序集路径? - H20rider
您可以将新文件复制并覆盖程序集路径中的现有二进制文件。 - Suleyman OZ

0

您特别要求在更新过程中使用ShadowCopy。如果这不是一个固定的要求(为什么会是呢?),以下这些内容对我来说真正开了眼界:

https://visualstudiomagazine.com/articles/2017/12/15/replace-running-app.aspx

https://www.codeproject.com/Articles/731954/Simple-Auto-Update-Let-your-application-update-i

这归结于您重命名目标文件(即使它正在运行,也是允许的),然后将所需文件移动/复制到现在空闲的目标位置。

vs-magazine文章非常详细,包括一些巧妙的技巧,例如查找当前应用程序是否正在使用文件(尽管仅适用于exe,对于.dll和其他文件,必须想出解决方案)。


0
作为 MEF 的一个有趣用例,我已经快速制作了一个演示,展示了如何在 C# 中热交换运行代码。这很简单,但省略了许多边缘情况。

https://github.com/i-e-b/MefExperiments

值得注意的类:

  • src/PluginWatcher/PluginWatcher.cs -- 监视文件夹以获取合同的新实现
  • src/HotSwap.Contracts/IHotSwap.cs -- 热交换的最小基本合同
  • src/HotSwapDemo.App/Program.cs -- 执行实时代码交换

这不会锁定插件文件夹中的任务 .dll,因此您可以在部署新版本后删除旧版本。希望这有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接