Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Load Current Assembly into different AppDomain

Tags:

c#

.net

appdomain

I have created an AppDomain with a different base directory. However, I cannot seem to load the currently executing assembly into the other AppDomain without having a copy of the current executing assembly in the base directory. I've even tried to load it from the bytes.

I get no exception when I try to load, but when I try to use:

domain.DoCallBack(new CrossAppDomainDelegate(... 

I get:

Could not load file or assembly ........... The system cannot find the file specified.

My code is as follows:

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;
}

Update:

It seems the assembly is loaded if i look in output i see this

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

but i still get the exception... i don't understand this

System.IO.FileNotFoundException was unhandled Message=Could not load file or assembly 'TaskExecuter, Version=1.0.4244.31921, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. Source=mscorlib
FileName=TaskExecuter, Version=1.0.4244.31921, Culture=neutral, PublicKeyToken=null FusionLog==== Pre-bind state information === LOG: User = Peter-PC\Peter LOG: DisplayName = TaskExecuter, Version=1.0.4244.31921, Culture=neutral, PublicKeyToken=null (Fully-specified) LOG: Appbase = file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks LOG: Initial PrivatePath = C:\ProgramData\TaskExecuter\TaskLib\uTorrentTasks\Libs Calling assembly : (Unknown). === LOG: This bind starts in default load context. LOG: Using application configuration file: d:\users\peter\documents\visual studio 2010\Projects\TaskExecuter\TaskExecuter.Terminal\bin\Release\TaskExecuter.Terminal.vshost.exe.Config LOG: Using host configuration file: LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config. LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind). LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter.DLL. LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter/TaskExecuter.DLL. LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter.DLL. LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter/TaskExecuter.DLL. LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter.EXE. LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter/TaskExecuter.EXE. LOG: Attempting download of new URL file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter.EXE. LOG: Attempting download of new URL file:///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 50 at TaskExecuter.TaskManagment.TaskFinder.Probe() in d:\users\peter\documents\visual studio 2010\Projects\TaskExecuter\TaskExecuter\TaskManagment\TaskFinder.cs:line 29 at TaskExecuter.TaskManagment.TaskManager.LoadTasks() in d:\users\peter\documents\visual studio 2010\Projects\TaskExecuter\TaskExecuter\TaskManagment\TaskManager.cs:line 63 at TaskExecuter.TaskManagment.TaskManager.Start() in d:\users\peter\documents\visual studio 2010\Projects\TaskExecuter\TaskExecuter\TaskManagment\TaskManager.cs:line 95 at TaskExecuter.Terminal.Program.Main(String[] args) in d:\users\peter\documents\visual studio 2010\Projects\TaskExecuter\TaskExecuter.Terminal\Program.cs:line 16 at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args) at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args) at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() at System.Threading.ThreadHelper.ThreadStart_Context(Object state) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.ThreadHelper.ThreadStart()
InnerException:

like image 846
Peter Avatar asked Jul 31 '11 23:07

Peter


2 Answers

I was able to recover the linked to blog post using archive.org and also come up with a working solution.

My goal was to dynamically compile an exe into a temporary location, and then have that exe shadow load all main dlls in a child appdomain so that the main application that spawned the exe can be updated easily. The basic approach is using childAppDomain.CreateInstanceFrom to create a type that in the constructor installs the assembly resolve event handler. My code looked like

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);

And the type that creates the needed AssemblyResolve handler (the blog post below describes why you need another type)

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;
    }
}

Here is the original blog post:

Application Domains is hard…

Have you ever been working with Application Domain in .NET?, in the beginning it doesn’t seem all that difficult, but ones you get to know them you begin to realize all the little difficulties.

Everything works fine as long as you don’t move outside the Host AppDomains.BaseDirectory, but in our case we wanted to have Plug-ins deployed at say location “C:\My Plug-ins” while the host application would run at “C:\Program Files\My App”, since we might run into dependencies from the AppDomain to some of the Host Assemblies problems was apparently inevitable.

The Classic Here is some simple code and our first attempt.

 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);

Seems very simple, but because “ApplicationBase” is different from “AppDomain.CurrentDomain.BaseDirectory” we ran into what seems to be a very well know exception.

System.IO.FileNotFoundException: Could not load file or assembly 'Host.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

If you have worked with any sort of dynamically loading assemblies I am fairly sure that is familiar to you. And the issue is that “Host.Services” was know within the Host Application Domain because it is stored in “C:\Program Files\My App”, and the Application Domain looking for it is looking in “C:\My Plug-ins”.

Well we Thought we instructed it to also look in “AppDomain.CurrentDomain.BaseDirectory” which would be “C:\Program Files\My App”, but that was not the case.

AppDomain.AssemblyResolve to the rescue? Ok so we have been working with these quirks before, so we knew how we could use “AppDomain.AssemblyResolve” to manually resolve any assemblies that the AppDomain it self could not handle.

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;

That should work right, well we thought so and ones again we were wrong, what happens now is that instead of actually getting as far as initializing the Application Domain and using it, instead it fails right where we hooked up the event handler for resolving assemblies.

Again the exception looks very much like the previous mentioned, but this time it can’t find the Assembly that contains the Type that has the “Resolve” handler we set up in the very last line in the above snippet.

AppDomain.Load then! Ok, so obviously when hooking up the event handler, the Application Domain needs to know the Type of the object handling that event, that is actually fairly understandable when you think about it, so if the Application Domain can’t even find that one and load we can’t really handle anything.

So what is next? Our idea was to manually instruct the Application Domain to load a shallow assembly that didn’t have any other dependencies that what could be found in the GAC, and the hook an event handler up.

 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;

Using a very simple little class like the following, and don’t mind the odd Resolve behavior.

 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:  }

So yes or no?… NO!… same problem still.

Things are much more simple! Actually things became much more simple in the end when we managed to make it work.

I Can’t say how exactly the .NET team has envisioned that this should work, we couldn't really find out any useable things that the “PrivateBinPath” and “PrivateBinPathProbe” was used for. Well we use them now, and made them work as we expected they would!

So we changed the “AssemblyLoader” class to look like this instead:

   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:  }

So rather than hooking up the event where we created the Application Domain, we let the class do it by it self, and to “CurrentDomain” instead.

Ok so wait, doesn’t that cause an issue when creating it in the factory since it is now loading for the wrong domain? Well thankfully you are able to create objects within domains from the outside.

So creating the domain is now done as follows:

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");

We don’t even care for maintaining a reference to the “AssemblyLoader” since it should pretty much be kept alive by hooking it self up to the Event.

Hopefully this can help some that has stumbled over the same problem, I see many workarounds where people then either just let Plug-ins be installed in same host directory, having all the necessary dependencies deployed along with the plug-in even though it isn’t something the plug-in knows it is dependant on and so forth.

The above at least enables us to install plug-ins away from our host application base which I think is nice.

If anyone have solved this differently, then please make a response, maybe we can find pros and cons in either way, or just discover a better solution.

If you have any questions or can’t get the above to work, then feel free to ask.

author: Jens Melgaard | posted @ Thursday, July 01, 2010 3:08 PM | Feedback (0)

like image 182
BrandonAGr Avatar answered Sep 23 '22 18:09

BrandonAGr


Is there any reason why you do not use the original assemblies ?

So unless your foreign appdomain uses credentials that prevent it from accessing the original assemblies, the method AppDomain.CreateInstanceFromAndUnwrap is capable of doing so.

I suggest you isolate your remotely executed code in a MarshalByRefObject class, using a class like this :

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

And use it like this :

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

remote.SetupLogging();

This will avoid the unnecessary trouble of passing return values via appdomain state, as DoCallBack does not return values. This will also avoid mixing AppDomain plumbing code with your application logic.

Finally, you may need to intercept AppDomain.AssemblyResolve inside MyRemoteClass for other dependencies to load properly, though.

like image 23
Jérôme Laban Avatar answered Sep 22 '22 18:09

Jérôme Laban