将当前程序集加载到不同的AppDomain中

10

我已经创建了一个具有不同基本目录的AppDomain。但是,如果没有当前执行程序集的副本在基本目录中,我似乎无法将当前执行程序集加载到其他AppDomain中。我甚至尝试从字节加载它。

当我尝试使用时,没有异常出现:

domain.DoCallBack(new CrossAppDomainDelegate(... 

我得到了以下错误信息:

无法加载文件或程序集...........系统找不到指定的文件。

我的代码如下:

private static void SaveAssemblies(Assembly ass, List<byte[]> assemblyByteList)
{
    AssemblyName[] assNames = ass.GetReferencedAssemblies();
    foreach (AssemblyName assName in assNames)
    {
        Assembly referedAss = Assembly.Load(assName);
        if (!referedAss.GlobalAssemblyCache)
        {
            SaveAssemblies(referedAss, assemblyByteList);
        }
    }
    byte[] rawAssembly = File.ReadAllBytes(ass.Location);
    assemblyByteList.Add(rawAssembly);
}

public static AppDomain CreateAppDomain(string dir, string name)
{
    AppDomainSetup domainSetup = new AppDomainSetup();
    domainSetup.ApplicationBase = dir;
    domainSetup.ApplicationName = Path.GetFileName(dir);
    domainSetup.PrivateBinPath = Path.Combine(dir, "Libs");

    AppDomain domain = AppDomain.CreateDomain(name, null, domainSetup);
    //Load system assemblies needed for the module
    List<byte[]> assemblyByteList = new List<byte[]>();
    SaveAssemblies(Assembly.GetExecutingAssembly(), assemblyByteList);

    foreach (byte[] rawAssembly in assemblyByteList)
        domain.Load(rawAssembly);

    domain.DoCallBack(new CrossAppDomainDelegate(SetupLogging));
    return domain;
}

更新:

看起来程序集已被加载,如果我查看输出,我会看到这个

'TaskExecuter.Terminal.vshost.exe' (Managed (v4.0.30319)): Loaded 'NLog' 'TaskExecuter.Terminal.vshost.exe' (Managed (v4.0.30319)): Loaded 'TaskExecuter', Symbols loaded.

但我仍然收到异常...我不理解这个。

System.IO.FileNotFoundException:找不到文件或程序集'TaskExecuter,Version = 1.0.4244.31921,Culture = neutral,PublicKeyToken = null'或其依赖项之一。系统无法找到指定的文件。 Source =mscorlib
FileName = TaskExecuter,Version = 1.0.4244.31921,Culture = neutral,PublicKeyToken = null FusionLog ===预绑定状态信息=== LOG:用户= Peter-PC \ Peter LOG:DisplayName = TaskExecuter,Version = 1.0.4244.31921,Culture = neutral,PublicKeyToken = null(完全指定)LOG:Appbase =file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks LOG:Initial PrivatePath = C:\ProgramData\TaskExecuter\TaskLib\uTorrentTasks\Libs 调用程序集:(未知)。 === LOG:此绑定在默认加载上下文中开始。 LOG:使用应用程序配置文件:d:\ users \ peter \ documents \ visual studio 2010 \ Projects \ TaskExecuter \ TaskExecuter.Terminal \ bin \ Release \ TaskExecuter.Terminal.vshost.exe.Config LOG:使用主机配置文件: LOG:使用计算机配置文件来自C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ config \ machine.config。 LOG:此时未对引用应用策略(私有、自定义、部分或基于位置的程序集绑定)。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter.DLL。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter/TaskExecuter.DLL。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter.DLL。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter/TaskExecuter.DLL。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter.EXE。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter/TaskExecuter.EXE。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter.EXE。 LOG:尝试下载新URL文件:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter/TaskExecuter.EXE。 StackTrace:at System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName,String codeBase,Evidence assemblySecurity,RuntimeAssembly locationHint,StackCrawlMark& stackMark,Boolean throwOnFileNotFound,Boolean forIntrospection,Boolean suppressSecurityChecks)at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName,String codeBase,Evidence assemblySecurity,RuntimeAssembly locationHint,StackCrawlMark& stackMark,Boolean throwOnFileNotFound,Boolean forIntrospection,Boolean suppressSecurityChecks)at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity,StackCrawlMark& stackMark,Boolean forIntrospection,Boolean suppressSecurityChecks)at System.Reflection.RuntimeAssembly.InternalLoad(String assemblyString,Evidence assemblySecurity,StackCrawlMark& stackMark,Boolean forIntrospection)at System.Reflection.Assembly.Load(String assemblyString)at System.Runtime.Serialization.FormatterServices.LoadAssemblyFromString(String assemblyName)at System.Reflection.MemberInfoSerializationHolder..ctor(SerializationInfo info,StreamingContext context)at System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)at TaskExecuter.AppDomainHelper.CreateAppDomain(String dir,String name)in d:\ users \ peter \ documents \ visual studio 2010 \ Projects \ TaskExecuter \ TaskExecuter.AppDomainHelper.cs:line 50at TaskExecuter.TaskManagment.TaskFinder.Probe()in d:\ users \ peter \ documents \ visual studio 2010 \ Projects \ TaskExecuter \ TaskExecuter \ TaskManagment \ TaskFinder.cs:line 29at TaskExecuter.TaskManagment.TaskManager.LoadTasks()in d:\ users \ peter \ documents \ visual studio 2010 \ Projects \ TaskExecuter \ TaskExecuter \ TaskManagment \ TaskManager.cs:line

3
最好的做法是不要将汇编简称为“屁股”。如果不这样称呼它可能会更友好一些 : )。 - Alexei Levenkov
@AlexeiLevenkov,等你开始构建一个StringBuilder来记录有关程序集的信息时,请等一下...你会用什么简称来称呼那个变量?(pluginLog、domainLog、typeLog、___) - JoeBrockhaus
6个回答

12

我能够使用archive.org恢复链接到的博客文章,并提出了可行的解决方案。

我的目标是动态编译exe文件到临时位置,然后使该exe文件在子应用程序域中加载所有主dll文件,以便生成exe的主应用程序可以轻松更新。基本方法是使用childAppDomain.CreateInstanceFrom创建一个类型,在其中构造函数安装程序集解析事件处理程序。我的代码如下:

var exportAppDomain = AppDomain.CreateDomain(
    runnerName,
    null,
    appDomainSetup,
    new PermissionSet(PermissionState.Unrestricted));

exportAppDomain.CreateInstanceFrom(
    Assembly.GetExecutingAssembly().Location,
    "ExportLauncher.AppDomainResolver",
    true,
    BindingFlags.Public | BindingFlags.Instance,
    null,
    new object[] { Assembly.GetExecutingAssembly().Location },
    null,
    null);

同时创建所需的AssemblyResolve处理程序的类型(下面的博客文章介绍了为什么需要另一个类型)

class AppDomainResolver
{
    string _sourceExeLocation;

    public AppDomainResolver(string sourceExeLocation)
    {
        _sourceExeLocation = sourceExeLocation;
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
    }

    Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        if (args.Name.Contains("exporterLauncher")) // why does it not already know it has this assembly loaded? the seems to be required
            return typeof(AppDomainResolver).Assembly;
        else
            return null;
    }
}

以下是原始博客文章的内容:

应用程序域很难......

你是否曾经在.NET中使用应用程序域?刚开始似乎并不那么困难,但是一旦你开始了解它们,你就会意识到所有的小困难。

只要不离开主机AppDomains.BaseDirectory,一切都正常工作。但在我们的情况下,我们想要将插件部署在" C:\My Plug-ins "位置,而主机应用程序将在"C:\Program Files\My App"上运行,因为我们可能会遇到从AppDomain到一些主机程序集的依赖关系问题,问题显然是不可避免的。

这是一个简单的代码示例和我们的第一次尝试。

 1:  string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
   2:  AppDomainSetup setup = new AppDomainSetup
   3:  {
   4:      ApplicationName = name,
   5:      ApplicationBase = applicationBase,
   6:      PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
   7:      PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
   8:      ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
   9:  };
  10:   
  11:  Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
  12:  AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);

看起来非常简单,但因为“ApplicationBase”与“AppDomain.CurrentDomain.BaseDirectory”不同,我们遇到了一个似乎非常熟悉的异常。

System.IO.FileNotFoundException: 找不到文件或程序集 'Host.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 或其中一个依赖项。系统无法找到指定的文件。

如果你曾经使用过任何动态加载程序集,我相信这个问题对你来说是很熟悉的。问题在于,“Host.Services”在主机应用程序域中是已知的,因为它存储在“C:\Program Files\My App”中,而正在寻找它的应用程序域正在寻找“C:\My Plug-ins”中的它。

我们认为我们已经告诉它也要查找“AppDomain.CurrentDomain.BaseDirectory”,即“C:\Program Files\My App”,但事实并非如此。

AppDomain.AssemblyResolve出马了? 好吧,我们之前处理过这些小问题,所以我们知道如何使用“AppDomain.AssemblyResolve”手动解析任何应用程序域本身无法处理的程序集。

1:  string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
   2:  AppDomainSetup setup = new AppDomainSetup
   3:  {
   4:      ApplicationName = name,
   5:      ApplicationBase = applicationBase,
   6:      PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
   7:      PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
   8:      ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
   9:  };
  10:   
  11:  Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
  12:  AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
  13:  domain.AssemblyResolve += Resolve;

看起来应该是行得通的,但我们又错了,现在的情况是,在初始化应用程序域并使用它之前,它就在解析程序集的事件处理程序上失败了。

异常看起来很像之前提到的那个,但这次找不到包含“Resolve”处理程序的类型的程序集。

那么,接下来呢?我们的想法是手动指示应用程序域加载一个浅显的程序集,其中没有其他依赖关系,只能在全局程序集缓存中找到,并挂钩一个事件处理程序。

 1:  string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
   2:  AppDomainSetup setup = new AppDomainSetup
   3:  {
   4:      ApplicationName = name,
   5:      ApplicationBase = applicationBase,
   6:      PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
   7:      PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
   8:      ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
   9:  };
  10:   
  11:  Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
  12:  AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
  13:  domain.Load(File.ReadAllBytes(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Host.AssemblyLoader.dll")));
  14:  domain.AssemblyResolve += new AssemblyLoader(AppDomain.CurrentDomain.BaseDirectory).Handle;

使用以下非常简单的类,不要介意奇怪的Resolve行为。

 1:  [Serializable]
   2:  public class AssemblyLoader
   3:  {
   4:      private string ApplicationBase { get; set; }
   5:   
   6:      public AssemblyLoader(string applicationBase)
   7:      {
   8:          ApplicationBase = applicationBase;
   9:      }
  10:   
  11:      public Assembly Resolve(object sender, ResolveEventArgs args)
  12:      {
  13:          AssemblyName assemblyName = new AssemblyName(args.Name);
  14:          string fileName = string.Format("{0}.dll", assemblyName.Name);
  15:          return Assembly.LoadFile(Path.Combine(ApplicationBase, fileName));
  16:      }
  17:  }

到底是“是”还是“否”?...不!…问题依旧存在。

事情其实很简单!当我们设法让它工作时,事情最终变得更加简单了。

我不能确定.NET团队最初希望这样工作的具体方式,我们无法找到任何有用的信息表明“PrivateBinPath”和“PrivateBinPathProbe”被用于什么。但现在我们使用它们,并使它们按照我们期望的方式工作!

因此,我们将“AssemblyLoader”类更改为以下内容:

   1:  [Serializable]
   2:  public class AssemblyLoader : MarshalByRefObject
   3:  {
   4:      private string ApplicationBase { get; set; }
   5:   
   6:      public AssemblyLoader()
   7:      {
   8:          ApplicationBase = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
   9:          AppDomain.CurrentDomain.AssemblyResolve += Resolve;
  10:      }
  11:   
  12:      private Assembly Resolve(object sender, ResolveEventArgs args)
  13:      {
  14:          AssemblyName assemblyName = new AssemblyName(args.Name);
  15:          string fileName = string.Format("{0}.dll", assemblyName.Name);
  16:          return Assembly.LoadFile(Path.Combine(ApplicationBase, fileName));
  17:      }
  18:  }

因此,我们不再连接创建应用程序域的事件,而是让类自己来完成,将其连接到“CurrentDomain”。

好的,等等,如果在工厂中创建它会不会引起问题,因为现在它正在为错误的域加载?幸运的是,您可以从外部在域内创建对象。

因此,现在创建域的方法如下:

1:  string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
   2:  AppDomainSetup setup = new AppDomainSetup
   3:  {
   4:      ApplicationName = name,
   5:      ApplicationBase = applicationBase,
   6:      PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
   7:      PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
   8:      ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
   9:  };
  10:   
  11:  Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
  12:  AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
  13:  domain.CreateInstanceFrom(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Host.AssemblyLoader.dll"),"Host.AssemblyLoader");

我们甚至不需要维护对“AssemblyLoader”的引用,因为它应该通过将自己连接到事件来保持活��状态。

希望这可以帮助一些遇到同样问题的人,我看到许多解决方法,人们要么只是将插件安装在同一主机目录中,即使插件并不知道它依赖于所需的所有依赖项等等。

以上至少使我们能够将插件安装在远离主机应用程序基础的位置,我认为这很好。

如果有人以不同的方式解决了这个问题,请回复,也许我们可以找到两种方法的优缺点,或者发现更好的解决方案。

如果您有任何问题或无法使上述内容��常工作,请随时提问。

作者:Jens Melgaard | 发布时间:2010年7月1日下午3:08 | 反馈(0)


2
PrivateBinPathProbe不是一个目录,而是一种指示应探测privatebinpath的方法。您应该像使用bool一样使用它。http://msdn.microsoft.com/en-us/library/system.appdomainsetup.privatebinpathprobe%28v=vs.110%29.aspx - JoeBrockhaus

3

您不使用原始程序集是否有任何原因?

因此,除非您的外部应用程序域使用阻止其访问原始程序集的凭据,否则 AppDomain.CreateInstanceFromAndUnwrap 方法能够执行此操作。

我建议您将远程执行的代码隔离在一个 MarshalByRefObject 类中,使用像这样的类:

public class MyRemoteClass : MarshalByRefObject
{
    public void SetupLogging()
    { 
       // ...
    }
}

然后像这样使用:

var assemblyPath = new Uri(typeof(MyRemoteClass).Assembly.CodeBase).LocalPath;
var remote = (MyRemoteClass)domain.CreateInstanceFromAndUnwrap(assemblyPath, "NameSpace.MyRemoteClass");

remote.SetupLogging();

通过使用DoCallBack,避免了通过appdomain状态传递返回值的不必要麻烦,因为DoCallBack不返回值。这还可以避免将AppDomain管道代码与应用程序逻辑混合。

最后,您可能需要在MyRemoteClass中拦截AppDomain.AssemblyResolve以便其他依赖项加载正常。


问题在于您只能加载在“ApplicationBase”下定义的程序集,而两个应用程序域具有完全不同的ApplicationBase值。 - Peter
这是在另一个应用程序域中执行方法的正确方式。 - Recep

2

在从字节加载程序集后,将.GetName().CodeBase设置为null可以解决问题...

在查找了一些资料后,我发现这个页面提供了比我的更好的解决方案!


嗯,不知怎么回事,即使将其赋值为 null,它仍然保留着原始值,停止工作了。 - Peter
8
该链接已失效。我想这就是为什么建议将解决方案粘贴在这里而不是留下一个链接的原因。 - Dragoljub Ćurčić

1

非常感谢您的提醒,但那应该是一条评论而不是答案,因为它与为什么它没有起作用无关... - Peter

0

我的猜测是您错过了错误信息的重要部分:

System.IO.FileNotFoundException未处理 Message=无法加载文件或程序集'TaskExecuter,Version=1.0.4244.31921,Culture=neutral,PublicKeyToken=null' 或其某个依赖项。系统找不到指定的文件。 Source=mscorlib

除了不能从应用程序基础路径之外的其他位置加载程序集外,可能还缺少一些依赖程序集,这些程序集可以被解析和加载。

顺便说一下,如果您开始从字节加载,则应查看已加载到域中的程序集。可能已经加载了依赖程序集,但无法自动解析依赖关系。如果同一个程序集加载了两次,则其类型将不兼容。您会得到有趣的CastException,说YourClass对象无法转换为YourClass。

你可以尝试为你的域注册一个AssemblyResolve事件处理程序,但是这样做很容易陷入一些黑魔法般的.dll地狱。 如果其他方法都失败了,你自己陷入了.dll地狱,就来找我吧: 当DisallowApplicationBaseProbing = true时,需要挂钩AssemblyResolve事件

0
如果您需要自己加载程序集,请避免从字节加载...我建议至少使用完整的程序集路径来加载。
一般来说,要调查加载程序集的问题,请搜索“融合日志查看器”(http://www.bing.com/search?q=fussion+log+viewer),并使用该工具查看代码尝试从哪里加载程序集。

问题在于您只能加载在“ApplicationBase”下定义的程序集,而这两个应用程序域具有完全不同的ApplicationBase值。 - Peter

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