1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Plugins load/management support
19 """
20
21
22 __maintainer__ = 'Philippe Normand <philippe@fluendo.com>'
23
24 from elisa.core import log, config
25 from elisa.core.plugin import Plugin
26 from elisa.core.component import ComponentError, UnMetDependency, InitializeFailure
27 from elisa.core.component import UnSupportedPlatform
28 from elisa.core.utils import classinit, misc
29 from elisa.core import common
30 from elisa.extern import path
31 from sets import Set
32 import pkg_resources
33 import os, sys, re, gc
34 import inspect
35 import types
36 import new
37 from twisted.internet import defer
38
39
48
50
52 Exception.__init__(self)
53 self.plugin_name = plugin_name
54
56 return "Plugin %r not found" % self.plugin_name
57
59
61 Exception.__init__(self)
62 self.component_name = component_name
63
65 return "Component %r not found" % self.component_name
66
68 """
69 The PluginRegistry is responsible to find all the Plugins. Plugins
70 enabled in the Application config file will be loaded by
71 instantiating their class.
72
73 The registry can create components by first searching them by name
74 in Plugins component registries and instantiating them directly.
75 Component instances are not handled by the PluginRegistry.
76
77 @ivar _plugin_classes: Plugin classes found by the Registry
78 @type _plugin_classes: dict mapping Plugin names to Plugin classes
79 @ivar _plugin_instances: Plugins currently instantiated
80 @type _plugin_instances: L{elisa.core.plugin.Plugin} list
81 @ivar _app_config: Application's config
82 @type _app_config: L{elisa.core.config.Config}
83 """
84
85
86 __metaclass__ = classinit.ClassInitMeta
87 __classinit__ = classinit.build_properties
88
90 """ Initialize the PluginRegistry instance variables and the
91 local plugin directory.
92
93 @param application_config: Application's config
94 @type application_config: L{elisa.core.config.Config}
95 """
96 log.Loggable.__init__(self)
97 self.debug("Creating")
98
99 self._plugin_directories = []
100 self._plugin_classes = {}
101 self._plugin_instances = {}
102 self._app_config = application_config
103 self._init_local_plugins_dir()
104
106 """
107 Register the local plugins directory in pkg_resources. The
108 directory name is found in the application's config in
109 'plugins_dir' option which is in the 'general' section
110 """
111 default = '~/.elisa/plugins'
112
113 expanded_default = os.path.expanduser(default)
114 if not os.path.exists(expanded_default):
115 try:
116 os.makedirs(expanded_default)
117 except OSError, e:
118 self.warning("Could not make directory %r: %s",expand_default,
119 e)
120 default = ''
121 else:
122 default_pkg = os.path.join(expanded_default, '__init__.py')
123 if not os.path.exists(default_pkg):
124 try:
125 open(default_pkg,'w').close()
126 except IOError, error:
127 self.warning("Could not create %r: %r", default_pkg,
128 error)
129 default = ''
130
131 directories = misc.env_var_explode_list('ELISA_PLUGIN_PATH', default)
132
133 for directory in directories:
134 if not directory:
135 continue
136 if directory.startswith('~'):
137 directory = os.path.expanduser(directory)
138 else:
139 directory = os.path.abspath(directory)
140
141 if directory.endswith(os.path.sep):
142 directory = directory[:-1]
143
144 if not os.path.exists(os.path.join(directory, '__init__.py')):
145 self.warning("__init__.py file missing in %r; "
146 "skipping plugin directory registration", directory)
147 else:
148 self.debug("Plugin directory: %r", directory)
149 self._plugin_directories.append(directory)
150
151
152 environment = pkg_resources.Environment(self._plugin_directories)
153
154 working_set = pkg_resources.working_set
155
156
157 if hasattr(working_set, 'find_plugins'):
158 distributions, errors = working_set.find_plugins(environment)
159 map(working_set.add, distributions)
160 if errors:
161 self.warning("Couldn't load: %s" % errors)
162 else:
163 self.warning("Please upgrade setuptools if you want to "\
164 "be able to use additional plugins "\
165 "from %s" % self._plugin_directories)
166
168 return self._plugin_instances
169
171 return self._plugin_classes
172
197
199 """ Remove the given plugin class from our internal plugins list.
200
201 @param plugin_class: Plugin's class
202 @type plugin_class: class
203 """
204 plugin_name = plugin_class.name
205
206
207
208
209 if plugin_name in self._plugin_classes:
210 del self._plugin_classes[plugin_name]
211 self.info("Unloaded the plugin '%s'" % plugin_name)
212
214 """
215 Look for the plugin with given name, if it's not found,
216 instantiate it and return it. If no plugin with such name is
217 found, raise an exception PluginNotFound.
218
219 @param name: name of the Plugin to look for
220 @type name: string
221 @rtype: L{elisa.core.plugin.Plugin}
222 @raise PluginNotFound: if the plugin could not be found
223 """
224
225 plugin = self._plugin_instances.get(name)
226
227
228 if plugin == None:
229 plugin_class = self._plugin_classes.get(name)
230
231 if plugin_class != None:
232 plugin = plugin_class()
233 self._plugin_instances[name] = plugin
234 else:
235 raise PluginNotFound(name)
236
237 return plugin
238
251
276
278 """
279 Check that all the plugins that L{plugin_class} depends on are loaded.
280
281 @param plugin_class: Plugin's class
282 @type plugin_class: class
283
284 @raises: L{elisa.core.component.UnMetDependency} if at least one
285 dependency is not satisfied; the list of unmet dependencies
286 is passed.
287 """
288 unmet_dependencies = []
289
290 for dependency in plugin_class.plugin_dependencies:
291 if dependency not in self._plugin_classes.keys():
292 unmet_dependencies.append(dependency)
293
294 if len(unmet_dependencies) > 0:
295 raise UnMetDependency(plugin_class, unmet_dependencies)
296
298 """
299 Check that all the components that L{component} depends on are
300 available.
301
302 @param plugin: plugin containing the tested component
303 @type plugin: Plugin
304 @param component_name: component which dependencies are to be checked
305 @type component_name: string
306
307 @raises: L{elisa.core.component.UnMetDependency} if at least one
308 dependency is not satisfied; the list of unmet dependencies
309 is passed.
310 """
311 informations = plugin.components[component_name]
312 dependencies = informations.get('component_dependencies',[])
313 unmet_dependencies = []
314
315 for dependency in dependencies:
316 dependency_plugin, dependency_component = dependency.split(':')
317 dependency_plugin_class = self._plugin_classes.get(dependency_plugin)
318 if not dependency_plugin_class or \
319 dependency_component not in dependency_plugin_class.components:
320 unmet_dependencies.append(dependency)
321
322 if len(unmet_dependencies) > 0:
323 raise UnMetDependency(component_name, unmet_dependencies)
324
326 entrypoints = list(pkg_resources.iter_entry_points('elisa.plugins'))
327 entrypoints.sort(key=lambda e: e.name)
328
329
330 for entrypoint in entrypoints:
331 try:
332 py_object = entrypoint.load()
333 except:
334 self.warning("Error loading plugin %r" % entrypoint.module_name)
335 common.application.handle_traceback()
336 continue
337
338 full_path = inspect.getsourcefile(py_object)
339 plugin_dir = os.path.dirname(full_path)
340
341 if type(py_object) == types.ModuleType:
342 module_name = entrypoint.module_name
343 dirname = os.path.basename(os.path.split(full_path)[0])
344 plugin_class = new.classobj(dirname.title(), (Plugin,),{})
345 plugin_class.__module__ = module_name
346
347
348 plugin_conf = 'plugin.conf'
349
350 config_path = plugin_class.get_resource_file(plugin_conf)
351 if not os.path.exists(config_path):
352 self.warning("Plugin config file not found for %r plugin",
353 entrypoint.name)
354 continue
355 plugin_class.config_file = config_path
356 else:
357 plugin_class = py_object
358
359 plugin_class.directory = plugin_dir
360
361 assert issubclass(plugin_class, Plugin), \
362 '%r is not a valid Plugin!' % plugin_class
363
364
365 self.register_plugin(plugin_class)
366
370
372
373 modules = []
374 for plugins_dir in self._plugin_directories:
375 try:
376 for filename in path.path(plugins_dir).walkfiles('*.py',
377 errors='ignore'):
378 parent = filename.parent
379
380 if parent not in sys.path:
381 sys.path.append(parent)
382
383 module_filename = os.path.basename(filename)
384 module_name, _ = os.path.splitext(module_filename)
385
386 if type(module_filename) == unicode:
387 module_filename = module_filename.encode('utf-8')
388 try:
389 module = __import__(module_name, {}, {},
390 [module_filename,])
391 except ImportError, error:
392 self.warning("Could not import module %s" % filename)
393 common.application.handle_traceback()
394 continue
395 else:
396 modules.append(module)
397
398 sys.path.remove(parent)
399 except OSError, error:
400 self.warning(error)
401
402 for module in modules:
403 for symbol_name in dir(module):
404 symbol = getattr(module, symbol_name)
405 if inspect.isclass(symbol) and symbol != Plugin and \
406 issubclass(symbol, Plugin):
407
408 self.register_plugin(symbol)
409
430
436
438 """ Use the PluginRegistry to to create a Component.
439
440 component_path format should be like this::
441
442 plugin_name:component_name[:instance_id]
443
444 The plugin_name is the name of the plugin sub-class to
445 load. See L{elisa.core.plugin.Plugin.name}.
446
447 The component_name is the name of the Component sub-class to
448 load. See L{elisa.core.component.Component.name}.
449
450 @param component_path: information to locate the Component to load
451 @type component_path: string
452 @rtype: L{elisa.core.component.Component}
453 @raise InitializeFailure: When the component failed to initialize
454 @raise ComponentNotFound: When the component could not be found in any plugin
455 @raise UnMetDependency: When the component misses a dependency
456 """
457 component = None
458
459 self.debug("Trying to create %r component" % component_path)
460
461 infos = self._get_component_infos(component_path)
462
463 ComponentClass, plugin, component_path_tuple = infos
464
465 if ComponentClass:
466 ComponentClass.name = component_path_tuple[1]
467
468
469
470 if not ComponentClass.plugin:
471 ComponentClass.plugin = plugin
472
473 instance_id = component_path_tuple[2]
474 self.log("Instantiating Component %r from %r",
475 ComponentClass.name, component_path_tuple[0])
476 component = ComponentClass()
477
478
479 component.path = component_path_tuple[3]
480 component.id = instance_id
481 component.load_config(self._app_config)
482 res = component.initialize()
483 if isinstance(res, defer.Deferred):
484
485
486
487
488 def component_initialize_done(result):
489 self.info("Component %s loaded" % component.name)
490
491 return component
492
493 self.info("Component %s still loading" % component.name)
494 res.addCallback(component_initialize_done)
495
496 return res
497
498 self.info("Component %s loaded" % component.name)
499 else:
500 raise ComponentNotFound(component_path)
501
502 return component
503
505 """
506 Get information about a component given its path. This method
507 doesn't instantiate Components, it just looks for Component
508 classes.
509
510 @param component_path: information to locate the Component to load
511 @type component_path: string
512 @rtype: (L{elisa.core.component.Component}, L{elisa.core.plugin.Plugin}, tuple)
513 """
514 ComponentClass = None
515 plugin = None
516
517 component_path_tuple = self._split_component_path(component_path)
518
519 if component_path_tuple:
520
521 plugin_name, component_name = component_path_tuple[0:2]
522
523 plugin = self.get_plugin_with_name(plugin_name)
524 self.debug("Found plugin %r, searching for Component %r",
525 plugin_name, component_name)
526
527
528 plugin.check_dependencies()
529
530 ComponentClass = self._import_component(plugin, component_name)
531
532 return (ComponentClass, plugin, component_path_tuple)
533
535 """
536 """
537 Class = None
538 component = plugin.components.get(component_name,{})
539 path = component.get('path')
540 if path:
541
542 if isinstance(path, basestring):
543
544
545
546
547 plugin.check_component_dependencies(component_name)
548
549 mod_class = path.split(':')
550 plugin_path = plugin.directory
551 old_path = sys.path[:]
552 sys.path.insert(0, os.path.abspath(os.path.dirname(plugin_path)))
553 package_name = os.path.basename(plugin_path)
554 if package_name:
555 full_path = "%s.%s" % (package_name,mod_class[0])
556 else:
557 full_path = mod_class[0]
558 try:
559 module = __import__(full_path,
560 {}, {}, [mod_class[1]])
561 Class = getattr(module, mod_class[1])
562 except (ImportError, AttributeError), error:
563 self.warning("Could not import component %s.%s",
564 plugin.name, path)
565 sys.path = old_path
566 common.application.handle_traceback()
567 else:
568 self.debug("Imported %r from %s", Class, plugin_path)
569 sys.path = old_path
570
571 else:
572 Class = path
573
574 return Class
575
577 """ Retrieve the class of a Component given its path.
578
579 @param component_path: information to locate the Component
580 @type component_path: string
581 @rtype: L{elisa.core.component.Component} class
582 """
583 self.debug("Retrieving component class of %r", component_path)
584 infos = self._get_component_infos(component_path)
585 ComponentClass, plugin, path_tuple = infos
586
587 return ComponentClass
588
589
591 """split the component path:
592 plugin_name:component_name[:instance_id]
593
594 add default instance_id (0) if missing
595
596 @param component_path: information to locate the Component to load
597 @type component_path: string
598 @returns: tuple of 4 elements :
599 (plugin_name, component_name, instance_id, adjusted_component_path)
600 return empty tuple if the intance is not valid
601 @rtype: tuple
602 """
603 component_path_tuple = ()
604 if component_path:
605 plugin_name = None
606 parts = component_path.split(':')
607
608 if len(parts) < 2:
609
610 self.warning("Syntax error in %r. Components must be "\
611 "specified with plugin_name:component_name"\
612 " syntax" % component_path)
613 elif len(parts) == 2:
614 plugin_name, component_name = parts
615 instance_id = 0
616
617 elif len(parts) == 3:
618 plugin_name, component_name, instance_id = parts
619 try:
620 instance_id = int(instance_id)
621 except:
622 self.warning("Syntax error in %r. Components must "\
623 "have a numerical instance_id", component_path)
624 plugin_name = None
625
626 if plugin_name:
627 component_path_tuple = (plugin_name, component_name,
628 instance_id, component_path)
629 else:
630
631 self.warning("Empty component_path (%r), nothing to create then",
632 component_path)
633
634 return component_path_tuple
635