[C#] Plugin model probleem

Pagina: 1
Acties:

  • Nazgul
  • Registratie: Februari 2000
  • Laatst online: 11-10-2022

Nazgul

Digital Pizza Crew

Topicstarter
Ik ben bezig met het schrijven van een applicatie die uitbreidbaar moet zijn dmv een soort plug-in structuur.

Nu is het in .NET mogelijk om dynamisch assemblies te laden uit (bijvoorbeeld) een subdirectory.
Ik heb het nu dus al zo ver dat als ik een DLL in die plugin directory kopieer, dat deze automatisch door mijn applicatie ingelezen en gebruikt wordt.
Maar ik wil het ook zo maken dat als ik een plugin verwijder of een nieuwere versie plaats, dat deze ook uit uit mijn programma geknikkerd wordt, danwel bijgewerkt naar de nieuwste versie.
Helaas biedt .NET niet de mogelijkheid om een eenmaal geladen assembly weer uit het geheugen te krijgen.

Na enig zoekwerk kwam ik op de MSDN site het volgende artikel tegen 'AppDomains and Dynamic Loading', waarin een oplossing voor dit probleem aangedragen wordt.

Het is namelijk mogelijk om een nieuw AppDomain te definieren, waarin je je plugin assemblies kunt laden. Bij een plugin wijziging unload je dan het AppDomain en leest alle plugins weer in in een nieuwe AppDomain.

Ik heb dus mijn code aan de hand van dat artikel aangepast, maar het werkt niet. :'(

Dit is het (ingekorte) stukje waarin de nieuwe AppDomain aangemaakt wordt:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Monitor
{
  private AppDomainSetup setup;
  private AppDomain appDomain;

  public Monitor()
  {
    string path=AppDomain.CurrentDomain.BaseDirectory;

    setup = new AppDomainSetup();
    setup.ApplicationBase = path;
    setup.CachePath= path + "cache\\";
    setup.ApplicationName = "Workers";
    setup.ShadowCopyFiles = "true";
    setup.ShadowCopyDirectories = path;
    appDomain = null;
  }

  public void Start()
  {
    ArrayList workers;
    Worker worker;
    ReadAssembly workerDomain;

    // Creeer nieuw AppDomain
    appDomain = AppDomain.CreateDomain("Workers", null, setup);

    // Geeft 'Workers' :)
    Console.WriteLine(appDomain.FriendlyName); 

    // Creeer klasse om plugins in te gaan lezen in nieuwe AppDomain
    workerDomain =  (ReadAssembly) 
      appDomain.CreateInstanceFromAndUnwrap
      ("WebMonitorTest.exe", "ReadAssembly");

    //  geeft 'WebMonitorTest.exe' :)
    Console.WriteLine(AppDomain.CurrentDomain.FriendlyName); 
    
    // Roep plugin loader functie aan in aangemaakte AppDomain
    workers = workerDomain.ReadWorkerAssemblies();


    // Doe een hoop troep met de plugins
    // KNIP

    // Geef AppDomain weer vrij
    workers=null;
    workerDomain=null;
    AppDomain.Unload(appDomain);
    appDomain = null;

  }
}


En dit is de ReadAssembly klasse:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[Serializable]
public class ReadAssembly
{
  public ArrayList ReadWorkerAssemblies()
  {
    return ReadAssemblies(new Worker().GetType());
  }

  private ArrayList ReadAssemblies(Type assemblyType)
  {
    ArrayList types = new ArrayList();
    string currentDirectory = 
      System.AppDomain.CurrentDomain.BaseDirectory;
    string[] dllFilenames = 
      System.IO.Directory.GetFiles(currentDirectory, "*.dll");

    // geeft 'WebMonitor.exe' :(            
    Console.WriteLine(System.AppDomain.CurrentDomain.FriendlyName);   
            
    foreach(string filename in dllFilenames)
    {
      try
      {
        Assembly asm = Assembly.LoadFrom(filename);
        Type[] typesInAssembly = asm.GetTypes();

        foreach(Type type in typesInAssembly)
        {
          if(type.IsSubclassOf(assemblyType))
          {
            types.Add(type);
          }
        }
      }
      catch(BadImageFormatException e) 
      { // Not a valid assembly, move on
        e=e; // To Disable 'e is never used' warning            
      }    
    }
    
    return types;
  }
}


Het probleem is dus dat bij het inladen van de plugins denkt dat hij in de WebMonitor.exe AppDomain zit, terwijl dit het Workers AppDomain zou moeten zijn.
Dat blijkt ook uit het feit dat ik de klassen uit de plugin assemblies nog steeds kan gebruiken na het unloaden van de Workers AppDomain.

Weet iemand wat ik fout doe?

No trees were killed in the sending of this message. However a large number of electrons were terribly inconvenienced.


Verwijderd

Hmm, ik denk dat het probleem hier is dat je een kopie van een Type uit het 'externe' appdomain kopieert in je eigen appdomain (dit gebeurt dan in regel 32 in je Monitor klasse). Bij een update van de externe lib worden de kopieen van de oude Types dus niet vervangen. Je kunt volgens mij nl. niet een Type gebruiken uit een ander domein, omdat de communicatie tussen appdomains niet op deze manier mogelijk is.

Een methode die ik me zou kunnen voorstellen is om gebruik te maken van .NET Remoting om de communicatie tussen de appdomains te regelen. Hoe je dit precies kunt implementeren weet ik zo snel niet een antwoord op :).

EDIT
-------
Ik heb even de moeite genomen om dat linkje naar de MSDN te lezen en het blijkt dat mijn verhaal niet helemaal (of helemaal niet ;) ) opgaat.

Wat ik er van heb begrepen is dat je in regel 32 niet een kopie, maar een proxie terugkrijgt die staat voor het 'echte' object in het remote appdomain. Kun je ook met een debugger checken of de 'workers' variabele in regel 40 ook een proxie is? Zoniet, dan maakt hij daarvan dus een kopie.

In het artikel werd ook stukje code besproken waarmee ze de plugin dir in de gaten houden. Hoe heb je dit geimplementeerd? Bij een verandering moet je volgens mij nl. de hele bende weer opnieuw inladen, en al je bestaande instanties van de klassen in een plugin opnieuw aanmaken.

[ Voor 41% gewijzigd door Verwijderd op 28-11-2002 19:45 ]


Verwijderd

Ik heb ook iets soortgelijks gedaan met plugins, maar ik laad ze gewoon in de huidige AppDomain. Ik vind het niet zo erg zat het geheugen van de assembly niet word vrijgegeven (geheugen genoeg).
Het enige probjeem wat ik had was dat de assemblies niet overschreven of gedeleted konden worden maar dat is opgelost met:
code:
1
System.AppDomain.CurrentDomain.ShadowCopyFiles = true;
code:
1
2
3
4
      catch(BadImageFormatException e) 
      { // Not a valid assembly, move on
        e=e; // To Disable 'e is never used' warning            
      }
is gelijk aan:
code:
1
      catch(BadImageFormatException) {}

  • Nazgul
  • Registratie: Februari 2000
  • Laatst online: 11-10-2022

Nazgul

Digital Pizza Crew

Topicstarter
Verwijderd schreef op 28 november 2002 @ 21:44:
Ik heb ook iets soortgelijks gedaan met plugins, maar ik laad ze gewoon in de huidige AppDomain. Ik vind het niet zo erg zat het geheugen van de assembly niet word vrijgegeven (geheugen genoeg).
Het enige probjeem wat ik had was dat de assemblies niet overschreven of gedeleted konden worden maar dat is opgelost met:
code:
1
System.AppDomain.CurrentDomain.ShadowCopyFiles = true;
Maar dan is het niet mogelijk om een plugin te updaten, want aangezien de assembly al geladen is kan ik hem (nieuwe versie) niet nog een keer laden.
[...] is gelijk aan: [...]
Dat wist ik niet. Thanx. :)
Verwijderd schreef op 28 november 2002 @ 19:27:
Ik heb even de moeite genomen om dat linkje naar de MSDN te lezen en het blijkt dat mijn verhaal niet helemaal (of helemaal niet ;) ) opgaat.

Wat ik er van heb begrepen is dat je in regel 32 niet een kopie, maar een proxie terugkrijgt die staat voor het 'echte' object in het remote appdomain. Kun je ook met een debugger checken of de 'workers' variabele in regel 40 ook een proxie is? Zoniet, dan maakt hij daarvan dus een kopie.
Ik zit nu thuis en heb dus geen toegang tot de code, maar ik heb hier thuis even een testproject in elkaar gedraaid om wat te kunnen experimenteren:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using System;
using System.Runtime.Serialization;

namespace PluginTest
{
  class MainClass
  {
    [STAThread]
    static void Main(string[] args)
    {
      // Initialiseer variabelen
      AppDomain appDomain;
      AppDomainSetup setup;
      Loader loader;
      string path;

      // Bepaal path
      path = System.Environment.CurrentDirectory;

      // Set AppDomainSetup parameters
      setup = new AppDomainSetup();
      setup.ApplicationBase=path;
      setup.ApplicationName="Plugins";

      // Creeer nieuw AppDomain
      appDomain = AppDomain.CreateDomain("Plugins", null, setup);
            
      // Toon naam van huidige AppDomain
      Console.WriteLine("Main, {0}", AppDomain.CurrentDomain.FriendlyName);

      // Toon naam van nieuwe AppDomain
      Console.WriteLine("AppDomain, {0}", appDomain.FriendlyName);

      // Creeer een Loader object in het nieuwe AppDomain
      loader = (Loader) appDomain.CreateInstanceFromAndUnwrap
        (path + "\\PluginTest.exe", "PluginTest.Loader");

      // Doe iets met het object
      loader.Go();

      // Even wachten, zodat we ook nog kunnen lezen wat hij uitspuugd.
      Console.ReadLine();
    }
  }

  public class Loader     
  {
    public void Go()
    {
      // Toon naam van huidige AppDomain
      Console.WriteLine("Loader, {0}", AppDomain.CurrentDomain.FriendlyName);
    }
  }
}

Dit geeft echter als output:
Main, PluginTest.exe
AppDomain, Plugins
Loader, PluginTest.exe


Dus hetzelfde probleem treed op als op mijn werk.

Nu heb ik de code bij dat artikel op de MSDN site gedownload en naast mijn code gelegd.
Het belangrijkste verschil zat hem in het feit dat hun Loader klasse overerft van MarshalByRefObject.
Als ik dat bij mij ook doe, dan wordt de output opeens:
Main, PluginTest.exe
AppDomain, Plugins
Loader, Plugins


En dat klopt dus wel. :) :)

Dus ik ga maandag op mijn werk kijken of dat daar ook de oplossing is.
In het artikel werd ook stukje code besproken waarmee ze de plugin dir in de gaten houden. Hoe heb je dit geimplementeerd? Bij een verandering moet je volgens mij nl. de hele bende weer opnieuw inladen, en al je bestaande instanties van de klassen in een plugin opnieuw aanmaken.
Ik heb gewoon een FileHandler gezet op DLL's in die directory. Zodra er een veranderd wordt er een event afgevuurd waarin ik het AppDomain vermoord en een nieuwe instantieer waarin de plugins opnieuw ingelezen worden.

No trees were killed in the sending of this message. However a large number of electrons were terribly inconvenienced.