I'm working on a sort of plugin system for a Discord bot. Right now i'm on macOS using Mono as my runtime and I've been looking at using seperate AppDomains to load and manage each of the plugins. The plugins themselves do have dependencies but these are already loaded in the main appdomain. So here lies my problem:
I've attempted to iterate over the loaded assemblies in the current domain and load them into my separate AppDomain so the plugin's dependencies are already loaded in. This results in a FileNotFoundException when trying to load said dependencies (even though I point them to the correct path in the exe directory).
I've tried without doing that and just going through and loading the plugin dll's into a separate AppDomain and seeing if it'll resolve automatically. Still throws a FileNotFoundException.
I tried manually creating an array in order of the dependendies (in my application's case, Newtonsoft.Json -> DSharpPlus -> Bot.CommandManager). I can't even load Newtonsoft.Json due to a missing dependency.
Now, I'm loading the raw bytes into a stream reader and passing that into AppDomain.Load to make sure that the FileNotFoundException comes from a lack of dependency and not the actual file being missing. The file is there, still throws FileNotFoundException on domain.Load
The only way I've been able to successfully load the plugins is just using Assembly.Load and not worrying about AppDomains which works great until I update something and want to unload/reload the plugin. Which is where I need to use AppDomain. I'm kind of lost, here's my current iteration of the code. It's probably extremely unnecessary to have a separate AppDomain for each plugin but I'm rolling with that for now.
private void SetupAppDomain()
{
string path = Directory.GetCurrentDirectory() + "/modules/";
Console.WriteLine(path);
List<string> installedModules = new List<string>();
string[] files = Directory.GetFiles(path);
foreach(string modulePath in files)
{
try
{
AppDomain domain = AppDomain.CreateDomain(Path.GetFileName(modulePath), AppDomain.CurrentDomain.Evidence);
StreamReader reader = new StreamReader(modulePath, System.Text.Encoding.GetEncoding(1252), false);
byte[] b = new byte[reader.BaseStream.Length];
reader.BaseStream.Read(b, 0, System.Convert.ToInt32(reader.BaseStream.Length));
domain.Load(b); //this throws the FileNotFoundException
Assembly[] a = domain.GetAssemblies();
int index = 0;
for (int i = 0; i < a.Length; i++)
{
if(a[i].GetName().Name + ".dll" == Path.GetFileName(modulePath))
{
index = i;
break;
}
}
installedModules.Add(a[index].GetName().Name);
}
catch(Exception ex)
{
throw ex;
}
}
}
Worst case scenario, I can just go back to the old way of doing it and just have to restart the bot when I want to reload assemblies.
Related
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.
I have a small console application which backs up and then copies new DLLs to an installation folder, and then is supposed to re-register the new DLLs. It can also restore DLLs from a backup and then re-register the backups.
I have the following code to perform the registeration:
Assembly asm = Assembly.LoadFrom(Path.Combine(Path.GetFullPath(options.RemotePath), Path.GetFileName(file)));
RegistrationServices regAsm = new RegistrationServices();
bool bResult = regAsm.RegisterAssembly(asm, AssemblyRegistrationFlags.SetCodeBase);
I'm having a problem with registering where on calling Assembly.LoadFrom I get a System.IO.FileLoadException with the message A procedure imported by 'MyLib.dll' could not be loaded
I thought this was probably a DLL in the remote folder that it was unable to load, so I wrapped my registration code in a change of current directory:
var wd = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(options.RemotePath);
...
Directory.SetCurrentDirectory(wd);
Unfortunately this didn't seem to resolve the issue. I did some more investigating and found that I can tell the AppDomain how to resolve dependencies using AppDomain.CurrentDomain.AssemblyResolve and ResolveEventHandler, and so I set this up by adding this code at the beginning of Main, before my registration code.
var otherCompanyDlls = new DirectoryInfo(options.RemotePath).GetFiles("*.dll");
Console.WriteLine("Setting AssemblyResolve");
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler((sender, a) =>
{
Console.WriteLine("Looking for {0}", a.Name);
var dll = otherCompanyDlls.FirstOrDefault(fi => fi.Name == a.Name);
if (dll == null)
{
return null;
}
return Assembly.LoadFrom(dll.FullName);
});
I still get the same error, and this AssemblyResolve handler code never seems to be triggered since the Console line I added is never written.
What is it that I am doing wrong? I was hoping to at least find out what DLL dependency it was trying to load but I can't even seem to find that.
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.
I am working on some software that will dynamically build menu items for certain dlls so that we can load components in dynamically based on what dlls are available on the users machine. Any dlls that I want to load have been flagged with an Assembly Attribute in the AssemblyInfo.cs file and how I determine whether or not I want to build a menu item for that dll. Here is my method so far:
private void GetReportModules() {
foreach (string fileName in Directory.GetFiles(Directory.GetCurrentDirectory())) {
if (Path.GetExtension(fileName) == ".dll" || Path.GetExtension(fileName) == ".exe") {
System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFrom(fileName);
object[] attributes = assembly.GetCustomAttributes(typeof(ReportNameAttribute), false);
if (attributes.Count() > 0) {
ReportNameAttribute reportNameAttribute = attributes[0] as ReportNameAttribute;
Type type = assembly.GetType(reportNameAttribute.BaseType);
MenuItem customReportsMenuItem = new MenuItem();
customReportsMenuItem.Header = reportNameAttribute.ReportName;
ReportsMenuItem.Items.Add(customReportsMenuItem);
customReportsMenuItem.Click += (s, ev) => {
var obj = Activator.CreateInstance(type);
type.InvokeMember("Show", System.Reflection.BindingFlags.Default | System.Reflection.BindingFlags.InvokeMethod, null, obj, null);
};
}
}
}
}
For the most part its working fine, I am getting the dlls that I am expecting back out and am creating my menu items fine. The problem is that in order to check for the attribute I first need to load the assembly using Reflection. Some of the other local dlls are throwing errors when I try to load them about missing dependencies or he module was expected to contain an assembly manifest. Is there a way I can safely check to see if an assembly CAN be loaded before I actually do so? (sounds stupid as I write it out). Any thoughts on the problem I'm running into or better suggestions for how to accomplish what I'm trying here? Feeling a little bit in over my head.
You can create a separate AppDomain, try to load the assemblies there, send the results back, and unload the AppDomain. This way you do not change your current AppDomain with 'garbage' of any loaded assemblies.
One way would be to make use of a try catch block. If it throw's an exception, you're not interested...
EDIT:
MSDN explains clearly the type of exceptions LoadFrom can throw. FileLoadException looks likely in your case.
I'm sure there is code out there that carried on after a catch. For example a logging framework. I would not want my framework to catch an exception and make my executable stop etc, i'd want it to smother the exception. My application should not fail just because a line of log miss fired.
You can try the Unmanaged Metadata API (http://msdn.microsoft.com/en-us/library/ms404384.aspx) or the Common Compiler Infrastructure Metadata API (http://ccimetadata.codeplex.com/) as alternatives to plain reflection.
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.