| 1 | # Copyright: 2006 Marien Zwart <marienz@gentoo.org> |
|---|
| 2 | # License: GPL2 |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | """Plugin system, heavily inspired by twisted's plugin system.""" |
|---|
| 6 | |
|---|
| 7 | # Implementation note: we have to be pretty careful about error |
|---|
| 8 | # handling in here since some core functionality in pkgcore uses this |
|---|
| 9 | # code. Since we can function without a cache we will generally be |
|---|
| 10 | # noisy but keep working if something is wrong with the cache. |
|---|
| 11 | # |
|---|
| 12 | # Currently we explode if something is wrong with a plugin package |
|---|
| 13 | # dir, but not if something prevents importing a module in it. |
|---|
| 14 | # Rationale is the former should be a PYTHONPATH issue while the |
|---|
| 15 | # latter an installed plugin issue. May have to change this if it |
|---|
| 16 | # causes problems. |
|---|
| 17 | |
|---|
| 18 | import operator |
|---|
| 19 | import os.path |
|---|
| 20 | |
|---|
| 21 | from pkgcore import plugins |
|---|
| 22 | from pkgcore.util.osutils import join as pjoin |
|---|
| 23 | from pkgcore.util import modules, demandload |
|---|
| 24 | demandload.demandload(globals(), 'tempfile errno pkgcore.log:logger') |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | # Global plugin cache. Mapping of package to package cache, which is a |
|---|
| 28 | # mapping of plugin key to a list of module names. |
|---|
| 29 | _cache = {} |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | def initialize_cache(package): |
|---|
| 33 | """Determine available plugins in a package. |
|---|
| 34 | |
|---|
| 35 | Writes cache files if they are stale and writing is possible. |
|---|
| 36 | """ |
|---|
| 37 | # package plugin cache, see above. |
|---|
| 38 | package_cache = {} |
|---|
| 39 | seen_modnames = set() |
|---|
| 40 | for path in package.__path__: |
|---|
| 41 | # Check if the path actually exists first. |
|---|
| 42 | try: |
|---|
| 43 | modlist = os.listdir(path) |
|---|
| 44 | except OSError, e: |
|---|
| 45 | if e.errno != errno.ENOENT: |
|---|
| 46 | raise |
|---|
| 47 | continue |
|---|
| 48 | # Directory cache, mapping modulename to |
|---|
| 49 | # (mtime, set([keys])) |
|---|
| 50 | stored_cache = {} |
|---|
| 51 | stored_cache_name = pjoin(path, 'plugincache') |
|---|
| 52 | try: |
|---|
| 53 | cachefile = open(stored_cache_name) |
|---|
| 54 | except IOError: |
|---|
| 55 | # Something is wrong with the cache file. We just handle |
|---|
| 56 | # this as a missing/empty cache, which will force a |
|---|
| 57 | # rewrite. If whatever it is that is wrong prevents us |
|---|
| 58 | # from writing the new cache we log it there. |
|---|
| 59 | pass |
|---|
| 60 | else: |
|---|
| 61 | try: |
|---|
| 62 | # Remove this extra nesting once we require python 2.5 |
|---|
| 63 | try: |
|---|
| 64 | for line in cachefile: |
|---|
| 65 | module, mtime, entries = line[:-1].split(':', 2) |
|---|
| 66 | mtime = int(mtime) |
|---|
| 67 | entries = set(entries.split(':')) |
|---|
| 68 | stored_cache[module] = (mtime, entries) |
|---|
| 69 | except ValueError: |
|---|
| 70 | # Corrupt cache, treat as empty. |
|---|
| 71 | stored_cache = {} |
|---|
| 72 | finally: |
|---|
| 73 | cachefile.close() |
|---|
| 74 | cache_stale = False |
|---|
| 75 | # Hunt for modules. |
|---|
| 76 | actual_cache = {} |
|---|
| 77 | assumed_valid = set() |
|---|
| 78 | for modfullname in modlist: |
|---|
| 79 | modname, modext = os.path.splitext(modfullname) |
|---|
| 80 | if modext != '.py': |
|---|
| 81 | continue |
|---|
| 82 | if modname == '__init__': |
|---|
| 83 | continue |
|---|
| 84 | if modname in seen_modnames: |
|---|
| 85 | # This module is shadowed by a module earlier in |
|---|
| 86 | # sys.path. Skip it, assuming its cache is valid. |
|---|
| 87 | assumed_valid.add(modname) |
|---|
| 88 | continue |
|---|
| 89 | # It is an actual module. Check if its cache entry is valid. |
|---|
| 90 | mtime = int(os.path.getmtime(pjoin(path, modfullname))) |
|---|
| 91 | if mtime == stored_cache.get(modname, (0, ()))[0]: |
|---|
| 92 | # Cache is good, use it. |
|---|
| 93 | actual_cache[modname] = stored_cache[modname] |
|---|
| 94 | else: |
|---|
| 95 | # Cache entry is stale. |
|---|
| 96 | logger.debug( |
|---|
| 97 | 'stale because of %s: actual %s != stored %s', |
|---|
| 98 | modname, mtime, stored_cache.get(modname, (0, ()))[0]) |
|---|
| 99 | cache_stale = True |
|---|
| 100 | entries = [] |
|---|
| 101 | qualname = '.'.join((package.__name__, modname)) |
|---|
| 102 | try: |
|---|
| 103 | module = modules.load_module(qualname) |
|---|
| 104 | except modules.FailedImport: |
|---|
| 105 | # This is a serious problem, but if we blow up |
|---|
| 106 | # here we cripple pkgcore entirely which may make |
|---|
| 107 | # fixing the problem impossible. So be noisy but |
|---|
| 108 | # try to continue. |
|---|
| 109 | logger.exception('plugin import failed') |
|---|
| 110 | else: |
|---|
| 111 | keys = set(getattr(module, 'pkgcore_plugins', ())) |
|---|
| 112 | actual_cache[modname] = (mtime, keys) |
|---|
| 113 | # Cache is also stale if it sees entries that are no longer there. |
|---|
| 114 | for key in stored_cache: |
|---|
| 115 | if key not in actual_cache and key not in assumed_valid: |
|---|
| 116 | logger.debug('stale because %s is no longer there', key) |
|---|
| 117 | cache_stale = True |
|---|
| 118 | break |
|---|
| 119 | if cache_stale: |
|---|
| 120 | # Write a new cache. |
|---|
| 121 | try: |
|---|
| 122 | fd, name = tempfile.mkstemp(dir=path) |
|---|
| 123 | except OSError, e: |
|---|
| 124 | # We cannot write a new cache. We should log this |
|---|
| 125 | # since it will have a performance impact. |
|---|
| 126 | |
|---|
| 127 | # Use error, not exception for this one: the traceback |
|---|
| 128 | # is not necessary and too alarming. |
|---|
| 129 | logger.error('Cannot write cache for %s: %s. ' |
|---|
| 130 | 'Try running pplugincache.', |
|---|
| 131 | stored_cache_name, e) |
|---|
| 132 | else: |
|---|
| 133 | cachefile = os.fdopen(fd, 'w') |
|---|
| 134 | try: |
|---|
| 135 | for module, (mtime, entries) in actual_cache.iteritems(): |
|---|
| 136 | cachefile.write( |
|---|
| 137 | '%s:%s:%s\n' % (module, mtime, ':'.join(entries))) |
|---|
| 138 | finally: |
|---|
| 139 | cachefile.close() |
|---|
| 140 | os.chmod(name, 0644) |
|---|
| 141 | os.rename(name, stored_cache_name) |
|---|
| 142 | # Update the package_cache. |
|---|
| 143 | for module, (mtime, entries) in actual_cache.iteritems(): |
|---|
| 144 | seen_modnames.add(module) |
|---|
| 145 | for key in entries: |
|---|
| 146 | package_cache.setdefault(key, []).append(module) |
|---|
| 147 | return package_cache |
|---|
| 148 | |
|---|
| 149 | |
|---|
| 150 | def get_plugins(key, package=plugins): |
|---|
| 151 | """Return all enabled plugins matching "key". |
|---|
| 152 | |
|---|
| 153 | Plugins with a C{disabled} attribute evaluating to C{True} are skipped. |
|---|
| 154 | """ |
|---|
| 155 | cache = _cache.get(package) |
|---|
| 156 | if cache is None: |
|---|
| 157 | cache = _cache[package] = initialize_cache(package) |
|---|
| 158 | for modname in cache.get(key, ()): |
|---|
| 159 | module = modules.load_module('.'.join((package.__name__, modname))) |
|---|
| 160 | for obj in module.pkgcore_plugins.get(key, ()): |
|---|
| 161 | if not getattr(obj, 'disabled', False): |
|---|
| 162 | yield obj |
|---|
| 163 | |
|---|
| 164 | |
|---|
| 165 | def get_plugin(key, package=plugins): |
|---|
| 166 | """Get a single plugin matching this key. |
|---|
| 167 | |
|---|
| 168 | This assumes all plugins for this key have a priority attribute. |
|---|
| 169 | If any of them do not the AttributeError is not stopped. |
|---|
| 170 | |
|---|
| 171 | @return: highest-priority plugin or None if no plugin available. |
|---|
| 172 | """ |
|---|
| 173 | candidates = list(plugin for plugin in get_plugins(key, package)) |
|---|
| 174 | if not candidates: |
|---|
| 175 | return None |
|---|
| 176 | candidates.sort(key=operator.attrgetter('priority')) |
|---|
| 177 | return candidates[-1] |
|---|