Using System.Diagnostics.Process to reflect on referenced assemblies? - c#

I have an executable, say abc.exe, which references a.dll (same folder as executable), b.dll (in random folder), and c.dll (in random folder).
However, these DLLs are not necessarily in the same directory as the executable (or in the GAC), and that's something I cannot do anything about.
I've tried to use System.Reflection.Assembly to try to find all the referenced assemblies used by abc.exe.
foreach (AssemblyName an in assembly.GetReferencedAssemblies())
{
Assembly.Load(an);
}
This seems to load the assemblies that are in the executable's directory (a.dll), but not surprisingly throws file not found exceptions for the others.
My idea is to load the process using System.Diagnostics.Process and then reflect on the process, since that should tell me where to find b.dll and c.dll(?)
However, I don't know how to go about doing so. Is this possible, and if so, how can it be done? Thanks!

So it turns out this can be done with System.Diagnostic.Process, e.g.:
public void findModules(string executablePath)
{
Process process = new Process();
process.StartInfo.FileName = executablePath;
process.Start();
process.WaitForInputIdle();
System.Threading.Thread.Sleep(10000);
ProcessModuleCollection mods = process.Modules;
foreach (ProcessModule m in mods)
{
Console.WriteLine(m.ModuleName + ":" + m.FileName);
}
}
Where ModuleName gives you the name of the assembly and FileName gives you the correct assembly path, regardless of where it is (On the network, in C:\Windows, etc.)
However, I cannot figure out how to 'detect' when an executable has finished loading all the modules. If I remove the sleep line, I see only a partial list of modules, which I guess are the ones that are done loading by the time process.Modules is called.

Related

How to unload assembly created by CSharpCodeProvider?

Problem
CSharpCodeProvider can be used to compile source .cs files into an assembly.
However, the assembly is automatically loaded into the AppDomain.CurrentDomain by default. In my case, this is a problem because I need to be able to re-compile the assembly again during runtime, and since it's already loaded in the CurrentDomain, I can't unload that, so I'm stuck.
I have looked through the docs and there seems to be no way to set the target app domain. I have also tried searching it on Google and only found answers where Assembly.Load was used, which I don't think I can use because I need to compile from raw source code, not a .dll
How would one go about doing this? Are there any alternatives or workarounds?
Main program
using (var provider = new CSharpCodeProvider())
{
param.OutputAssembly = "myCompiledMod"
var classFileNames = new DirectoryInfo("C:/sourceCode").GetFiles("*.cs", SearchOption.AllDirectories).Select(fi => fi.FullName).ToArray();
CompilerResults result = provider.CompileAssemblyFromFile(param, classFileNames);
Assembly newAssembly = result.CompiledAssembly // The assembly is already in AppDomain.CurrentDomain!
// If you try compile again, you'll get an error; that class Test already exists
}
C:/sourceCode/test.cs
public class Test {}
What I tried already
I already tried creating a new AppDomain and loading it in there. What happens is the assembly ends up being loaded in both domains.
// <snip>compile code</snip>
Evidence ev = AppDomain.CurrentDomain.Evidence;
AppDomain domain = AppDomain.CreateDomain("NewDomain", ev);
domain.Load(newAssembly);
The answer was to use CSharpCodeProvider().CreateCompiler() instead of just CSharpCodeProvider, and to set param.GenerateInMemory to false. Now I'm able to see line numbers and no visible assembly .dll files are being created, and especially not being locked. This allows for keeping an assembly in memory and reloading it when needed.

Unable to load symbols for dll in c# project

I have a main application which loads drivers from their compiled dll files. I am running into a problem where I want to be able to debug these dll files but the symbols are not being loaded by the project.
The dll files are part of the solution but have to be built separately from the main application. The main application then at runtime loads these dlls from a directory. I am having an error in one of these loaded dll files (not an exception, just unexpected results) but cannot debug the files. Can anyone give me an idea on how to proceed with debugging these?
EDIT:
To give a better idea of how the dlls are loaded here is the code from the main project:
public List<BaseClass> getObjects(string objectName)
{
List<BaseClass> availableDrivers = new List<BaseClass>();
string currentDir = Directory.GetCurrentDirectory();
currentDir = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath); //Use this to find path even when running as service
if (Directory.Exists(currentDir + "\\Objects"))
{
string[] files = Directory.GetFiles(currentDir + "\\Objects", "*.dll");
foreach (string file in files)
{
Console.WriteLine("LOOKING AT:"+ file);
try
{
Assembly dll = Assembly.LoadFrom(file);
Type[] types = dll.GetTypes(); // .Where(x => x.BaseType.Name == "BaseClass");
foreach (Type type in types)
{
if (type.BaseType != null && (type.BaseType.Name == "BaseClass" || type.BaseType.Name == "PeriodicBaseClass"))
{
BaseClass ClassObj = (BaseClass)Activator.CreateInstance(type);
if (objectName == "" || objectsName == ClassObj.Name)
{
availableDrivers.Add(ClassObj);
}
}
}
}
catch (Exception e)
{
Error.Log("Error reading driver:" + e.Message,MessageType.Error);
//Console.WriteLine("Error reading driver:" + e.Message);
}
}
}
return availableDrivers;
}
So as you can see, when I run the program itself the drivers get loaded by retrieving their compiled dll files and when putting breakpoints in the dll code I get the message saying symbols cannot be loaded. I have tried checking Debug>Windows>Modules but the dlls don't show up there due to not being loaded directly in the application.
If the dlls in question are debug versions (that is, they were compiled for debugging) and their current .pdb files are in the same directory, you should be able to step through them just as if they were in your own project.
The other alternative is to open the solution that builds these dll and debug it by attaching to the process of the program you are running.
https://msdn.microsoft.com/en-us/library/3s68z0b3.aspx
So I ended up solving this problem. The way I did this was create a small console application in the solution that runs the same methods as the main application but right from the projects in the solution instead of loading the dlls dynamically.
I then set the startup project to the console application and was able to step through the relevant files properly.

Swap out DLL while in use

I am building a plugin-type system with each plugin represented as a DLL. I would like to be able to reload them without stopping the main application. This means that they must be loaded at runtime, without pre-built links between them (do a filesearch for dlls and load them). I have this set up using Assembly.LoadFile(filename), however, when I try to use File.Copy to replace the DLL it throws an exception, saying something akin to 'file in use'. I have tried using AppDomain, loading all plugins through this secondary domain, and unloading it before reloading, but this throws the same exception.
My current code:
if (pluginAppDomain != null)
AppDomain.Unload(pluginAppDomain);
foreach (string s in Directory.GetFiles(path_to_new_DLLs))
{
string name = s.Substring(s.LastIndexOf('\\') + 1);
Console.WriteLine("Copying " + name);
File.Copy(s, Path.Combine(current_directory, name), true); // Throws exception here
}
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = Environment.CurrentDirectory;
setup.ShadowCopyFiles = "true";
// I think this is where the problem is, maybe I'm forgetting to set something
pluginAppDomain = AppDomain.CreateDomain("KhybotPlugin", null, setup);
foreach (String s in Directory.GetFiles(Environment.CurrentDirectory, "*.dll"))
{
int pos = s.LastIndexOf('\\') + 1;
Assembly dll = pluginAppDomain.Load(s.Substring(pos, s.Length - pos - 4));
// Elided... Load types from DLL, etc, etc
}
Generally you need to unload the AppDomain for the communication.
If you want to prevent the mentioned error you simply can load your dll by using Assembly.Load(Byte[]).
You can also use the Managed Extensibility Framework which will save you a lot of work.
Assembly.Load issue solution
Assembly loaded using Assembly.LoadFrom() on remote machine causes SecurityException
Loading plugin DLLs into another AppDomain is the only solution - so you are on the right path. Watch out for leaking object from second app domain to main one. You need to have all communication with plugins to be happening in plugin's AppDomain.
I.e. returning plugin's object to main code will likely leak plugin's Assembly usage to main AppDomain.
Start with very simple code completely in plugin's AppDomain like "load assembly and create class, but don't return anything to main Domain". Than expand usage when you get more understanding on communication between AppDomains.
Note: unless you doing it for educational purposes using existing system (i.e. MEF) is better.
You could do something like this ...
if (pluginAppDomain != null)
{
AppDomain.Unload(pluginAppDomain);
}
//for each plugin
pluginAppDomain = AppDomain.CreateDomain("Plugins Domain");
x = pluginAppDomain.CreateInstanceFromAndUnwrap("Plugin1.dll", "Namespace.Type");
You should not reference the plugins in your main app directly. Put them in separate project/s and reference them through an interface.

GetExportedTypes() FileNotFoundException: Assembly couldn't be found

My task: Find all Forms (WindowsForm or WPF, doesn't matter) in a dll or exe file and return that list. In theory that works (meaning: if I've an assembly with a WPF or WindowsForm my code manages to get all Forms, TextBoxes, Labels etc. ). When it comes to "real" assemblies it fails. I get FileNotFound exceptions when calling GetExportedTypes() for every "custom" assembly (.NET assemblies are found, no problems there). I already use GetReferencedAssemblies() to load the referenced assemblies (Reflection.Assembly.LoadFrom) and yes it does work (all assemblies are found and loaded into the AppDomain) but it doesn't help.
I checked the version numbers (they match), I copied my executable and the assembly into one directory with all referenced assemblies, doesn't work.
Here is my code, maybe someone figures out what I'm (obviously) doing wrong:
foreach (AssemblyName reference in selectedAssembly.GetReferencedAssemblies())
{
if (System.IO.File.Exists(
System.IO.Path.GetDirectoryName(selectedAssembly.Location) +
#"\" + reference.Name + ".dll"))
{
System.Reflection.Assembly.LoadFrom(
System.IO.Path.GetDirectoryName(selectedAssembly.Location) +
#"\" + reference.Name + ".dll");
}
else if (System.IO.File.Exists(#"C:\dll\" + reference.Name + ".dll"))
{
System.Reflection.Assembly.LoadFrom(#"C:\dll\" + reference.Name + ".dll");
}
else
{
System.Reflection.Assembly.ReflectionOnlyLoad(reference.FullName);
}
selectedAssembly.GetExportedTypes();
}
at first check if the referenced dll exists in the directory where the assembly is, if not check if it exists in C:\dll and if it's not there try and use the GAC. it does work and I've no errors from there but as soon as I come to GetExportedTypes it fails with a FileNotFound exception on the first custom library.
*edit 1 what do I mean by "real assemblies": I mean assemblies which are more complex and have references to non-standard-.NET libraries/assemblies
Thanks for the hint to fuslogvw.exe Hans Passant but what do you mean by "with code like this"?
okay I used fuslogvw.exe and I get two exceptions for every single dll that is referenced by the "selectedAssembly".
The first one says something like
"The binding starts in LoadFrom-context
The image owned by the system isn't searched in LoadFrom-Context"
the other logentry says that the dll referenced by the selectedAssembly couldn't be found and it tried to download it from application's base path and all directories below...but not from it's actual location...so, key question: how do I change the Load-context to LoadFrom? And why is .NET so stubborn on this? I mean the assemblies are loaded in the AppDomain, it shouldn't care about the actual location of the assembly.
okay problem solved. Here is the solution:
http://ayende.com/blog/1376/solving-the-assembly-load-context-problem
I implemented that into my existing class (removed the static-keyword and put the body of the Init method into my method), compiled it and it worked.
Thanks for your help guys.
okay problem solved. Here is the solution: http://ayende.com/blog/1376/solving-the-assembly-load-context-problem
I implemented that into my existing class (removed the static-keyword and put the body of the Init method into my method), compiled it and it worked.
Thanks for your help guys.
just in case the website will someday be unavailable, here is the sourcecode from ayende
static Dictionary<string, Assembly>assemblies;
public static void Init()
{
assemblies = new Dictionary<string, Assembly>();
AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(CurrentDomain_AssemblyLoad);
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly assembly = null;
assemblies.TryGetValue(args.Name, out assembly);
return assembly;
}
static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
Assembly assembly = args.LoadedAssembly;
assemblies[assembly.FullName] = assembly;
}
I would recommend using Reflector to see which references you may not have loaded. For instance, you are only loading the referenced assemblies that the current assembly is looking at. Do you step down through each child to find their referenced assemblies as well? The FileNotFound error is probably pointing you in the direction of a type that is declared in another assembly that isn't loaded.

My app domain won't unload

At runtime, I'd like to be able to unload a DLL and reload a modified version of it. My first experiment went down in flames. Can anyone tell me why?
private static void Main()
{
const string fullPath = "C:\\Projects\\AppDomains\\distrib\\MyLibrary.dll";
// Starting out with a version of MyLibrary.dll which only has 1 method, named Foo()
AssemblyName assemblyName = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.Load(assemblyName);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);
// Breakpoint here; swap out different version of MyLibrary.dll which only has 1 method, named Goo()
AssemblyName assemblyName2 = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
Assembly asm2 = appDomain2.Load(assemblyName2);
foreach (Type type in asm2.GetExportedTypes())
{
foreach (MemberInfo memberInfo in type.GetMembers())
{
string name = memberInfo.Name;
// Breakpoint here: Found Foo and but no Goo! I was expecting Goo and no Foo.
}
}
}
private static void appDomain_DomainUnload(object sender, EventArgs e)
{
// This gets called before the first breakpoint
}
Edit:
Okay, this is obviously my first time posting. Thanks Daniel for formatting my code (I now see the toolbar button to do that, and the preview pane, too!). I don't see a way to post a "comment" in reply to either those who asked for clarification to the original post or to the 1 answer, so I'll just post another "answer" to keep the conversation going. (Pointers appreciated on how comments are done would be appreciated).
Comments to the posting:
Mitch - Went down in flames 'cause my foreach loop should have been iterating over the types in the modified DLL, not the previously loaded/unloaded one.
Matthew - That might work, but I really need the case of the same file name to work.
Mike Two - Not strongly named.
Comments to the answer:
Blue & Mike Two - I'll noodle on your suggestions, but first I need to understand a critical aspect. I had read that you have to take care not to pull the assembly into the main app domain, and the code previously had a copy of the foreach loop before the unload. So, suspecting that accessing MethodInfos was sucking the assembly into the main app domain, I removed the loop. That's when I knew I needed to ask for help, 'cause the first DLL still wouldn't unload!
So my question is: What in the following code segment causes the main app domain to ever directly (or indirectly) access anything in the dll...why would it cause the assembly to also load in the main app domain:
appDomain.Load(assemblyName);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);
Didn't give me much confidence I'd ever actually be able to use the DLL before unloading it.
Edit 2:
Mike Two - Thanks for your persistence...loading the assembly from within DoCallBack was the secret sauce. For anyone interested, here is some more info that might be useful down the road:
Sounds like no one could actually reproduce my conditions exactly. To demonstrate the original problem, I generated my dlls this way: 1. Added class library project to solution 2. Built version 1.0.0 with Foo; renamed resulting assembly as MyLibrary.dll.f. 3. Renamed Foo to Goo and built another version 1.0.0; renamed resulting assembly as MyLibrary.dll.g. 4. Removed project from solution. Before starting the run, I removed the .f and ran to breakpoint (first line after the unload). Then I tacked the .f back on and removed the .g from the other dll and ran to the next breakpoint. Windows didn't stop me from renaming. Note: Although it would be better practice, I didn't change the version number because I didn't want to assume my customers always would, since the default entry in AssemblyInfo isn't the wildcard version. It seemed like the uglier case to handle.
Also, I just now discovered something that would have clued me in sooner:
AssemblyName assemblyName = AssemblyName.GetAssemblyName(FullPath);
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.Load(assemblyName);
Assembly[] tempAssemblies = appDomain.GetAssemblies();
// MyLibrary.dll has been loaded into the temp domain...good
Assembly[] mainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// MyLibrary.dll has been loaded into the main domain, too...bad!
So I'm not sure what the point of AppDomain.Load is, but it seems to have the bonus side effect of loading the assembly into the main app domain as well. Using this experiment, I could see Mike Two's solution cleanly loads it only into the temp domain:
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.DoCallBack(CallBackDelegate); // Executes Assembly.LoadFrom
Assembly[] tempAssemblies = appDomain.GetAssemblies();
// MyLibrary.dll has been loaded into the temp domain...good
Assembly[] mainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// MyLibrary.dll has NOT been loaded into the main domain...great!
So, Mike Two, exactly how does this StackOverflow newbie mark your answer as "accepted"? Or can I not do that since I'm only a guest here?
Now I'm off to learn how to actually use MyLibrary without sucking the assembly into the main app domain. Thanks everyone for participating.
Edit 3:
BlueMonkMN - I went ahead and took out the event subscription and get the same result. The full program is now listed below:
const string fullPath = "C:\\Projects\\AppDomains\\distrib\\MyLibrary.dll";
// Starting out with a version of MyLibrary.dll which only has 1 method, named Foo()
AssemblyName assemblyName = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.Load(assemblyName);
AppDomain.Unload(appDomain);
// Breakpoint here; swap out different version of MyLibrary.dll which only has 1 method, named Goo()
AssemblyName assemblyName2 = AssemblyName.GetAssemblyName(fullPath);
AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
Assembly asm2 = appDomain2.Load(assemblyName2);
foreach (Type type in asm2.GetExportedTypes())
{
foreach (MemberInfo memberInfo in type.GetMembers())
{
string name = memberInfo.Name;
// Breakpoint here: Found Foo and but no Goo! I was expecting Goo and no Foo.
}
}
Seems like there should be no way the assembly is getting pulled into the main app domain between these 2 lines:
appDomain.Load(assemblyName);
AppDomain.Unload(appDomain);
I tried to replicate this. In the dll to load (MyLibrary.dll) I built two versions. The first had one class with one method named Foo and had a version number of 1.0.0.0. The second had the same class but the method had been renamed Bar (I'm a traditionalist) and a version number of 2.0.0.0.
I put a breakpoint after the unload call. Then I tried to copy the second version on top of the first version. I assume that is what you are doing because the path never changes. Windows would not let me copy version 2 over version 1. The dll was locked.
I changed the code to load the dll using code executed inside the AppDomain by using DoCallback. That worked. I could swap the dll's and find the new method. Here is the code.
class Program
{
static void Main(string[] args)
{
AppDomain appDomain = AppDomain.CreateDomain("MyTemp");
appDomain.DoCallBack(loadAssembly);
appDomain.DomainUnload += appDomain_DomainUnload;
AppDomain.Unload(appDomain);
AppDomain appDomain2 = AppDomain.CreateDomain("MyTemp2");
appDomain2.DoCallBack(loadAssembly);
}
private static void loadAssembly()
{
string fullPath = "LoadMe1.dll";
var assembly = Assembly.LoadFrom(fullPath);
foreach (Type type in assembly.GetExportedTypes())
{
foreach (MemberInfo memberInfo in type.GetMembers())
{
string name = memberInfo.Name;
Console.Out.WriteLine("name = {0}", name);
}
}
}
private static void appDomain_DomainUnload(object sender, EventArgs e)
{
Console.Out.WriteLine("unloaded");
}
}
I did not strong name the assemblies. If you do you are likely to find the first one cached. You can tell by running gacutil /ldl (List download cache) from the command line. If you do find it cached run gacutil /cdl to clear the download cache.
I have found that if you ever directly access types in an assembly it gets loaded into your own domain. So what I have had to do is create a third assembly that implements interfaces common to both assemblies. That assembly gets loaded into both domains. Then just be careful to only use interfaces from that third assembly when interacting with the external assembly. That should allow you to unload the second assembly by unloading its domain.

Categories