Exemple de plugin pour Trac

Trac, outil de gestion de projet de développement, est conçu pour recevoir de nouvelles fonctionnalités par ajout de plugin. Ce billet montre quelques techniques pour réaliser des plugins eux-mêmes extensibles.

Le schema suivant montre les éléments essentiels du modèle de plugin que Trac offre.

Pour créer un plugin il suffit de créer une classe qui hérite de Component. Ainsi Trac va charger la classe au démarrage.

class Core(Component):
 """
 Main class of the Trac Cron Plugin. This is the entry point
 for Trac plugin architecture
 """
 pass

Un composant est un objet passif qui est appelé lorsque Trac est activé. Comme c’est une application web, c’est sur requête du client que tout s’active. Pour répondre à ces sollicitations le plugin doit implémenter une interface connue que Trac va appeler.

Par exemple le plugin peut définit sa propre interface d’administration. Pour cela il doit implémenter IAdminPanelProvider er ITemplateProvider

class Core(Component):
 """
 Main class of the Trac Cron Plugin. This is the entry point
 for Trac plugin architecture
 """    

 implements(IAdminPanelProvider, ITemplateProvider)

 # IAdminProviderPanel

 def get_admin_panels(self, req):
  if ('TRAC_ADMIN' in req.perm) :
   yield ('tracini', 'trac.ini', 'cron_admin', u'Trac Cron')

 def render_admin_panel(self, req, category, page, path_info):
  req.perm.assert_permission('TRAC_ADMIN')
  data = {}
  return 'cron_admin.html', data

 # ITemplateProvider

 def get_htdocs_dirs(self):
  return []

 def get_templates_dirs(self):
  from pkg_resources import resource_filename
  return [resource_filename(__name__, 'templates')]

L’implémentation au sens Trac consiste pour un plugin à s’enregistrer auprès de Trac comme fournisseur des interfaces par le biais de l’appel à la fonction

implements(*args)

en passant les interfaces voulues.

Dans l’exemple ci-dessous Trac va appeler la fonction get_admin_panel() quand il voudra créer la liste des panneau de configuration et render_admin_panel() quand il voudra afficher la panneau de configuration du plugin donné.

Mais on peut créer des plugins actifs qui initient des traitements périodiquement. Pour cela Trac ne propose rien, il faut faire soit même un plugin qui crée une Thread lors de son instanciation.

class Core(Component):
  def__init__(self,*args,**kwargs):
    Component.__init__(self, *args, **kwargs)
    Timer(60 , self.wake_up).start()

  def wake_up(self):
    print "Hello, one minute again"
    Timer(60 , self.wake_up).start()

Il existe un plugin qui se charge de toute la mécanique d’ordonnancement laissant le développeur se concentrer sur la tâche à faire. Il s’agit de TracCronPlugin.

Cela nous amène à expliquer comment écrire des plugin extensibles. La règle d’or est de ne pas lier son plugin avec du code interne ou même des class internes. Il faut toujours penser une fonctionnalité comme une interface.

La première étape est de bien isoler l’interface de la fonctionnalité. Par exemple on peut isoler l’interface d’un tâche comme devant avoir une méthode pour être lancée, une méthode pour s’identifier et une méthode de description. Alors l’interface d’une tâche peut se définir ainsi:

class ICronTask(Interface):
 """
 Interface for component task
 """

 def wake_up(self, *args):
  """
  Call by the scheduler when the task need to be executed
  """
  raise NotImplementedError

 def getId(self):
  """
  Return the key to use in trac.ini to cinfigure this task
  """
  raise NotImplementedError

 def getDescription(self):
  """
  Return the description of this task to be used in the admin panel.
  """
  raise NotImplementedError

Supposons que dans Trac il y ait des plugin qui implémentent cette interface (nous avons vu plus haut comment faire), il reste alors au plugin utilisateur d’utiliser un point d’extension en instanciant la classe ExtensionPoint comme ceci:

class Core(Component):
 task_event_list = ExtensionPoint(ITaskEventListener)

 def do_something_with_task(self):
  for task in self.cron_tack_list:
   # do something with a task
   task.wake_up()

On voit qu’un point d’extension est une séquence qui permet d’itérer sur les composants (plugin) qui implémentent l’interface passé en paramètre du constructeur de  ExtensionPoint

L’exemple complet peut être trouver en regardant le code source de TracCronPlugin

Planificateur de tâches dans Trac

wikipedia:Trac icon, modified for the Open Ico...
Image via Wikipedia

Trac est un outils de gestion de projet pour lequel il existe de nombreuses extensions que l’on trouve sur le site trac-hacks . Mais il existe un plugin qui manquait à cette liste: un planificateur de tâche comme Cron.

Pourquoi aurait-on besoin d’un cron? Certaines tâches récurrentes comme la création périodique de ticket de type tâche comme par exemple “revue de code” pourraient ne plus avoir besoin d’être créées à la main. Dans mon cas, j’ai une version personnalisée de Trac qui possède une fonction de synchronisation des utilisateurs natifs Trac avec une table spécifique mise à jour journalièrement. La synchronisation est codée dans une méthode appelée par un bouton dans l’interface d’administration. Cette synchronisation ne peut être appelée que depuis Trac car elle fait usage de l’API de Trac.

Pour parvenir à nos fins nous pouvons utiliser le planificateur de l’OS ( Cron unix ou AT de Windows) mais cette solution manque d’élégance car on va ajouter un adhérence avec l’OS. De plus même si on enrichie la commande trac-admin pour bénéficier de l’environnement de Trac, il restera à gérer la concurrence avec le processus trac. C’est pour ses raisons que le choix d’un ordonnanceur de tâche intégré à Trac a fait son chemin.

Comment développer un tel planificateur?

La documentation de Trac nous montre qu’il est possible de définir des “composant” et des “extension”. Un composant est un plugin Trac qui est instancié par Trac. Une extension est un composant qui implémente une interface. Dans Trac il existe des interface standard comme ITicketValidator qui indique que le composant sait valider les champs d’un ticket. Chaque composant peut utiliser des extensions.

Pour notre besoin il nous suffit de :

  1. définir une interface de tâche ICronTask
  2. définir un composant Corequi utilisera les extensions de type ICronTask
  3. Le composant Core doit se charger de gérer l’ordonnanceur par une thread dédiée
  4. La thread dédiée doit se lancer dès le démarrage de Trac: pour cela on va intercepter l’instanciation du composant parce que Trac ne définit par d’interface pour capter son démarrage

Tels sont les éléments clés à mettre en œuvre auxquels s’ajoutent les point suivants pour une plus grande utilisabilité:

  1. Paramétrage dans trac.ini et par interface d’administration
  2. État de l’ordonnanceur : marche/arrêt paramétrable
  3. précision de l’ordonnanceur paramétrable
  4. plusieurs planification pour une même tâche
  5. planification dans l’interface d’administration

Désormais ce plugin existe, il est disponible sur Trac-Hacks, il s’agit de TracCronPlugin