I'm trying to load my plugin dll into separate AppDomain, but Load() method fails with FileNotFoundException. Moreover, it seems like setting PrivateBinPath property of AppDomainSetup has no effect, because in log I see "Initial PrivatePath = NULL". All plugin have strong name. Normally each plugin is stored in [Application startp path]\postplugins\[plugindir]. If I put plugins subdirectories under [Application startp path] directory, everything works. I also have tried to change AppBase property manually but it does not change.
Here is the code:
public void LoadPostPlugins(IPluginsHost host, string pluginsDir)
{
_Host = host;
var privatePath = "";
var paths = new List<string>();
//build PrivateBinPath
var dirs = new DirectoryInfo(pluginsDir).GetDirectories();
foreach (var d in dirs)
{
privatePath += d.FullName;
privatePath += ";";
}
if (privatePath.Length > 1) privatePath = privatePath.Substring(0, privatePath.Length - 1);
//create new domain
var appDomainSetup = new AppDomainSetup { PrivateBinPath = privatePath };
Evidence evidence = AppDomain.CurrentDomain.Evidence;
var sandbox = AppDomain.CreateDomain("sandbox_" + Guid.NewGuid(), evidence, appDomainSetup);
try
{
foreach (var d in dirs)
{
var files = d.GetFiles("*.dll");
foreach (var f in files)
{
try
{
//try to load dll - here I get FileNotFoundException
var ass = sandbox.Load(AssemblyName.GetAssemblyName(f.FullName));
var f1 = f;
paths.AddRange(from type in ass.GetTypes()
select type.GetInterface("PluginsCore.IPostPlugin")
into iface
where iface != null
select f1.FullName);
}
catch (FileNotFoundException ex)
{
Debug.WriteLine(ex);
}
}
}
}
finally
{
AppDomain.Unload(sandbox);
}
foreach (var plugin in from p in paths
select Assembly.LoadFrom(p)
into ass
select
ass.GetTypes().FirstOrDefault(t => t.GetInterface("PluginsCore.IPostPlugin") != null)
into type
where type != null
select (IPostPlugin)Activator.CreateInstance(type))
{
plugin.Init(host);
plugin.GotPostsPartial += plugin_GotPostsPartial;
plugin.GotPostsFull += plugin_GotPostsFull;
plugin.PostPerformed += plugin_PostPerformed;
_PostPlugins.Add(plugin);
}
}
And here is the log:
'FBTest.vshost.exe' (Managed (v4.0.30319)): Loaded 'D:\VS2010Projects\PNotes - NET\pnfacebook\FBTest\bin\Debug\postplugins\pnfacebook\pnfacebook.dll', Symbols loaded.
A first chance exception of type 'System.IO.FileNotFoundException' occurred in FBTest.exe
System.IO.FileNotFoundException: Could not load file or assembly 'pnfacebook, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9e2a2192d22aadc7' or one of its dependencies. The system cannot find the file specified.
File name: 'pnfacebook, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9e2a2192d22aadc7'
at System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity, RuntimeAssembly reqAssembly, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.InternalLoad(String assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean forIntrospection)
at System.Reflection.RuntimeAssembly.InternalLoad(String assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
at System.Reflection.Assembly.Load(String assemblyString)
at System.UnitySerializationHolder.GetRealObject(StreamingContext context)
at System.AppDomain.Load(AssemblyName assemblyRef)
at PNotes.NET.PNPlugins.LoadPostPlugins(IPluginsHost host, String pluginsDir) in D:\VS2010Projects\PNotes - NET\pnfacebook\FBTest\PNPlugins.cs:line 71
=== Pre-bind state information ===
LOG: User = ANDREYHP\Andrey
LOG: DisplayName = pnfacebook, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9e2a2192d22aadc7
(Fully-specified)
LOG: Appbase = file:///D:/VS2010Projects/PNotes - NET/pnfacebook/FBTest/bin/Debug/
LOG: Initial PrivatePath = NULL
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: No application configuration file found.
LOG: Using host configuration file:
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.
LOG: Post-policy reference: pnfacebook, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9e2a2192d22aadc7
LOG: Attempting download of new URL file:///D:/VS2010Projects/PNotes - NET/pnfacebook/FBTest/bin/Debug/pnfacebook.DLL.
LOG: Attempting download of new URL file:///D:/VS2010Projects/PNotes - NET/pnfacebook/FBTest/bin/Debug/pnfacebook/pnfacebook.DLL.
LOG: Attempting download of new URL file:///D:/VS2010Projects/PNotes - NET/pnfacebook/FBTest/bin/Debug/pnfacebook.EXE.
LOG: Attempting download of new URL file:///D:/VS2010Projects/PNotes - NET/pnfacebook/FBTest/bin/Debug/pnfacebook/pnfacebook.EXE.
when you load an assembly into the AppDomain in that way, it is the current AppDomain's PrivateBinPath that is used to find the assembly.
For your example, when I added the following to my App.config it ran fine:
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="[PATH_TO_PLUGIN]"/>
</assemblyBinding>
</runtime>
This is not very useful to you though.
What I did instead was to create a new assembly that contained the IPostPlugin and IPluginsHost interfaces, and also a class called Loader that looked like this:
public class Loader : MarshalByRefObject
{
public IPostPlugin[] LoadPlugins(string assemblyName)
{
var assemb = Assembly.Load(assemblyName);
var types = from type in assemb.GetTypes()
where typeof(IPostPlugin).IsAssignableFrom(type)
select type;
var instances = types.Select(
v => (IPostPlugin)Activator.CreateInstance(v)).ToArray();
return instances;
}
}
I keep that new assembly in the application root, and it doesn't need to exist in the plugin directories (it can but won't be used as the application root will be searched first).
Then in the main AppDomain I did this instead:
sandbox.Load(typeof(Loader).Assembly.FullName);
Loader loader = (Loader)Activator.CreateInstance(
sandbox,
typeof(Loader).Assembly.FullName,
typeof(Loader).FullName,
false,
BindingFlags.Public | BindingFlags.Instance,
null,
null,
null,
null).Unwrap();
var plugins = loader.LoadPlugins(AssemblyName.GetAssemblyName(f.FullName).FullName);
foreach (var p in plugins)
{
p.Init(this);
}
_PostPlugins.AddRange(plugins);
So I create an instance of the known Loader type, and then get that to create the plugin instances from within the plug-in AppDomain. That way the PrivateBinPaths are used as you want them to be.
One other thing, the private bin paths can be relative so rather than adding d.FullName
you could add pluginsDir + Path.DirectorySeparatorChar + d.Name
to keep the final path list short. That's just my personal preference though! Hope this helps.