I am trying to make a small C# tool to compare two svn revision builds and track properties changes in any classes. My goal is to use reflection to compare the properties of each class of my dll without using Momo.Cecil.
From experimenting, then reading this article Assembly Loading and a few threads found on Google, I learnt that two DLLs with same identities would get resolved as the same if we dont use Assembly.ReflectionOnlyLoadFrom.
Trying to use their code to create an AppDomain for each loading, and also try many variants from searches, I get this exception and I can't find any thread explaining how to solve this issue:
API restriction: The assembly 'file:///D:\somepath\82\MyLib.dll' has already loaded from a different location. It cannot be loaded from a new location within the same appdomain.
This error happen on the 2nd call (the path 82) on the following line:
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyPath);
Maybe I din't understand something very basic which make me unable to create a new AppDomain correctly?
Here is all the code used to reproduce this problem.
Code from my entry point
//Let say one of the property has been renamed between both commits
var svnRev81Assembly = ReflectionOnlyLoadFrom(#"D:\somepath\81\MyLib.dll");
var svnRev82Assembly = ReflectionOnlyLoadFrom(#"D:\somepath\82\MyLib.dll");
Implementation of the loader
private string _CurrentAssemblyKey;
private Assembly ReflectionOnlyLoadFrom(string assemblyPath)
{
var path = Path.GetDirectoryName(assemblyPath);
// Create application domain setup information.
AppDomainSetup domaininfo = new AppDomainSetup();
domaininfo.ApplicationBase = path;
domaininfo.PrivateBinPath = path;
_CurrentAssemblyKey = Guid.NewGuid().ToString();
AppDomain currentAd = AppDomain.CreateDomain(_CurrentAssemblyKey, null, domaininfo); //Everytime we create a new domain with a new name
//currentAd.ReflectionOnlyAssemblyResolve += CustomReflectionOnlyResolver; This doesnt work anymore since I added AppDomainSetup
currentAd.SetData(_CurrentAssemblyKey, path);
//Loading to specific location - folder 81 or 82
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyPath);
//Preloading the
foreach (var assemblyName in assembly.GetReferencedAssemblies())
{
Assembly.ReflectionOnlyLoadFrom(Path.Combine(path, assemblyName.Name + ".dll"));
}
Type[] types = assembly.GetTypes();
// Lastly, reset the ALS entry and remove our handler
currentAd.SetData(_CurrentAssemblyKey, null);
//currentAd.ReflectionOnlyAssemblyResolve -= CustomReflectionOnlyResolver; This doesnt work anymore since I added AppDomainSetup
return assembly;
}
This can solved by loading dlls in seperate appdomain.
private static void ReflectionOnlyLoadFrom()
{
var appDomain = AppDomain.CreateDomain("Temporary");
var loader1 = (Loader)appDomain.CreateInstanceAndUnwrap(typeof(Loader).Assembly.FullName, typeof(Loader).FullName);
loader1.Execute(#"D:\somepath\81\MyLib.dll");
var loader2 = (Loader)appDomain.CreateInstanceAndUnwrap(typeof(Loader).Assembly.FullName, typeof(Loader).FullName);
loader2.Execute(#"D:\somepath\82\MyLib.dll");
loader1.CompareTwoDLLs(loader2);
AppDomain.Unload(appDomain);
}
Loader.cs
public class Loader : MarshalByRefObject
{
public Assembly TempAssembly { get; set; }
public string Execute(string dllPath)
{
TempAssembly = Assembly.LoadFile(dllPath);
return TempAssembly.FullName;
}
public void GetRefAssemblyTypes()
{
foreach (var refAssembly in TempAssembly.GetReferencedAssemblies())
{
var asm = Assembly.Load(refAssembly);
var asmTypes = asm.GetTypes();
}
}
public void CompareTwoDLLs(Loader l2)
{
var types1 = TempAssembly.GetTypes();
var types2= l2.TempAssembly.GetTypes();
GetRefAssemblyTypes();
//logic to return comparison result
}
}
Related
I am trying to learn more about Reflection, and took some code already built and added to it. Now I am trying to query the GAC for other assemblies and build type instances, and etc. I modified the code I found here, but myAssemblyList is empty. Can you tell me what I am doing wrong? I debugged and placed a break at "var currentAssembly = value.GetAssembly(f);" and it returns null. All the code I have seen populates Assemblies from the current AppDomain, but I have seen methods like LoadFrom(), which should work with a directory path. I also saw this post, and compiled it.
class Program
{
static void Main(string[] args)
{
AppDomainSetup domaininfo = new AppDomainSetup();
domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
Evidence adevidence = AppDomain.CurrentDomain.Evidence;
AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);
Type type = typeof(Proxy);
var value = (Proxy)domain.CreateInstanceAndUnwrap(
type.Assembly.FullName,
type.FullName);
//String myDir = "C:\\Windows\\Microsoft.NET\\assembly\\GAC_64\\";
String myDir = "C:\\Windows\\Microsoft.NET\\assembly\\";
List<Assembly> myAssemblyList = new List<Assembly>();
foreach (String f in Directory.GetFiles(myDir, "*.dll", SearchOption.AllDirectories))
{
//Console.WriteLine($"Here is f: {f}");
var currentAssembly = value.GetAssembly(f);
if (currentAssembly != null)
{
myAssemblyList.Add(currentAssembly);
Console.WriteLine(currentAssembly.FullName);
//Console.ReadLine();
}
Console.WriteLine($"Total Assemblies found: {myAssemblyList.Count}");
}
Console.WriteLine($"Total Assemblies found: {myAssemblyList.Count}");
Console.ReadLine();
}
}
public class Proxy : MarshalByRefObject
{
public Assembly GetAssembly(string assemblyPath)
{
try
{
return Assembly.LoadFile(assemblyPath);
}
catch (Exception)
{
return null;
// throw new InvalidOperationException(ex);
}
}
}
I jumped one directory back, and tried to collect from GAC_* i.e. 32, 64, and MSIL. I added a test for null for currentAssembly to address an issue with GetAssembly(). But still some directories that contain dll and non-dll files cause exceptions.
modify this line:
foreach (String f in Directory.GetFiles(myDir))
with
foreach (String f in Directory.GetFiles(myDir, "*.dll", SearchOption.AllDirectories)
I want to make a reloadable assembly function for scripting.(So i can debug scripts quicker)
The dll generation works and loading too. The main problem is, that I`m using the functions of my temporary AppDomain in my main AppDomain. The dll seems to be linked to my main AppDomain too, because I cant delete it while the program is running.
If I remove all MethodInfo references from my main AppDomain "context" then I have no problems deleting it.
Here you can see how the program works:
Generate DLL from external process
Load DLL by (temp AppDomain).DoCallBack(...)
Get Type & MethodInfo and call it.
AppDomain.Unload(temp AppDomain)
So if I skip step 3, I have no problems deleten the dll. But I cant check if the return of the Function really shows the updated value (which i edit inside the script).
I have posted the source code for each step here:
1.
//This actually isn`t as important for you
Process assemblyGeneration = new Process();
assemblyGeneration.StartInfo.FileName = AppDomain.CurrentDomain.BaseDirectory + #"lib\AssemblyCompiler.exe";
assemblyGeneration.StartInfo.Arguments = "\"" + AppDomain.CurrentDomain.BaseDirectory + "script.dll\" \"" + source + "\" \"System.dll\"";
assemblyGeneration.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
assemblyGeneration.StartInfo.UseShellExecute = true;
assemblyGeneration.Start();
assemblyGeneration.WaitForExit();
assemblyGeneration.Dispose();
2.
_appDomain.DoCallBack(() =>
{
byte[] fileContent = File.ReadAllBytes(AppDomain.CurrentDomain.BaseDirectory + "script.dll");
AppDomain.CurrentDomain.Load(fileContent);
});
3.
MethodInfo info = _compiledScript.GetTypeFrom("LSCT.ScriptClass").GetMethod("DefineX");
var func = (Func<double>) Delegate.CreateDelegate(typeof(Func<double>), info);
double x = func();
Console.WriteLine("Output was : " + x);
4.
AppDomain.Unload(_appDomain);
So is there a way to work around the issue, that I cant reload the DLL?
I think I have solved the problem. Maybe there is a performance issue now but I have not checked it yet.
Anyways, I created a new class which inherit MarshalByRefObject and let it load from the temp AppDomain.
Here is the code for the new class:
public class ProcessRunner : MarshalByRefObject
{
public void Run()
{
Type typ = null;
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
{
Type t = asm.GetType("LSCT.ScriptClass");
if (t != null)
{
typ = t;
break;
}
}
MethodInfo info = typ.GetMethod("DefineX");
var func = (Func<double>)Delegate.CreateDelegate(typeof(Func<double>), info);
double x = func();
Console.WriteLine("Output was : " + x);
}
}
And here is how to invoke it from the temp AppDomain:
ProcessRunner pr = (ProcessRunner)_appDomain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, "Animation_Engine.UserInterface.Code.ProcessRunner");
pr.Run();
Note: As soon as you pass any references from your main AppDomain (by
parameters for example) to the executing class, the dll will be linked to your main AppDomain
and is not able to unload until the main AppDomain ends.
This is my code:
System.Security.PermissionSet PS = new System.Security.PermissionSet(PermissionState.None);
PS.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess,Path));
PS.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
AppDomainSetup ADS = new AppDomainSetup();
ADS.ApplicationBase= Path;
AppDomain domain = AppDomain.CreateDomain("Pluging", null, ADS, PS, null);
Assembly asm = Assembly.LoadFrom(Path + "MacroBase.dll");
domain.Load(asm.FullName);
MacroBase.MacroBase em = (MacroBase.MacroBase)domain.CreateInstanceAndUnwrap(asm.FullName, "MacroBase.MacroBase");
em.Application(1);
parameter Path has the address of floder that contains dll. Right now it is
"D:\Programming Projects\Server3\Macros\c7b465b2-8314-4c7e-be3c-10c0185b4ac6"
a copy of macrobase.dll is inside that Guid folder. Appdomain loads this dll and runs the method Application.
I expected the last line not to be able to access c:\ due to FileIOPermissionAccess that was applied at the beginning, but the mentioned method:
MacroBase.Application(int i)
{
System.IO.File.ReadAllBytes("c:\\test1_V.103.xls");
}
runs as if it was fully unrestricted.
based on this article from Microsoft:
How to: Run Partially Trusted Code in a Sandbox
I have also tried the following format with no better results(It can access c:):
System.Security.PermissionSet PS = new System.Security.PermissionSet(PermissionState.None);
PS.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess,Path));
PS.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
AppDomainSetup ADS = new AppDomainSetup();
ADS.ApplicationBase= Path;
AppDomain domain = AppDomain.CreateDomain("Pluging", null, ADS, PS, null);
Assembly asm = Assembly.LoadFrom(Path + "MacroBase.dll");
domain.Load(asm.FullName);
System.Runtime.Remoting.ObjectHandle handle = Activator.CreateInstanceFrom(domain, Path + "MacroBase.dll", "MacroBase.MacroBase");
MacroBase.MacroBase m = (MacroBase.MacroBase)handle.Unwrap();
m.Application(1);
MacroBase.Macrobase is a placeholder for future macros. It is placed inside a dll called macrobase.dll . Right now it only contains some dummy code:
namespace MacroBase
{
[Serializable]
public class MacroBase
{
public void Application(int i)
{
List<int> i1 = new System.Collections.Generic.List<int>() { 1,2,3,4};
System.IO.File.ReadAllBytes("c:\\test1_V.103.xls");
switch(i)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
}
}
Your class marked as [Serializable] and does not derive from MarshalByRefObject, that means when instance get thru Application domain boundaries, it get serialized and than deserialized in target domain. So your code get executed in your current domain rather then in separate domain. You should derive your MacroBase.Macrobase class from MarshalByRefObject, to make code to be executed in separate domain.
Here's a small class I'm using to probe for a list of available plugins:
internal static class PluginDirectoryLoader
{
public static PluginInfo[] ListPlugins(string path)
{
var name = Path.GetFileName(path);
var setup = new AppDomainSetup
{
ApplicationBase = path,
ShadowCopyFiles = "true"
};
var appdomain = AppDomain.CreateDomain("PluginDirectoryLoader." + name, null, setup);
var exts = (IServerExtensionDiscovery)appdomain.CreateInstanceAndUnwrap("ServerX.Common", "ServerX.Common.ServerExtensionDiscovery");
PluginInfo[] plugins = null;
try
{
plugins = exts.ListPlugins(); // <-- BREAK HERE
}
catch
{
// to do
}
finally
{
AppDomain.Unload(appdomain);
}
return plugins ?? new PluginInfo[0];
}
}
The path parameter points to a subdirectory containing the plugin assemblies to load. The idea is to load them using a separate AppDomain with shadow copying enabled.
In this case, shadow copying isn't really necessary seeing as the AppDomain is unloaded quickly, but when I actually load the plugins in the next block of code I intend to write, I want to use shadow copying so the binaries can be updated on the fly. I have enabled shadow copying in this class as a test to make sure I'm doing it right.
Apparently I'm not doing it right because when I break in the debugger on the commented line in the code sample (i.e. plugins = exts.ListPlugins()), the original plugin assemblies are locked by the application!
Seeing as I'm specifying that assemblies loaded by the AppDomain should be shadow copied, why are they being locked by the application?
I figured it out. There was one property I missed in AppDomainSetup. The property was ShadowCopyDirectories.
var setup = new AppDomainSetup
{
ApplicationBase = path,
ShadowCopyFiles = "true",
ShadowCopyDirectories = path
};
When breaking on the line mentioned in my question, I can now delete the plugin assemblies even without unloading the AppDomain.
I am building a program that uses a very simple plugin system. This is the code I'm using to load the possible plugins:
public interface IPlugin
{
string Name { get; }
string Description { get; }
bool Execute(System.Windows.Forms.IWin32Window parent);
}
private void loadPlugins()
{
int idx = 0;
string[] pluginFolders = getPluginFolders();
Array.ForEach(pluginFolders, folder =>
{
string[] pluginFiles = getPluginFiles(folder);
Array.ForEach(pluginFiles, file =>
{
try
{
System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFile(file);
Array.ForEach(assembly.GetTypes(), type =>
{
if(type.GetInterface("PluginExecutor.IPlugin") != null)
{
IPlugin plugin = assembly.CreateInstance(type.ToString()) as IPlugin;
if(plugin != null)
lista.Add(new PluginItem(plugin.Name, plugin.Description, file, plugin));
}
});
}
catch(Exception) { }
});
});
}
When the user selects a particular plugin from the list, I launch the plugin's Execute method. So far, so good! As you can see the plugins are loaded from a folder, and within the folder are several dll's that are needed but the plugin. My problem is that I can't get the plugin to 'see' the dlls, it just searches the launching applications startup folder, but not the folder where the plugin was loaded from.
I have tried several methods:
1. Changing the Current Directory to the plugins folder.
2. Using an inter-op call to SetDllDirectory
3. Adding an entry in the registry to point to a folder where I want it to look (see code below)
None of these methods work. What am I missing? As I load the dll plugin dynamically, it does not seem to obey any of the above mentioned methods. What else can I try?
Regards,
MartinH.
//HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths
Microsoft.Win32.RegistryKey appPaths = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(
string.Format(
#"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{0}",
System.IO.Path.GetFileName(Application.ExecutablePath)),
Microsoft.Win32.RegistryKeyPermissionCheck.ReadWriteSubTree);
appPaths.SetValue(string.Empty, Application.ExecutablePath);
object path = appPaths.GetValue("Path");
if(path == null)
appPaths.SetValue("Path", System.IO.Path.GetDirectoryName(pluginItem.FileName));
else
{
string strPath = string.Format("{0};{1}", path, System.IO.Path.GetDirectoryName(pluginItem.FileName));
appPaths.SetValue("Path", strPath);
}
appPaths.Flush();
Use Assembly.LoadFrom not Assembly.LoadFile
Whenever I dynamically load plugins like this, I usually create an app domain and load the assembly in the new app domain. When creating an app domain, you can specify the base directory. Dependent assemblies will be loaded from this base directory.