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.
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();
}
}
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;
}
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With