3

i am trying to deploy DLLs inside a windows service by importing DLLs from various repositories like google drive/ dropbox/ ftp etc...

But before any new DLL can be instantiated, I would want the previous running instance to be shut down.

I am using tasks and reflection in this.

I am unable to figure out how to cancel the task that instantiate the DLL at run time ( as the instantiated dll is a long running application example file watcher.. )

    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken ct = cts.Token;

            // instantiate the dll though reflection
        t = Task.Factory.StartNew(() =>
        {
            try
            {
                Assembly assembly = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + dllName);
                Type type = assembly.GetType("myclass.Program");

                MethodInfo minfo = type.GetMethod("Main", BindingFlags.Public | BindingFlags.NonPublic |
                          BindingFlags.Static | BindingFlags.Instance);
                minfo.Invoke(Activator.CreateInstance(type), null);

            }
            catch (Exception ex)
            {

                log.Error(ex.ToString());
            }
        }, cts.Token);

Ques : I want to cancel the task t before my appication detects a new dll and tries to execute that through this task code.

EDIT I removed the cancellation token code as it was breaking. Here is the actual code with the cancellation token.

    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken ct = cts.Token;

if (t != null)
        {
            cts.Cancel();
            try
            {
                ct.ThrowIfCancellationRequested();
            }
            catch (Exception ex)
            {

                cts.Dispose();
                t.Dispose();
            }

        }

                // instantiate the dll though reflection
        t = Task.Factory.StartNew(() =>
        {
            try
            {
                Assembly assembly = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + dllName);
                Type type = assembly.GetType("myclass.Program");

                MethodInfo minfo = type.GetMethod("Main", BindingFlags.Public | BindingFlags.NonPublic |
                          BindingFlags.Static | BindingFlags.Instance);
                minfo.Invoke(Activator.CreateInstance(type), null);

            }
            catch (Exception ex)
            {

                log.Error(ex.ToString());
            }
        }, cts.Token);

My idea was that if I could somehow cancel and dispose the task that was holding the instantiation context , the assembly would be released and then i will be able to update the assembly and re-instantiate it through the task again.

i know i am going wrong somewhere, kindly explain.

EDIT

i had high hopes with assemblyDomain.DoCallBack(delegate). But im getting an error. Here is the toned down version of code that throws the bug.

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


namespace AppDomain
{
    [Serializable]
    class Program
    {
        static System.AppDomain assemblyDomain = null;

        static void Main(string[] args)
        {

            var inp = "go";

            while (inp.ToString().ToLower().Trim() != "stop")
            {
                start();
                inp = Console.ReadLine();
            }

        }

        private static void start()
        {


            //Check if appdomain and assembly is already loaded
            if (assemblyDomain != null)
            {
                //unload appDomain and hence the assembly
                System.AppDomain.Unload(assemblyDomain);

                //Code to download new dll
            }

            string cwd = System.AppDomain.CurrentDomain.BaseDirectory;

            string sourceFileName = @"C:\Users\guest\Documents\visual studio 2010\Projects\DotNetTraining\Lecture 1 - dotNetProgramExecution\bin\Debug\Lecture 1 - dotNetProgramExecution.exe";

            string dllName = "Lecture 1 - dotNetProgramExecution.exe";

            // copy the file
            if (File.Exists(cwd + dllName))
            {
                File.Delete(cwd + dllName);
            }

            File.Copy(sourceFileName, cwd + dllName);

            assemblyDomain = System.AppDomain.CreateDomain("assembly1Domain", null);
            assemblyDomain.DoCallBack(() =>
               {
                   var t = Task.Factory.StartNew(() =>
                   {
                       try
                       {

                           string sss = "";
                           Assembly assembly = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + dllName);
                           Type type = assembly.GetType("Lecture_1___dotNetProgramExecution.Program");

                           MethodInfo minfo = type.GetMethod("Main", BindingFlags.Public | BindingFlags.NonPublic |
                                     BindingFlags.Static | BindingFlags.Instance);
                           minfo.Invoke(Activator.CreateInstance(type), null);



                           //        //var pathToDll = @"assembly path";
                           //        //var dllName = "assembly name";
                           //        var assembly = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + dllName);
                           //        var targetAssembly = assembly.CreateInstance("Lecture_1___dotNetProgramExecution.Program");
                           //        Type type = targetAssembly.GetType();
                           //        MethodInfo minfo = type.GetMethod("Main", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
                           //        minfo.Invoke(targetAssembly, null);

                       }
                       catch (Exception ex)
                       {

                           Console.WriteLine(ex.ToString());
                       }
                   });
               });
        }
    }
}


Error :
Type 'AppDomain.Program+<>c__DisplayClass2' in assembly 'AppDomain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

Stack Trace :

at System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)
   at AppDomain.Program.start() in c:\users\guest\documents\visual studio 2010\Projects\DotNetTraining\AppDomain\Program.cs:line 58
   at AppDomain.Program.Main(String[] args) in c:\users\guest\documents\visual studio 2010\Projects\DotNetTraining\AppDomain\Program.cs:line 24
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

Please Note : I have marked the class Program in the assembly im importing as Serializable

namespace Lecture_1___dotNetProgramExecution
{
    [Serializable]
    class Program
    {
        static void Main()
        {

Updated :

code of the dynamically pulled assembly

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

namespace Lecture_1___dotNetProgramExecution
{
    [Serializable]
    class Program
    {
        static void Main()
        {
            try
            {

                Task.Factory.StartNew(() =>
                {

                    while (true)
                    {
                        StringBuilder sb = new StringBuilder();
                        sb.Append("log something new yippe ");
                        // flush every 20 seconds as you do it
                        File.AppendAllText(@"C:\logs.txt", sb.ToString());
                        sb.Clear();
                        Thread.Sleep(3000);
                    }

                });



                FileSystemWatcher fsw = new FileSystemWatcher();

                fsw.Path = @"c:\watched";
                //fsw.filter = ".dll";
                fsw.Created += new FileSystemEventHandler(fsw_Created);
                fsw.BeginInit();
                //throw new FileNotFoundException();
                Console.ReadLine();

            }
            catch (Exception ex)
            {

                Task.Factory.StartNew(() =>
                {

                    while (true)
                    {
                        StringBuilder sb = new StringBuilder();
                        sb.Append("loggind froom exception log something");
                        // flush every 20 seconds as you do it
                        File.AppendAllText(@"C:\logs.txt", sb.ToString());
                        sb.Clear();
                        Thread.Sleep(1000);
                    }

                });
                Console.ReadLine();
            }
        }

        static void fsw_Created(object sender, FileSystemEventArgs e)
        {
            throw new NotImplementedException();
        }


    }
}
ankur
  • 557
  • 1
  • 10
  • 37
  • which is the slow operation `Assembly.LoadFile`? – NeddySpaghetti Aug 01 '15 at 12:52
  • 3
    If you're trying to deploy updated assemblies I think you need to load your assemblies in a separate `AppDomain` to enable them to unloaded. You basically shut down the `AppDomain` to unload the assemblies and create a new one to load the new assemblies. – Enigmativity Aug 01 '15 at 13:00
  • Why don't you call `cts.Cancel()`? – NeddySpaghetti Aug 01 '15 at 13:02
  • cts.cancel is not working. – ankur Aug 01 '15 at 13:11
  • @Enigmativity : i dont want to do that. i want to remain in the context of my application and delete the old taks - > followed by deleting the old assembly - > followed by bringing in the newer assembly (by downloading/copying to my local bin) - > again firing the task code so that now the newer assembly is instantiate and the older context is destroyed. – ankur Aug 01 '15 at 13:14
  • The basic issue here is that minfo.Invoke() never returns. So your task keeps running forever. And is not checking the cancellation token, the Main() method you call knows beans about it. So doesn't know when to stop. Starting another thread to call Main() solves the immediate problem, but not the real problem, you still have code running that doesn't know when it needs to stop. Throw away what you have and re-think your approach, the way you are doing it now isn't going to get you anywhere. – Hans Passant Aug 01 '15 at 13:46
  • @ankur - Once an assembly has been loaded you don't expect that the framework keeps checking if the DLL has been deleted/modified? If the framework did that every time it tried to instantiate an object it would be extremely slow. I think you need to use app domains. – Enigmativity Aug 02 '15 at 00:42
  • @ankur - I just read you updated question. There is no concept of an instance holding an assembly. The app domain does. Once loaded the assembly stays loaded regardless of what the objects are doing. – Enigmativity Aug 02 '15 at 00:45
  • yes. my servrice/framework would check daily that if any updates are available and if there are, i would want to delete the old assembly and bring in the new one. – ankur Aug 02 '15 at 06:42
  • as you said that there is no concept of an instance holding and assembly. would it mean that even if the instantiated code that task t has created returns and exits completely, still i wont be able to delete the assembly? – ankur Aug 02 '15 at 06:45
  • You are making wrong class Serializable. You need to make "Lecture_1___dotNetProgramExecution.Program" class Serializable. Oh! I see you made the "Lecture_1___dotNetProgramExecution.Program", I got the same error too and it get resolved by making that class Serializable. I have running code with me, do you want me to send that code for your reference. – Deepak Bhatia Aug 03 '15 at 14:28
  • Is there any class with the name "DisplayClass2" in your "Lecture_1___dotNetProgramExecution" code? Can you share code of "Lecture_1___dotNetProgramExecution"? – Deepak Bhatia Aug 03 '15 at 14:34
  • let me update the post – ankur Aug 03 '15 at 14:40
  • i have updated the post. Actually i made both the classes serializable, and before i used the " assemblyDomain.DoCallBack(() =>" method, the serialize error wasnt coming. – ankur Aug 03 '15 at 14:45
  • actually i dont have this DisplayClass2 anywhere in my code. this error came after i used the DoCallBack expression. – ankur Aug 03 '15 at 14:50

1 Answers1

4

From your question it appears that you want to unload the dynamically loaded assembly if any upgrade is available, then reload the latest assembly. The cancellation taken will no help in this case. In fact I don't see you are using cancellation token anywhere.

The only way to unload a dynamically loaded assembly is to fist load the the assembly in separate app domain then unload the app domain itself if assembly is no more needed. So you should be doing as follow:

  1. Create a new app domain. Keep the reference of app domain, you will need it later to unload the domain and hence assembly.
  2. Load the assembly in newly created app domain.
  3. Create an instance of type from newly loaded assembly as needed and execute its method.
  4. When a new version of dll is available, unload the previously created app domain. This will automatically unload the assembly too.
  5. Download the new assembly and start from step 1 again.

See this for how to load/unload app domain and assembly in it: Using AppDomain in C# to dynamically load and unload dll

EDIT: Below is the code snippet with AppDomain.DoCallback

using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;


namespace AppDomain
{
[Serializable]
class Program
{
    static System.AppDomain assemblyDomain = null;

    static void Main(string[] args)
    {

        var inp = "go";

        while (inp.ToString().ToLower().Trim() != "stop")
        {
            start();
            inp = Console.ReadLine();
        }

    }

    private static void start()
    {


        //Check if appdomain and assembly is already loaded
        if (assemblyDomain != null)
        {
            //unload appDomain and hence the assembly
            System.AppDomain.Unload(assemblyDomain);

            //Code to download new dll
        }

        string cwd = System.AppDomain.CurrentDomain.BaseDirectory;

        string sourceFileName = @"C:\Users\deepak\Documents\visual studio 2010\Projects\ConsoleApplication1\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe";


        string dllName = "ConsoleApplication2.exe";

        // copy the file
        if (File.Exists(cwd + dllName))
        {
            File.Delete(cwd + dllName);
        }

        File.Copy(sourceFileName, cwd + dllName);

        assemblyDomain = System.AppDomain.CreateDomain("assembly1Domain", null);
        assemblyDomain.DoCallBack(() =>
        {
            var t = Task.Factory.StartNew(() =>
            {
                try
                {

                    string sss = "";
                    string dllName1 = "ConsoleApplication2.exe";
                    Assembly assembly = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + dllName1);
                    Type type = assembly.GetType("Lecture_1___dotNetProgramExecution.Program");

                    MethodInfo minfo = type.GetMethod("Main", BindingFlags.Public | BindingFlags.NonPublic |
                              BindingFlags.Static | BindingFlags.Instance);
                    minfo.Invoke(Activator.CreateInstance(type), null);
                }
                catch (Exception ex)
                {

                    Console.WriteLine(ex.ToString());
                }
            });
        });
    }
}
}


using System;
using System.Text;
using System.Threading;

namespace Lecture_1___dotNetProgramExecution
{
[Serializable]
class Program
{
    static void Main()
    {
        while (true)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("log something new yippe ");
            // flush every 20 seconds as you do it
            //File.AppendAllText(@"C:\logs.txt", sb.ToString());
            Console.WriteLine(sb.ToString());
            sb.Clear();
            Thread.Sleep(3000);
        }
    }
}
}
Community
  • 1
  • 1
Deepak Bhatia
  • 1,090
  • 1
  • 8
  • 13
  • i get what you are trying to say, but i think my understanding was a little vague and it still is . i have updated the question. kindly have a look. i will look into app domain link – ankur Aug 01 '15 at 19:46
  • You don't need cancellation token at all. I will edit my post with the code once I will have access to my laptop. – Deepak Bhatia Aug 02 '15 at 05:17
  • i did the exact same thing after i read about the feature of loading assmblies into a separate app domain altogether. but when after few minutes my above finds out that there is a newer version available... the above code gets fired again - > i successfully unload the app domain - > then when i try to delete the older dll from my current bin, it throws an error saying the assembly is "Access to the path 'mydll.exe' is denied" – ankur Aug 02 '15 at 12:44
  • I am able to figure out this issue. The assembly was loading fine in a separate domain but type instance was being created by the code that was running in current domain context, and that load the assembly in current domain as well. AppDomain.DoCallBack did the trick. See updated code in my above post. – Deepak Bhatia Aug 03 '15 at 02:55
  • i have added the code for the dll that was being imported dynamically. Please have a look. – ankur Aug 04 '15 at 10:25
  • I will try this my tonight and get back to you. – Deepak Bhatia Aug 04 '15 at 13:22
  • I am able to figure out the issue. The issue is that you are sharing a sting variable between two appdomain and because of this .net framework is creating a temp class to make that variable accessible in both the domains (Closure concept). That temp class ('AppDomain.Program+<>c__DisplayClass2' ) is not serializable and hence you are getting the error. See the updated code in my above answer where "dllName" variable is now not shared. Also your "Lecture_1___dotNetProgramExecution" is to complex, find the simplified version above. – Deepak Bhatia Aug 05 '15 at 03:31
  • thanks a tonn.. how were you able to find out that its cause by a class that was used to implement the closure. – ankur Aug 05 '15 at 09:02
  • You are welcome. Class names like "AppDomain.Program+<>c__DisplayClass2'" are generated by framework for closure or anonymous methods. – Deepak Bhatia Aug 05 '15 at 13:59
  • could you also please advice me on how should i proceed if i want to share data between two app domains. – ankur Aug 07 '15 at 12:18
  • Let me know your specific requirement, for example you just want to send data from default domain to new domain or their is some other requirement? – Deepak Bhatia Aug 07 '15 at 13:03
  • ok. i want to keep the main domain alive, always. i cannot afford to shut it down and then update the dll's and then restart the application (main app domain). i call these dll's that im deploying thru tasks/threads "modules". these modules have to share the context with the main app domain and hence data sharing(something like closure/static) is required. – ankur Aug 07 '15 at 13:56
  • There can be many approaches but the simplest one could be to use a database to maintain state of each app domain share with main appdomain. Will that help? – Deepak Bhatia Aug 10 '15 at 03:10
  • keeping collections in memory.. live tpl threads... tpl in-memory dataflows.... i dont think databases would help. – ankur Aug 11 '15 at 08:08
  • You can use MarshalByRefObject Class in this case. See this for details: https://msdn.microsoft.com/en-us/library/system.marshalbyrefobject(v=vs.110).aspx. Let me know if you face any problem in implementing this. – Deepak Bhatia Aug 11 '15 at 09:31