Remplacement de Process.Start avec AppDomains

Contexte

J’ai un service Windows qui utilise diverses DLL tierces pour effectuer des tâches sur des fichiers PDF. Ces opérations peuvent utiliser un certain nombre de ressources système et semblent parfois souffrir de memory leaks en cas d’erreurs. Les DLL sont des wrappers gérés autour d’autres DLL non gérées.

Solution actuelle

J’atténue déjà ce problème dans un cas en intégrant un appel à l’une des DLL dans une application de console dédiée et en appelant cette application via Process.Start (). Si l’opération échoue et qu’il y a des memory leaks ou des descripteurs de fichiers non validés, cela n’a pas vraiment d’importance. Le processus se terminera et le système d’exploitation récupérera les poignées.

Je voudrais appliquer cette même logique aux autres endroits de mon application qui utilisent ces DLL. Cependant, je ne suis pas très enthousiaste à l’idée d’append plus de projets de console à ma solution et d’écrire encore plus de code de plate-forme qui appelle Process.Start () et parsing la sortie des applications de la console.

Nouvelle solution

Une alternative élégante aux applications de consoles dédiées et Process.Start () semble être l’utilisation d’AppDomains, comme ceci: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx .

J’ai implémenté un code similaire dans mon application, mais les tests unitaires ne sont pas prometteurs. Je crée un fichier FileStream dans un fichier de test dans un AppDomain distinct, mais ne le jette pas. J’essaie alors de créer un autre FileStream dans le domaine principal, et cela échoue en raison du locking de fichier non publié.

Fait intéressant, l’ajout d’un événement DomainUnload vide au domaine de travail permet le test de l’unité. Peu importe, je crains que créer AppDomains “worker” ne résoudra pas mon problème.

Pensées?

Le code

///  /// Executes a method in a separate AppDomain. This should serve as a simple replacement /// of running code in a separate process via a console app. ///  public T RunInAppDomain( Func func ) { AppDomain domain = AppDomain.CreateDomain ( "Delegate Executor " + func.GetHashCode (), null, new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory } ); domain.DomainUnload += ( sender, e ) => { // this empty event handler fixes the unit test, but I don't know why }; try { domain.DoCallBack ( new AppDomainDelegateWrapper ( domain, func ).Invoke ); return (T)domain.GetData ( "result" ); } finally { AppDomain.Unload ( domain ); } } public void RunInAppDomain( Action func ) { RunInAppDomain ( () => { func (); return 0; } ); } ///  /// Provides a serializable wrapper around a delegate. ///  [Serializable] private class AppDomainDelegateWrapper : MarshalByRefObject { private readonly AppDomain _domain; private readonly Delegate _delegate; public AppDomainDelegateWrapper( AppDomain domain, Delegate func ) { _domain = domain; _delegate = func; } public void Invoke() { _domain.SetData ( "result", _delegate.DynamicInvoke () ); } } 

Le test unitaire

 [Test] public void RunInAppDomainCleanupCheck() { const ssortingng path = @"../../Output/appdomain-hanging-file.txt"; using( var file = File.CreateText ( path ) ) { file.WriteLine( "test" ); } // verify that file handles that aren't closed in an AppDomain-wrapped call are cleaned up after the call returns Portal.ProcessService.RunInAppDomain ( () => { // open a test file, but don't release it. The handle should be released when the AppDomain is unloaded new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ); } ); // sleeping for a while doesn't make a difference //Thread.Sleep ( 10000 ); // creating a new FileStream will fail if the DomainUnload event is not bound using( var file = new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ) ) { } } 

    Les domaines d’application et l’interaction entre domaines sont très minces, il faut donc s’assurer qu’il comprend vraiment comment les choses fonctionnent avant de faire quoi que ce soit … Mmm … Disons “non standard” 🙂

    Tout d’abord, votre méthode de création de stream s’exécute réellement sur votre domaine “par défaut” (surprise-surprise!). Pourquoi? Simple: la méthode que vous transmettez à AppDomain.DoCallBack est définie sur un object AppDomainDelegateWrapper et cet object existe sur votre domaine par défaut. C’est là que sa méthode est exécutée. MSDN ne parle pas de cette petite “fonctionnalité”, mais il est assez facile de vérifier: il suffit de définir un point d’arrêt dans AppDomainDelegateWrapper.Invoke .

    Donc, fondamentalement, vous devez vous débrouiller sans object “wrapper”. Utilisez la méthode statique pour l’argument de DoCallBack.

    Mais comment passez-vous votre argument “func” dans l’autre domaine afin que votre méthode statique puisse l’accepter et l’exécuter?

    Le moyen le plus évident est d’utiliser AppDomain.SetData , ou vous pouvez lancer votre propre méthode, mais peu importe comment vous le faites exactement, il y a un autre problème: si “func” est une méthode non statique, l’object défini sur doit être en quelque sorte passé dans l’autre domaine d’application. Il peut être transmis soit par valeur (alors qu’il est copié, champ par champ) ou par référence (création d’une référence d’object interdomaine avec toute la beauté de Remoting). Pour faire plus tôt, la classe doit être marquée avec un atsortingbut [Serializable] . Pour ce faire, il doit hériter de MarshalByRefObject . Si la classe n’est ni l’un ni l’autre, une exception sera lancée lors d’une tentative de transmission de l’object à l’autre domaine. Gardez à l’esprit, cependant, que le fait de passer par référence tue à peu près toute l’idée, car votre méthode sera toujours appelée sur le même domaine que l’object – c’est-à-dire celui par défaut.

    En conclusion du paragraphe ci-dessus, vous avez deux options: soit passer une méthode définie sur une classe marquée d’un atsortingbut [Serializable] (et garder à l’esprit que l’object sera copié), soit passer une méthode statique. Je soupçonne que, pour vos besoins, vous aurez besoin du premier.

    Et juste au cas où cela vous échapperait, j’aimerais souligner que votre deuxième surcharge de RunInAppDomain (celle qui prend Action ) passe une méthode définie sur une classe qui n’est pas marquée [Serializable] . Vous ne voyez aucune classe là-bas? Vous n’avez pas à: avec les delegates anonymes contenant des variables liées, le compilateur en créera un pour vous. Et il se trouve que le compilateur ne prend pas la peine de marquer cette classe générée automatiquement [Serializable] . Malheureusement, mais c’est la vie 🙂

    Après avoir dit tout cela (beaucoup de mots, n’est-ce pas? :-), et en supposant que vous ne juriez pas de passer des méthodes non statiques et non [Serializable] , voici vos nouvelles méthodes RunInAppDomain :

      ///  /// Executes a method in a separate AppDomain. This should serve as a simple replacement /// of running code in a separate process via a console app. ///  public static T RunInAppDomain(Func func) { AppDomain domain = AppDomain.CreateDomain("Delegate Executor " + func.GetHashCode(), null, new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory }); try { domain.SetData("toInvoke", func); domain.DoCallBack(() => { var f = AppDomain.CurrentDomain.GetData("toInvoke") as Func; AppDomain.CurrentDomain.SetData("result", f()); }); return (T)domain.GetData("result"); } finally { AppDomain.Unload(domain); } } [Serializable] private class ActionDelegateWrapper { public Action Func; public int Invoke() { Func(); return 0; } } public static void RunInAppDomain(Action func) { RunInAppDomain( new ActionDelegateWrapper { Func = func }.Invoke ); } 

    Si tu es toujours avec moi, j’apprécie 🙂

    Maintenant, après avoir passé tellement de temps à réparer ce mécanisme, je vais vous dire que c’était sans but.

    La chose est, AppDomains ne vous aidera pas à vos fins. Ils ne prennent en charge que les objects gérés, alors que le code non géré peut fuir et bloquer tout ce qu’il veut. Le code non géré ne sait même pas qu’il existe des éléments tels que les domaines d’application. Il ne connaît que les processus.

    Donc, au bout du compte, votre meilleure solution rest votre solution actuelle: engendrez simplement un autre processus et soyez heureux. Et, je suis d’accord avec les réponses précédentes, vous n’avez pas besoin d’écrire une autre application de console pour chaque cas. Passez simplement un nom qualifié complet d’une méthode statique et faites en sorte que l’application de console charge votre assembly, charge votre type et appelle la méthode. Vous pouvez en fait le regrouper de manière très similaire à ce que vous avez essayé avec AppDomains. Vous pouvez créer une méthode appelée “RunInAnotherProcess”, qui examinera l’argument, en extraira le nom complet et le nom de la méthode (en s’assurant que la méthode est statique) et générera l’application de la console, qui fera le rest.

    Vous n’avez pas besoin de créer de nombreuses applications de console, vous pouvez créer une application unique qui recevra comme paramètre le nom complet du type qualifié. L’application va charger ce type et l’exécuter.
    Séparer tout en petits processus est la meilleure méthode pour disposer de toutes les ressources. Un domaine d’application ne peut pas disposer de ressources complètes, mais un processus le peut.

    Avez-vous envisagé d’ ouvrir un canal entre l’application principale et les applications secondaires? De cette façon, vous pouvez transmettre des informations plus structurées entre les deux applications sans parsingr la sortie standard.