реализовать обратный вызов через границу ApplicationDomain в .net - PullRequest
5 голосов
/ 14 апреля 2011

Я загружаю dll динамически, используя Applicationdomain для выгрузки при необходимости. То, что я не могу заставить работать - это метод обратного вызова из созданного Appdomain, если задача в загруженном dll завершает себя.

Что у меня до сих пор

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

    event EventHandler TerminatedItself;
}

и сторона "Стартер"

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);
  };
}

, что приводит к исключению не найденной сборки, поскольку OnClosedItself () - это метод типа, известного только Starter, которого нет в домене приложения.

Если я включаю OnClosedItself в качестве делегата в сериализуемый класс, это тоже самое.

Есть предложения?

Edit: Я пытаюсь создать самообновляющуюся задачу. Для этого я создал стартер, который может остановить и воссоздать задачу, если доступна новая версия. Но если задача остановлена ​​откуда-то еще, она также должна уведомить стартера о прекращении.

// убрал много временного кода из вопроса

РЕДАКТИРОВАТЬ 2: Хапло указал мне правильное направление. Мне удалось реализовать обратный вызов с семафорами.

Ответы [ 3 ]

3 голосов
/ 26 апреля 2011

Недавно я использовал другой подход, который может быть проще, чем семафорный подход, просто определите интерфейс в сборке, на которую могут ссылаться оба домена приложений. Затем создайте класс, который реализует этот интерфейс и наследуется от MarshalByRefObject

Интерфейс будет любым, обратите внимание, что любые аргументы любых методов в интерфейсе должны быть сериализованы, когда вызов проходит через границу домена приложения

/// <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();
}

Затем в сборке, которую может использовать родительский домен приложения, я определяю реализацию этого интерфейса, производную от 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
}

Поэтому, когда я создаю дочерний домен приложения, я просто передаю ему экземпляр нового FailureNotifier (). Поскольку MarshalByRefObject был создан в родительском домене, любые вызовы его методов будут автоматически перенаправляться в домен приложения, в котором он был создан, независимо от того, из какого домена приложения он был вызван. Поскольку вызов будет происходить из другого потока независимо от метода интерфейса, он должен быть потокобезопасным

_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;
}
3 голосов
/ 14 апреля 2011

Я решил эту ситуацию с помощью третьей сборки , которая имела общий тип (в вашем случае реализация для IBoostrapper). В моем случае у меня было больше типов и логики, но для вас было бы немного излишне иметь сборку только для одного типа ...

Может быть, вы бы предпочли использовать с общим именем Mutex ? Затем вы можете синхронизировать 2 задачи AppDomains ...

EDIT:

Вы создаете мьютекс на главном Appdomain, а также, поскольку изначально принадлежал , поэтому он никогда не остановится на WaitOne (), поскольку вы уже владеете им.

Вы можете, например, создать Mutex на порожденном Appdomain внутри класса реализации IBootstrapper, который изначально принадлежал. После возврата вызова CreateInstanceAndUnwrap мьютекс должен существовать, и он принадлежит Bootstrapper. Теперь вы можете открыть мьютекс (позвоните OpenExisting , чтобы быть уверенным, что делитесь им), а затем вы можете подождать с ним WaitOne. После того, как порожденный загрузчик AppDomain завершится, вы можете освободить мьютекс, и основной Appdomain завершит работу.

Мьютексы распространяются на всю систему, поэтому их можно использовать в процессах и доменах приложений. Взгляните на примечания раздел MSDN Mutex

РЕДАКТИРОВАТЬ: Если вы не можете заставить его работать с мьютексами, посмотрите следующий короткий пример с использованием семафоров. Это просто для иллюстрации концепции, я не загружаю никаких дополнительных сборок и т. Д .... Основной поток в стандартном домене приложений будет ожидать освобождения семафора из порожденного домена. Конечно, если вы не хотите, чтобы основной AppDomain завершал свою работу, вам не следует разрешать выход из основного потока.

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();
    }
}
1 голос
/ 26 апреля 2011

благодаря Haplo я смог реализовать синхронизацию следующим образом

// 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();
}
...