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.
Related
I was looking to load a usercontrol from a folder. I want people to be able to package an extension for my app. To do this, they'll have to create a c# user control and put the designer, code, and resx file into a folder. Then when they want to use their "extension", they'll select a folder from a folder selector (I have that) and my app will load their extension. I want to specifically pull out the user control and cast it into a usercontrol object. Am I able to do this and if so, how?
I looked around the internet and there doesn't really seem to be any close to this question. I wasn't able to create any script that could accomplish this. I don't even know where to start in this one. I know that I have to compile their usercontrol.
If this is not possible, the next best solution I can think of is maybe a precompiled usercontrol. If this is possible, how would I load that?
Any help would be appreciated. Thanks!
If you wish to compile the sources, this can be done using the System.CodeDom. Other than that you should load the types from the assembly and test to see if there is a UserControl in there, load it up and add it to your form.
Here is some example of what i described:
public void LoadPlugin(params string[] sourceCodeFilesForUserControl)
{
// Compile the source files
CSharpCodeProvider codeProvider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters();
parameters.IncludeDebugInformation = true;
parameters.GenerateInMemory = true;
// Add references that they can use
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll"); // important for UserControl
parameters.TreatWarningsAsErrors = false;
CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, sourceCodeFilesForUserControl);
if (results.Errors.Count > 0)
{
// Handle compile errors
StringBuilder sb = new StringBuilder();
foreach (CompilerError CompErr in results.Errors)
{
sb.AppendLine("Line number " + CompErr.Line +
", Error Number: " + CompErr.ErrorNumber +
", '" + CompErr.ErrorText + ";");
}
Console.Write(sb.ToString());
}
else
{
// The assembly we can search for a usercontrol
var assembly = results.CompiledAssembly;
// If the assembly was already compiled you might want to load it directly:
// assembly = Assembly.LoadFile(#"C:\Program Files\MyTool\plugins\someplugin.dll");
// Get the first type in the assembly that is a UserControl
var userControl = assembly.GetTypes().FirstOrDefault(x => x.BaseType == typeof(UserControl));
// Create a instance of the UserControl
var createdUserControl = Activator.CreateInstance(userControl, new object[] { }) as UserControl;
// Add the created UserControl to the current form
this.Controls.Add(createdUserControl);
}
}
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
}
}
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.
In the following sample app I create a new AppDomain and I execute it with shadow copy enabled. From the new AppDomain I then try to delete (replace) the original main exe. However I get an "access is denied error". Interestingly, after launching the program, from Windows Explorer it is possible to rename the main exe (but not to delete it).
Can shadow copy work for runtime overwriting of the main exe?
static void Main(string[] args)
{
// enable comments if you wanna try to overwrite the original exe (with a
// copy of itself made in the default AppDomain) instead of deleting it
if (AppDomain.CurrentDomain.IsDefaultAppDomain())
{
Console.WriteLine("I'm the default domain");
System.Reflection.Assembly currentAssembly = System.Reflection.Assembly.GetExecutingAssembly();
string startupPath = currentAssembly.Location;
//if (!File.Exists(startupPath + ".copy"))
// File.Copy(startupPath, startupPath + ".copy");
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = Path.GetFileName(startupPath);
setup.ShadowCopyFiles = "true";
AppDomain domain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup);
domain.SetData("APPPATH", startupPath);
domain.ExecuteAssembly(setup.ApplicationName, args);
return;
}
Console.WriteLine("I'm the created domain");
Console.WriteLine("Replacing main exe. Press any key to continue");
Console.ReadLine();
string mainExePath = (string)AppDomain.CurrentDomain.GetData("APPPATH");
//string copyPath = mainExePath + ".copy";
try
{
File.Delete(mainExePath );
//File.Copy(copyPath, mainExePath );
}
catch (Exception ex)
{
Console.WriteLine("Error! " + ex.Message);
Console.ReadLine();
return;
}
Console.WriteLine("Succesfull!");
Console.ReadLine();
}
You can achive self updating application within a single application with multiple AppDomains. The trick is move application executable to a temporary directory and copy back to your directory, then load the copied executable in a new AppDomain.
static class Program
{
private const string DELETED_FILES_SUBFOLDER = "__delete";
/// <summary>
/// The main entry point for the application.
/// </summary>
[LoaderOptimization(LoaderOptimization.MultiDomainHost)]
[STAThread]
static int Main()
{
// Check if shadow copying is already enabled
if (AppDomain.CurrentDomain.IsDefaultAppDomain())
{
// Get the startup path.
string assemblyPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
string assemblyDirectory = Path.GetDirectoryName(assemblyPath);
string assemblyFile = Path.GetFileName(assemblyPath);
// Check deleted files folders existance
string deletionDirectory = Path.Combine(assemblyDirectory, DELETED_FILES_SUBFOLDER);
if (Directory.Exists(deletionDirectory))
{
// Delete old files from this folder
foreach (var oldFile in Directory.EnumerateFiles(deletionDirectory, String.Format("{0}_*{1}", Path.GetFileNameWithoutExtension(assemblyFile), Path.GetExtension(assemblyFile))))
{
File.Delete(Path.Combine(deletionDirectory, oldFile));
}
}
else
{
Directory.CreateDirectory(deletionDirectory);
}
// Move the current assembly to the deletion folder.
string movedFileName = String.Format("{0}_{1:yyyyMMddHHmmss}{2}", Path.GetFileNameWithoutExtension(assemblyFile), DateTime.Now, Path.GetExtension(assemblyFile));
string movedFilePath = Path.Combine(assemblyDirectory, DELETED_FILES_SUBFOLDER, movedFileName);
File.Move(assemblyPath, movedFilePath);
// Copy the file back
File.Copy(movedFilePath, assemblyPath);
bool reload = true;
while (reload)
{
// Create the setup for the new domain
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = assemblyFile;
setup.ShadowCopyFiles = true.ToString().ToLowerInvariant();
// Create an application domain. Run
AppDomain domain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup);
// Start application by executing the assembly.
int exitCode = domain.ExecuteAssembly(setup.ApplicationName);
reload = !(exitCode == 0);
AppDomain.Unload(domain);
}
return 2;
}
else
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
MainForm mainForm = new MainForm();
Application.Run(mainForm);
return mainForm.ExitCode;
}
}
}
As it's an interesting use case of MEF, I've bashed out a quick demo of how to hot-swap running code in C#. This is very simple and leaves out a lot of edge cases.
https://github.com/i-e-b/MefExperiments
Notable classes:
src/PluginWatcher/PluginWatcher.cs -- watches a folder for new implementations of a contract
src/HotSwap.Contracts/IHotSwap.cs -- minimal base contract for a hot-swap
src/HotSwapDemo.App/Program.cs -- Does the live code swap
This doesn't lock the task .dlls in the Plugins folder, so you can delete old versions once new ones are deployed.
Hope that helps.
You asked specifically to use ShadowCopy for the update process. If that (and why would it be?) not a fixed requirement, these ones were real eye openers for me:
https://visualstudiomagazine.com/articles/2017/12/15/replace-running-app.aspx
https://www.codeproject.com/Articles/731954/Simple-Auto-Update-Let-your-application-update-i
It comes down to you renaming the target file (which is allowed, even when it is locked since it is running) and then moving/copying the desired file to the now freed destination.
The vs-magazine article is very detailed, including some nifty tricks like finding out if a file is in use by current application (though only for exe, for .dlls and others one has to come up with a solution).
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.