implement callback over ApplicationDomain-boundary in .net
Asked Answered
H

3

5

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.

Heartland answered 14/4, 2011 at 16:49 Comment(2)
i don't have that much exprience with appdomains but can't you just tell the new appdomain to load the assembly containing your type? or an assembly containing a type shared by both appdomains?Thailand
@Thailand starter and dll reside in different locations and domain.Load(Assembly.GetExecutingAssembly().GetName()); still brings up the missing dll exceptionHeartland
S
3

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();
    }
}
Susie answered 14/4, 2011 at 17:20 Comment(6)
@Susie named mutex seems promising, but i cant get it to block, WaitOne() always instantly returns: static void Main(string[] args) { bool createdNew; var mutex = new Mutex(/*initiallyOwned*/ true, "SomeName", out createdNew); Console.WriteLine(createdNew); Console.WriteLine(mutex.WaitOne()); Console.WriteLine(mutex.WaitOne()); Console.Read(); return; }Heartland
it seems the mutexes with the same name (is use a shared const string) are different, because they dont block on waitone(), regardless from wich side i call waitone() firstHeartland
@Heartland - I completed my answerSusie
@Heartland - take a look to an example #230065 .Additionally, another way to do it is exactly the same, but instead of a mutex use a Semaphore initially set to zero.Susie
@Susie i cant get the mutex to work, it doesnt block. And there is another problem, when i unload the domain, the mutex is always released, which terminates the starter on every change of the loaded assemblyHeartland
@Heartland - I've added a short example of code using semaphores, if you cannot make it work with mutexes.Susie
Q
3

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;
}
Queenqueena answered 26/4, 2011 at 15:28 Comment(2)
After looking at this again, maybe the interface itself is not really necessary, you could just define the MarshalByRefObject class somewhere both can reference, using the interface might be slightly better design. The child appdomain is still going to need to load that type in order to use it. In my scenario all of the appdomains have the same current directory and load most of all the same dlls so this wasn't an issue.Queenqueena
nice idea and it works. Nevertheless i need the semaphore to block exiting the main loop of my starter, so i settled with it. Otherwise i would have gone your way. Unfortunaltly i cant accept both answersHeartland
H
1

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();
}
Heartland answered 26/4, 2011 at 14:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.