Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

implement callback over ApplicationDomain-boundary in .net

I load a dll dynamically using an Applicationdomain to unload when nessesary. What i cant get to work is a callback-method from the created Appdomain if the task in the loaded dll terminates itself.

What i have so far

public interface IBootStrapper
{
    void AsyncStart();
    void StopAndWaitForCompletion();

    event EventHandler TerminatedItself;
}

and the "Starter" side

private static Procedure CreateDomainAndStartExecuting()
{
  AppDomain domain = AppDomain.CreateDomain("foo", null, CODEPATH, string.Empty, true);
  IBootStrapper strapper = (IBootStrapper)domain.CreateInstanceAndUnwrap(DYNAMIC_ASSEMBLY_NAME, CLASSNAME);
  strapper.ClosedItself += OnClosedItself;
  strapper.AsyncStart();

  return delegate
  {
      strapper.StopAndWaitForCompletion();
      AppDomain.Unload(domain);
  };
}

which results in a assembly not found exception because OnClosedItself() is a method of a type only known to the Starter, which is not present in the appdomain.

If I wrapp the OnClosedItself as delegate in a serializable class it's the same.

Any suggestions?

Edit: What I'm trying to do is building a selfupdating task. Therefor i created a starter, which can stop and recreate the task if a new version is available. But if the task is stopped from somewhere else, it should also notify the starter to terminate.

// stripped a lot of temporary code from the question

EDIT 2: Haplo pointed me to the right direction. I was able to implement the callback with semaphores.

like image 218
Firo Avatar asked Apr 14 '11 16:04

Firo


3 Answers

I solved this situation by using a third assembly that had the shared type (in your case the implementation for IBoostrapper). In my case I had more types and logic, but for you it might be a bit overkill to have an assembly just for one type...

Maybe you would prefer to use a shared named Mutex? Then you can synchronize the 2 AppDomains tasks...

EDIT:

You are creating the mutex on the main Appdomain, and also as initially owned, so it will never stop on WaitOne() beacuse you already own it.

You can, for example, create the Mutex on the spawned Appdomain inside the IBootstrapper implementing class, as initially owned. After the CreateInstanceAndUnwrap call returns, the mutex should exist and it's owned by the Bootstrapper. So you can now open the mutex (call OpenExisting so you are sure that you're sharing it), and then you can WaitOne on it. Once the spawned AppDomain bootstrapper completes, you can Release the mutex, and the main Appdomain will complete the work.

Mutexes are system wide, so they can be used across processes and AppDomains. Take a look on the remarks section of MSDN Mutex

EDIT: If you cannot make it work with mutexes, see the next short example using semaphores. This is just to illustrate the concept, I'm not loading any additional assembly, etc.... The main thread in the default AppDomain will wait for the semaphore to be released from the spawned domain. Of course, if you don't want the main AppDomain to terminate, you should not allow the main thread to exit.

class Program
{
    static void Main(string[] args)
    {
        Semaphore semaphore = new Semaphore(0, 1, "SharedSemaphore");
        var domain = AppDomain.CreateDomain("Test");

        Action callOtherDomain = () =>
            {
                domain.DoCallBack(Callback);
            };
        callOtherDomain.BeginInvoke(null, null);
        semaphore.WaitOne();
        // Once here, you should evaluate whether to exit the application, 
        //  or perform the task again (create new domain again?....)
    }

    static void Callback()
    {
        var sem = Semaphore.OpenExisting("SharedSemaphore");
        Thread.Sleep(10000);
        sem.Release();
    }
}
like image 80
Haplo Avatar answered Oct 10 '22 14:10

Haplo


I used another approach recently that might be simpler than the semaphore approach, just define an interface in an assembly that both appdomains can reference. Then create a class that implements that interface and derivces from MarshalByRefObject

The interface would be whatever, note that any arguments to any methods in the interface will have to be serialized when the call goes over the appdomain boundary

/// <summary>
/// An interface that the RealtimeRunner can use to notify a hosting service that it has failed
/// </summary>
public interface IFailureNotifier
{
    /// <summary>
    /// Notify the owner of a failure
    /// </summary>
    void NotifyOfFailure();
}

Then in an assembly that the parent appdomain can use I define an implementation of that interface that derives from MarshalByRefObject:

/// <summary>
/// Proxy used to get a call from the child appdomain into this appdomain
/// </summary>
public sealed class FailureNotifier: MarshalByRefObject, IFailureNotifier
{
    private static readonly Logger Log = LogManager.GetCurrentClassLogger();

    #region IFailureNotifier Members

    public void NotifyOfFailure()
    {
        Log.Warn("Received NotifyOfFailure in RTPService");

        // Must call from threadpool thread, because the PerformMessageAction unloads the appdomain that called us, the thread would get aborted at the unload call if we called it directly
        Task.Factory.StartNew(() => {Processor.RtpProcessor.PerformMessageAction(ProcessorMessagingActions.Restart, null);});
    }

    #endregion
}

So when I create the child appdomain I simply pass it an instance of new FailureNotifier(). Since the MarshalByRefObject was created in the parent domain then any calls to its methods will automatically get marshalled over to the appdomain it was created in regardless of what appdomain it was called from. Since the call will be happening from another thread whatever the interface method does will need to be threadsafe

_runner = RealtimeRunner.CreateInNewThreadAndAppDomain(
    operationalRange,
    _rootElement.Identifier,
    Settings.Environment,
    new FailureNotifier());

...

/// <summary>
/// Create a new realtime processor, it loads in a background thread/appdomain
/// After calling this the RealtimeRunner will automatically do an initial run and then enter and event loop waiting for events
/// </summary>
/// <param name="flowdayRange"></param>
/// <param name="rootElement"></param>
/// <param name="environment"></param>
/// <returns></returns>
public static RealtimeRunner CreateInNewThreadAndAppDomain(
    DateTimeRange flowdayRange,
    byte rootElement,
    ApplicationServerMode environment,
    IFailureNotifier failureNotifier)
{
    string runnerName = string.Format("RealtimeRunner_{0}_{1}_{2}", flowdayRange.StartDateTime.ToShortDateString(), rootElement, environment);

    // Create the AppDomain and MarshalByRefObject
    var appDomainSetup = new AppDomainSetup()
    {
        ApplicationName = runnerName,
        ShadowCopyFiles = "false",
        ApplicationBase = Environment.CurrentDirectory,
    };
    var calcAppDomain = AppDomain.CreateDomain(
        runnerName,
        null,
        appDomainSetup,
        new PermissionSet(PermissionState.Unrestricted));

    var runnerProxy = (RealtimeRunner)calcAppDomain.CreateInstanceAndUnwrap(
        typeof(RealtimeRunner).Assembly.FullName,
        typeof(RealtimeRunner).FullName,
        false,
        BindingFlags.NonPublic | BindingFlags.Instance,
        null,
        new object[] { flowdayRange, rootElement, environment, failureNotifier },
        null,
        null);

    Thread runnerThread = new Thread(runnerProxy.BootStrapLoader)
    {
        Name = runnerName,
        IsBackground = false
    };
    runnerThread.Start();

    return runnerProxy;
}
like image 27
BrandonAGr Avatar answered Oct 10 '22 14:10

BrandonAGr


thanks to Haplo i was able to implement the synchronization as follows

// In DYNAMIC_ASSEMBLY_NAME
class Bootstrapper : IBootStrapper
{
    public void AsyncStart()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        m_task = new MyTask();

        m_thread = new Thread(delegate()
        {
            m_task.Run();
            if (m_task.Completed)
                Semaphore.OpenExisting(KeepAliveStarter.SEMAPHORE_NAME).Release();
        });
        thread.Start();
    }

    public void StopAndWaitForCompletion()
    {
        m_task.Shutdown();
        m_thread.Join();
    }
}

// in starter
private static Procedure CreateDomainAndStartExecuting()
{
  AppDomain domain = AppDomain.CreateDomain("foo", null, CODEPATH, string.Empty, true);
  IBootStrapper strapper = (IBootStrapper)domain.CreateInstanceAndUnwrap(DYNAMIC_ASSEMBLY_NAME, CLASSNAME);
  strapper.AsyncStart();

  return delegate
  {
      strapper.StopAndWaitForCompletion();
      AppDomain.Unload(domain);
  };
}

static void Main(string[] args)
{
    var semaphore = new Semaphore(0, 1, KeepAliveStarter.SEMAPHORE_NAME);
    DateTime lastChanged = DateTime.MinValue;
    FileSystemEventHandler codeChanged = delegate
    {
        if ((DateTime.Now - lastChanged).TotalSeconds < 2)
            return;
        lastChanged = DateTime.Now;
        Action copyToStopCurrentProcess = onStop;
        onStop = CreateDomainAndStartExecuting();
        ThreadPool.QueueUserWorkItem(delegate
        {
            copyToStopCurrentProcess();
        });
    };
    FileSystemWatcher watcher = new FileSystemWatcher(CODEPATH, ASSEMBLY_NAME + ".dll");
    watcher.Changed += codeChanged;
    watcher.Created += codeChanged;

    onStop = CreateDomainAndStartExecuting();

    watcher.EnableRaisingEvents = true;

    semaphore.WaitOne();

    onStop();
}
like image 1
Firo Avatar answered Oct 10 '22 12:10

Firo