Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Utiliser les AppDomains pour charger et décharger des plugins

La réflection est un des gros points forts de .Net. D'autant plus qu'il est très facile de s'en servir. L'une des utilisations de la réflection est la gestion de plugins.

L'utilisation des AppDomains peut apporter beaucoup lorsqu'on charge des assemblies par réflection. Déjà, si vos plugins peuvent être développés par n'importe qui, il peut être intéressant de restreindre les permissions du code du plugin. Par exemple vous pouvez interdire au plugin de lire ou écrire sur le disque.

D'autre part, sans utiliser d'AppDomain (si vous chargez vos plugins dans le même AppDomain que votre application principale), il n'est pas possible de décharger de la mémoire les assemblies chargés par réflection. Par contre, si vous chargez vos plugins dans un autre AppDomain, il est possible de décharger l'AppDomain et tous les assemblies qui y ont été chargés. Cela peut être utile par exemple si votre programme doit charger un grand nombre d'assemblies temporairement, mais qu'il ne puisse pas redémarrer. Si les assemblies qui ne sont plus utilisés ne peuvent pas être déchargés, cela peut conduire à une "pollution" de la mémoire.

Création d'un AppDomain avec des permissions réduites

La création d'un AppDomain avec des permissions réduites a été grandement simplifiée par .Net 2.0. Il suffit maintenant de définir un objet PermissionSet, qui contient toutes les permissions que possèderons les assemblies chargés dans cet AppDomain, puis d'utiliser la méthode suivante (de la classe AppDomain) :

public static AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info, PermissionSet grantSet, params StrongName[] fullTrustAssemblies);

Cette méthode crée un AppDomain dans lequel les assemblies chargés listés dans fullTrustAssemblies seront FullTrust (aucune restriction de sécurité) et tous les autres auront les permissions définies dans grantSet.

PermissionSet ps = new PermissionSet(PermissionState.None);
ps.AddPermission(
new SecurityPermission(PermissionState.Unrestricted));
ps.AddPermission(
new UIPermission(PermissionState.Unrestricted));

AppDomain sideDomain = AppDomain.CreateDomain("Side Domain", AppDomain.CurrentDomain.Evidence, ads, ps, null);

Echanger des objets entre deux AppDomains

Il faut ensuite charger un assembly (par réflection) dans cet AppDomain. Le but est de le charger dans le domaine secondaire, sans le charger dans le domaine principal. Comme ça, en déchargeant l'AppDomain secondaire, on décharge aussi le plugin.

Pour cela, il faut utiliser la méthode CreateInstanceAndUnwrap (ou ses dérivées) :

IPlugin o = sideDomain.CreateInstanceAndUnwrap("LibTest", "LibTest.TestClass") as IPlugin;

Cela instancie un objet TestClass dans le domaine sideDomain, et renvoie cet objet.

Il faut comprendre une chose très importante. Les AppDomains sont un peu comme des processus, mais à l'intérieur du CLR. La mémoire de chaque AppDomain est isolée de celle des autres. Un AppDomain ne peut pas avoir de référence à un objet qui appartient à un autre AppDomain (pour cela, il faut utiliser un proxy). Il est quand même possible d'accéder à des objets d'un autre AppDomain par certains mécanismes. Pour que cela soit possible, l'objet doit être soit :

  • Serializable : donc décoré de [Serializable]. Dans ce cas là, l'objet est serialisé dans l'AppDomain de départ et déserializé dans l'AppDomain de destination. C'est donc une copie que vous avez dans l'AppDomain de destination. Il y a une instance de l'objet dans chaque AppDomain.
    Seulement, tous les types qu'utilise cet objet (voir même le type de l'objet) seront chargés dans l'AppDomain de destination s'ils ne l'étaient pas déjà (l'assemly qui contient la définition de ces types, plus exactement).
    En fait, dès qu'un AppDomain a une instance d'une classe dans sa mémoire, il doit obligatoirement avoir chargé l'assembly dans lequel cette classe est définie. Heureusement d'ailleurs, cela permet au CLR de garantir que le code de chaque méthode que l'on peut appeler est bien disponible et chargé en mémoire.
  • Remotable : c'est à dire qu'il dérive de MarshalByRefObject. Dans ce cas là, lorsque vous passez l'objet d'un AppDomain à un autre, c'est en réalité un proxy que vous obtenez dans l'AppDomain de destination. Il n'y a pas d'instance de l'objet dans l'AppDomain de destination, l'objet réel n'existe que dans l'AppDomain de départ.
    Lorsque vous appelez une méthode de cet objet (depuis l'AppDomain de destination), le proxy va en fait appeler la méthode de l'objet réel, dans l'AppDomain de départ (les restrictions de sécurité de l'AppDomain de départ vont donc bien s'appliquer). C'est de cette façon que l'on peut exécuter du code dans un AppDomain distant.
    De plus, aucun assembly n'est chargé dans l'AppDomain de destination puisqu'il n'y a pas d'instance de l'objet dans l'AppDomain de destination.

Il est donc possible d'échanger des objets d'un AppDomain à un autre, soit par copie (objet serializable), soit par référence (objet remotable). Il y a plusieurs façons de passer un objet d'un AppDomain à un autre :

  • Vous pouvez utiliser CreateInstanceAndUnwrap, l'objet passera (en tant que copie ou en tant que proxy) de l'AppDomain "distant" vers l'AppDomain courant :
    IPlugin o = sideDomain.CreateInstanceAndUnwrap("LibTest", "LibTest.TestClass") as IPlugin;
  • Vous pouvez passer des objets en arguments à un proxy, l'objet va de l'AppDomain courant vers l'AppDomain "distant" :
    o.SendObject(anyObject);
  • Vous pouvez avoir un objet en retour d'un appel d'une méthode sur un proxy, l'objet va de l'AppDomain "distant" vers l'AppDomain courant :
    ICollection collection = o.GetObject();

Contrôler le chargement des assemblies dans l'AppDomain principal

Si vous ne voulez pas que l'assembly de votre plugin soit chargé dans l'AppDomain principal (pour pouvoir le décharger), veillez bien à soit faire retourner des objet remotable, soit des objets définis dans les assemblies que vous voulez bien charger.

Par exemple, lors de l'appel suivant (o est un proxy) :

ICollection collection = o.GetCollection("Un argument");

  • Si l'objet retourné est remotable, vous obtiendrez un proxy de l'objet "réel", et dans tous les cas, LibTest ne sera pas chargé dans l'AppDomain principal puisque aucune instance n'existe dans l'AppDomain principal.
  • Si l'objet retourné est de type ArrayList (par exemple), il est sérializable. C'est donc une copie qui est retournée. Comme ArrayList est défini dans mscorlib.dll, cet assembly est chargé si il ne l'était pas déjà (ce qui est peu probable). Mais l'assembly LibTest n'est pas chargé dans l'AppDomain principal.
  • Par contre, si l'objet retouné est de type MaCollection, défini dans LibTest, le CLR aura besoin de charger LibTest pour "connaître" le type MaCollection (même si du point de vue de votre code, vous manipulez toujours une ICollection). Donc le plugin sera chargé dans l'AppDomain principal.

Donc pour éviter cette "pollution", faites attention à faire retourner à votre plugin soit des objets remotables, soit des objets définis dans votre assembly ou dans celles du framework, soit des objets dont on ne peut pas hériter (sealed, struct).

Attention aux pièges...

Si vous utilisez par exemple :

sideDomain.Load("LibTest");

Vous chargez l'assembly LibTest dans sideDomain, bien entendu, mais aussi dans l'AppDomain principal, donc vous ne pourrez plus le décharger de l'AppDomain principal, c'est l'échec. Pourquoi ?

La classe Assembly est sérializable (et pas remotable), et contient des instances d'objets Type qui représentent des types définis dans LibTest. Donc dès que l'AppDomain reçoit une instance de Assembly (c'est ce que renvoie AppDomain.Load), le CLR est obligé de charger cet assembly.

N'essayez donc pas de faire :

foreach (Assembly assembly in sideDomain.GetAssemblies())
    Console
.WriteLine("Assembly Chargé : " + assembly.FullName);

car vous allez récupérer une copie de objet Assembly chargé dans l'autre AppDomain, ce qui aura pour effet de charger tout les assemblies de l'autre AppDomain dans le votre. Par contre, il peut être intéressant de le faire pour l'assembly en cours (AppDomain.CurrentDomain) puisque cela vous dira quels assemblies y sont chargés.

De la même façon, lorsque vous examinez une liste d'assemblies dans un dossier par réflection, le seul fait de retourner une objet Assembly (par Assembly.LoadFile()) charge l'assembly en mémoire dans l'AppDomain courrant. Vous aurez donc tous les assemblies du dossier chargés en mémoire sans qu'il vous soit possible de les décharger.

Quelques références intéressantes sur le sujet

Publié mercredi 11 octobre 2006 20:21 par RaptorXP
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

# re: Utiliser les AppDomains pour charger et décharger des plugins

Wahoooo !

Bel article.
mercredi 11 octobre 2006 23:57 by cyril

# re: Utiliser les AppDomains pour charger et décharger des plugins

Lol, merci ;)
jeudi 12 octobre 2006 13:42 by RaptorXP
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01

- SharePoint Online: Script PowerShell pour supprimer une colonne dans tous les sites d’une collection par Blog Technique de Romelard Fabrice le 11-27-2018, 18:01