简体   繁体   中英

Delete dll file after unloading an AppDomain

I'm currently trying to make a game engine, trying to replicate piece-by-piece unity functions, for loading some scripts, i have no problem, but when i have to reload them, the compilation with mono fail, telling me DLL is already accessed, or the DLL file can't be deleted.

Here is my code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Diagnostics;
using System.Security.Policy;
using System.Security;
using System.Security.Permissions;

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            byte[] response = new System.Net.WebClient().DownloadData(assemblyPath);
            return Assembly.ReflectionOnlyLoad(response);
        }
        catch (Exception)
        {
            return null;
        }
    }

    public Assembly GetAssembly2(string assemblyPath, AppDomain domain)
    {
        try
        {
            byte[] bytesDLL = new System.Net.WebClient().DownloadData(assemblyPath);
            return domain.Load(bytesDLL);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }

    public Assembly GetAssemblyByName(AssemblyName name, AppDomain domain)
    {
        return domain.ReflectionOnlyGetAssemblies().
        SingleOrDefault(assembly => assembly.GetName() == name);
    }
}

class Program
{
    public static AppDomain domain;
    public static Assembly assembly;
    public static Type type;
    public static String dllPath;
    public static String scriptPath;
    public static String className;
    public static String file;
    public static dynamic instance;

    private static bool Compile(String path, out String dir)
    {       
        ProcessStartInfo start = new ProcessStartInfo();
        dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        dir = Path.Combine(dir, Path.GetFileNameWithoutExtension(path) + ".dll");

        if (File.Exists(dir))
        {
            Console.WriteLine("???????");
            File.Delete(dir);
            Console.WriteLine("???????2");
        }

        start.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Mono\\lib\\mono\\4.5\\mcs.exe");
        start.UseShellExecute = false;
        start.RedirectStandardError = true;
        start.RedirectStandardOutput = true;
        start.Arguments = "\"" + path + "\" " + "/target:library" + " " + "/out:" + "\"" + dir + "\""; //+ " " + "/reference:OctogonEngine.dll" + " /reference:AssimpNet.dll";

        using (Process process = Process.Start(start))
        {
            using (StreamReader reader = process.StandardError)
            {
                string result = reader.ReadToEnd();
                Console.WriteLine(result);
            }

            using (StreamReader reader = process.StandardOutput)
            {
                string result = reader.ReadToEnd();
                Console.WriteLine(result);
            }
        }

        Console.WriteLine("compilation ok");
        return (true);
    }

    public static void Unload()
    {
        FileStream[] streams = null;

        if (assembly != null)
            streams = assembly.GetFiles();

        instance = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        type = null;
        assembly = null;
        AppDomain.Unload(domain);
        assembly = null;

        if (streams != null)
        {
            for (int i = 0; i < streams.Length; i++)
            {
                streams[i].Dispose();
            }
        }

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Directory.Delete(cachePath, true);
        return;
    }

    static Assembly GetAssemblyByName(string name, AppDomain domain)
    {
        return domain.GetAssemblies().
               SingleOrDefault(assembly => assembly.GetName().Name == name);
    }

    public static String cachePath = "./cache/";

    public static void Load()
    {
        Directory.CreateDirectory(cachePath);

        if (Compile(scriptPath, out Program.dllPath))
        {
            if (File.Exists(Program.dllPath))
            {
                className = Path.GetFileNameWithoutExtension(Program.dllPath);

                AppDomainSetup setup = new AppDomainSetup();
                setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
                setup.ShadowCopyFiles = "true";
                setup.CachePath = cachePath;
                domain = AppDomain.CreateDomain(className, null, setup);
                domain.DoCallBack(() => AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName("test.dll")));
                var assemblyLoader = (Proxy)domain.CreateInstanceAndUnwrap(typeof(Proxy).Assembly.FullName, typeof(Proxy).FullName);
                assembly = assemblyLoader.GetAssembly(Program.dllPath);

                /*if (assembly == null)
                {
                    Console.WriteLine("damn");
                }*/

                if (assembly != null)
                {
                    type = assembly.GetType(className);
                }

                if (File.Exists(scriptPath))
                    Program.file = File.ReadAllText(scriptPath);
            }
        }
    }

    static bool check = false;

    static void AppDomainInit(string[] args)
    {
        if (!File.Exists(args[0]))
        {
            return;
        }
    }

    public static void init(String scriptPath)
    {
        if (File.Exists(scriptPath))
        {
            Program.file = File.ReadAllText(scriptPath);
            Program.scriptPath = scriptPath;
            Program.Load();
        }
    }

    static void Main(string[] args)
    {
        Program.init(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "test.cs"));
        Program.Unload();
        //here is the crash :/
        File.Delete(Program.dllPath);
        Console.WriteLine("???");
    }
}

(To test, you may need to copy mono in the executing directory, wich can be found at : http://www.mono-project.com/download/ )

Does anybody have a clue on what i can do to either, force delete that dll file, or to make that file accessible for deletion?

If not, does somebody have a clue on the way unity load and reload scripts, how to make it the good way?

My sollution, in case someone need it, was to compile with a CSharpCodeProvider , whith this parameters:

CompilerParameters compilerParams = new CompilerParameters
{
GenerateInMemory = true,
GenerateExecutable = false,
IncludeDebugInformation = true
};

then i don't even need to build a dll from it, the result is an usable assembly.

if you really need to load and then delete an assembly at runtime, you still can do it by using

byte[] bytes = File.ReadAllBytes("pathtoyourdll");
AppDomain.CurrentDomain.Load(bytes);

you can then delete the dll, but it takes some memory.

So I crafted this example which works fine for me

project consoleapplication1.exe

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                String pathToAssembly = args[0];
                AppDomain dom = AppDomain.CreateDomain("some");
                AssemblyName assemblyName = new AssemblyName();
                assemblyName.CodeBase = "loader.dll";
                dom.Load(assemblyName);
                object loader = dom.CreateInstanceAndUnwrap("loader", "loader.AsmLoader");
                Type loaderType = loader.GetType();
                loaderType.GetMethod("LoadAssembly").Invoke(loader, new object[] { pathToAssembly });                
                //make sure the given assembly is not loaded in the main app domain and thus would be locked
                AppDomain.CurrentDomain.GetAssemblies().All(a => { Console.WriteLine(a.FullName); return true; });
                AppDomain.Unload(dom);
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                File.Delete(pathToAssembly);
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

class library loader.dll:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace loader
{
    public class AsmLoader: MarshalByRefObject
    {
        public AsmLoader()
        {
        }

        public void LoadAssembly(string path)
        {
            AssemblyName n = new AssemblyName();
            n.CodeBase = path;
            AppDomain.CurrentDomain.Load(n);
        }
    }
}

class library testasm.dll

... whatever code....

put all 3 files in the same folder, open up a cmd line in that folder and issue the command:

consoleapplication1.exe testasm.dll

It will load the loader.dll into the main app domain and the remote appdomain create a marshalled proxy from the AsmLoader object, load the testasm.dll into the remote domain via the marshalled AsmLoader.LoadAssembly invocation. The consoleapplication1.exe then outputs all assemblies loaded in the current appdomain to console to see that the testasm.dll is not loaded in it. Unloads the remote appdomain, deletes the testasm.dll just fine.

In my case the solution was to call after this line of code:

AppDomain.Unload(domain);

also this:

domain = null;

It appears that unloading from the AppDomain is not enough, because the domain object still holds reference to the file that was loaded/unloaded. After I set it to null, I was able to use File.Delete(unloadedAssemblyPath) without other GC workarounds.

@Michal:

I tried many other ways too,

my head is burning, but i tried whats on your link, and i think i also tried to understand and use every little example i found on google :/

but here i have tried again:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Diagnostics;
using System.Security.Policy;
using System.Security;
using System.Security.Permissions;
using System.Configuration;

class Program
{
    public static void Main(string[] args)
    {
        String pathToAssembly = args[0];
        AppDomain dom = AppDomain.CreateDomain("some");     
        AssemblyName assemblyName = new AssemblyName();
        assemblyName.CodeBase = pathToAssembly;
        Assembly assembly = dom.Load(assemblyName);
        AppDomain.Unload(dom);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        File.Delete(pathToAssembly);
    }
}

And i got the same problem, file cannot be accessed for deletion.

i even tried to do like that:

  domain.DoCallBack(() => AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName("test.dll")));

it works, i can delete then, but if after i try to retrieve assembly from domain(by name), then the deletion fail again.

i tried with shadow copy files, MultiDomainHost, and lot of other things (everything that i found on google)

for now my solution is to create another DLL each time a script has been modified, and delete them when the program start, but its not a very reassuring method, if users have a lot of scripts to load, or keep the engine on for some days, its not good. :/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM