El código y otra información adicional está disponible en la página del proyecto Monet Plugins Library y tenéis disponible la última versión del código en el control de versiones de subversion http://svn.thealphasite.org/Monet.
Todo el proyecto responde a la licencia LGPL, lo cual implica, a grandes rasgos que el proyecto es esencialmente GPL (con todas las obligaciones que ello conlleva, como distribuir las modificaciones echas al código y mantener la misma licencia) pero permitiendo que el framework sea utilizado dese una aplicación o librería que no sea GPL. En general esta es una de las licencias que más "libre" me parece. Básicamente, si quieres modificarla y mejorarla tienes que "devolver" lo que has hecho y por tanto la librería va mejorando constantemente, pero si simplemente quieres usarla desde algún otro programa, entonces no encuentras restricciones.
En esta biblioteca encontraremos tanto la definición de los interfaces que definen un plugin, la definición de los atributos necesarios para declarar Hooks, el interfaz del plugin manager así como una clase que nos permitirá acceder, registrar y recuperar servicios.
Para la definición de servicios existen dos clases auxiliares. La primera, CService, encapsula la información del propio servicio, datos como el nombre, el grupo o tipo en el que se engloba el servicio o la descripción del mismo. Así mismo guarda una referencia al objeto que realmente implementa dicho servicio. Está clase es usada por el Plugin Manager para encapsular la información recogida de los atributos de un plugin en cuanto a los servicios que implementa.
La segunda clase, Modules, es una clase estática que nos permite registrar y recuperar servicios del sistema. La clase es usada por el Plugin Manager para registrar los servicios según son declarados por los plugins y por cada plugin o parte del sistema que desee utilizar un servicio simplemente recuperandolo (pidiendoselo) a la clase.
La implementación de la clase estática modules es relativamente sencilla. Si echamos un vistazo:
Básicamente la clase contiene una lista de servicios y la funcionalidad básica para almacenar (registrar), eliminar (desregistrar) y obtener los distintos servicios. Puede obtenerse tanto el objeto CService en si mismo (con toda la información asociada que contiene) como el objeto que implementa el servicio (que deberá ser casteado al interfaz adecuado). El primer caso será más probablemente usado por el Plugin Manager, para mantener y mostrar la información relacionada con los servicios. El segundo caso probablemente se use mucho más por los plugins que desean acceder y usar el servicio.
Dentro de la librería de plugins se definen tanto los interfaces como los atributos necesarios para mantener la información asociada.
Cada plugin del sistema debe implementar al menos 2 métodos básicos (carga y descarga) y una propiedad.
Los métodos realizan la carga y descarga del plugin. Esto implica, para el caso de la carga, realizar todas las acciones necesarias para dejar el plugin preparado para su ejecución, por ejemplo, crear un menú en el interface gráfico, realizar toda una serie de acciones en el registro o lanzar una tarea en segundo plano para, por ejemplo, quedarse escuchando de un determinado puerto TCP. La opción de descarga deberá deshacer dichas opciones de forma que el sistema vuelva a estar en el estado que estaba antes de que se cargara el plugin.
Aunque hemos visto que durante la fase de carga el plugin puede, si lo desea, crear una tarea que se encargue de realizar determinadas acciones en background (y pueden concebirse plugins con dicho planteamiento), esta no es la idea inicial. La fase de carga está orientada a preparar el plugin, fundamentalmente los datos que necesite tal como registrar manejadores de eventos en determinados servicios o configurar parte del registro. Si deseamos que nuestro plugin directamente no devuelva el control a la aplicación, sino que sencillamente se quede ejecutando en su propia tarea debemos marca la propiedad Threaded a true. Esto hará (suponiendo que tengamos un plugin manager que soporte esa opción) que el lanzador cree una tarea dedicada para dicho plugin y se la asigne a la ejecución del Load.
Si elegimos este modo de funcionamiento (por ejemplo levantando un servidor TCP en el método Load) deberemos hacer que el método Unload provoque la finalización del bucle de ejecución del método Load.
Además de la definición del plugin existen toda una serie de atributos que nos permitirán definir la metainformación asociada a cada plugin. De está forma podremos marcar las dependencias que este tiene, los hooks que declara y los que intercepta.
No voy a especificar aqui la implementación de dichos atributos porque son bastante básicos de forma que me limitaré a hacer una pequeña descripción de cada uno de ellos:
El plugin manager es el encargado de manejar toda la información que hemos definido anteriormente y hacer buen uso de ella. Mediante el uso de atributos hemos creado un esqueleto de metainformación que nos permite marcar cada plugin así como almacenar información sobre sus características y las acciones que queremos que realice. El Plugin Manager debe encargarse de generar las estructuras de datos necesarias en memoria para mantener dicha información y proveer de los medios adecuados para ejecutar acciones sobre los plugins dados.
Examinando el interfaz del Plugin Manager vemos que tiene una serie de métodos que nos permitirán listar los plugins, verificar si pueden o no cargar (en función de las dependencias que tengan), cargarlos, descargarlos, etc. Por supuesto el propio Plugin Manager es en si mismo un plugin que implementa un servicio. De esta forma, cualquier otro plugin puede hacer referencia y acceder a la información del Plugin Manager.
Creo que el interfaz está suficientemente comentado y es bastante claro por lo que voy a pasar a centrarme en el como realiza las acciones necesarias el Plugin Manager.
La información de los plugins se obtiene mediante el método privado GetPluginInfo. Dicho plugin abre cada ensamblado que se le pasa y recorre cada tipo del sistema buscando el interfaz IPlugin. Una vez encontrado adquiere el atributo de plugin correspondiente (si no existe devuelve un error).
Para ello haremos uso de diversas funciones del framework de .NET, concretamente de la parte de reflection que nos permite obtener información de cualquier ensamblado en runtime. Una descripción del uso de Reflection queda fuera de los objetivos de este articulo, para más información sobre reflection visita el artículo Usando .NET Reflection
De los propios atributos del plugin se obtienen los datos básicos y se almacenan en las estructuras que el plugin manager utilizar para retener esa información. A continuación se comprueba si el plugin es instalable (si implementa el interfaz IInstalablePlugin) y finalmente se comprueban las dependencias declaradas (RequiresPluginAttribute y RequiresServicesAttribute).
Como hemos visto durante el paso anterior lo único que se hace es recopilar información sobre el plugin, incluyendo las dependencias que el plugin ha declarado. El Plugin Manager implementa un método que comprueba si un determinado plugin puede o no cargar dado el estado de sus dependencias.
El proceso de comprobación es relativamente sencillo, consiste sencillamente en comprobar que, para cada una de las dependencias del plugin dado, estas se encuentran cargadas. Así comprobamos que los plugins necesarios (identificados en la lista de dependencias de plugins leida en el paso anterior) están cargados, así mismo comprobamos que, en caso de que sea un servicio lo que el plugin necesita para funcionar, este está disponible.
Cada "pedazo" de información se guarda en un diccionario (tabla hash). Hay una para las dependencias en otros plugins, y otra para las dependencias en servicios. Lo que hacemos es recorrer cada una de dichas dependencias y comprobar que se satisface.
Uno de los puntos principales de este sistema de plugins es el mecanismo de hooks y su intercepción, es decir, la posibilidad de que un determinado plugin modifique el comportamiento de otro interceptando sus llamadas.
Para ello, como ya habíamos mencionado, se han definido dos atributos (Hooks y Hookable) utilizado para marcar los métodos que interceptan y los eventos que son interceptados.
El plugin manager es la pasta que se encarga de unir esos dos conceptos. Para ello nos apoyamos en tres métodos auxiliares: ProcessHooks, ProcessHookables y UpdateHookInfo
Los métodos ProcessHookables y ProcessHookInfo se limitan a actualizar la información almacenada sobre los propios hooks dentro de la estructura de información del plugin.
El método UpdateHookInfo se llama cada vez que cambia dicha información y se encarga de asociar los métodos con los eventos, si echamos un vistazo al código.
En primer lugar recorremos todos los eventos declarados como hookables y comprobamos si alguno de los plugins ya activos en el sistema los intercepta. A la vez, para cada plugin que vamos mirando comprobamos si contiene algún hook que sea interceptado por el plugin que se activa.
El mecanismo de intercepción se basa en la creación de delegados y en su asignación a los eventos declarados. Para ello en primer lugar obtenemos un EventInfo, que es una estructura de .NET que encapsula la información asociada un evento y nos permite actuar sobre ella, asociado al plugin. Dicho EventInfo contiene dos métodos fundamentales, AddEventHandler y RemoveEventHandler, que nos permiten añadir un manejador al evento. Dichos métodos admiten varias sobrecargas, la que nosotros utilizaremos acepta como parametros una instancia de objeto y un delegado.
Por otro lado el método CreateDelegate de la clase Delegate nos permite obtener un delegado adecuado en función del tipo de delegado, la instancia del objeto que provee el método así como el propio método. De esta forma obtenemos por un lado el evento y por otro lado una instancia válida del delegado (para que dicha instancia sea válida y se pueda construir, el método declarado como hook tiene que coincidir con la definición del delegado).
En ocasiones es posible que queramos permitir la configuración avanzada de un plugin. Para ello se ha definido el interfaz IConfigurable de forma que, para aquellos plugins que lo implementen pueda, el propio plugin, definir un formulario de configuración invocable por el Plugin Manager.
El Plugin Manager es, en si mismo, un plugin que además proporciona un servicio. La función principal del plugin manager es la carga, descarga y configuración de plugins, así como la obtención de información acerca del mismo.
El interfaz extendido IPluginManagerGUI proporciona un interfaz que define dos métodos (Show y ShowModal) que permiten presentar un interfaz de manejado de dicho funcionamiento. El plugin manager que acompaña el artículo proporciona una implementación de dicho interfaz aunque podría realizarse cualquier otra invocando a los métodos del plugin manager.
En general, siempre que desarrollemos un plugin que dependa otro plugin estaremos creando una dependencia real con él, es decir, si vamos a utilizarlo como servicio, debemos tener, al menos la descripción del interfaz de dicho servicio.
Esto, en general, es la forma habitual de trabajar y la que mayor flexibilidad nos permite, sin embargo en alguna ocasión es posible que no dispongamos de dicha implementación en código o que no queramos proporcionarla. Para este caso se define el interfaz IMessageable que imita el mecanismo de comunicación basado en mensajes de windows. Para ello tenemos la siguiente definición:
De esta forma podemos mandar mensajes a un determinado servicio. Evidentemente tendremos que conocer el tipo de mensajes y los argumentos que reciben (por mucho que queramos no hay forma de ir a ciegas), pero de esta forma, no necesitamos ningún tipo de definición de interfaz.
Lo ideal es que cada plugin definido, permita acceder de esta forma a cada uno de los servicios que proporciona, de forma que se pueda acceder a ellos de ambos modos...