plugin_manager.py 14.6 KB
Newer Older
mathieui's avatar
mathieui committed
1 2 3 4 5 6 7
"""
Plugin manager module.
Define the PluginManager class, the one that glues all the plugins and
the API together. Defines also a bunch of variables related to the
plugin env.
"""

8
import imp
9
import os
mathieui's avatar
mathieui committed
10
from os import path
mathieui's avatar
mathieui committed
11
import logging
12
from gettext import gettext as _
13
from sys import version_info
14

15
import core
mathieui's avatar
mathieui committed
16
import tabs
17
from plugin import PluginAPI
mathieui's avatar
mathieui committed
18 19
from config import config

mathieui's avatar
mathieui committed
20 21
log = logging.getLogger(__name__)

22
class PluginManager(object):
mathieui's avatar
mathieui committed
23 24 25 26 27
    """
    Plugin Manager
    Contains all the references to the plugins
    And keeps track of everything the plugin has done through the API.
    """
28 29
    def __init__(self, core):
        self.core = core
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
        # module name -> module object
        self.modules = {}
        # module name -> plugin object
        self.plugins = {}
        # module name -> dict of commands loaded for the module
        self.commands = {}
        # module name -> list of event_name/handler pairs loaded for the module
        self.event_handlers = {}
        # module name -> dict of tab types; tab type -> commands
        # loaded by the module
        self.tab_commands = {}
        # module name → dict of keys/handlers loaded for the module
        self.keys = {}
        # module name → dict of tab types; tab type → list of keybinds (tuples)
        self.tab_keys = {}
45
        self.roster_elements = {}
46 47 48 49 50 51 52 53 54

        if version_info[1] >= 3: # 3.3 & >
            from importlib import machinery
            self.finder = machinery.PathFinder()

        self.initial_set_plugins_dir()
        self.initial_set_plugins_conf_dir()
        self.fill_load_path()

55
        self.plugin_api = PluginAPI(core, self)
56

mathieui's avatar
mathieui committed
57 58 59
    def disable_plugins(self):
        for plugin in set(self.plugins.keys()):
            try:
60
                self.unload(plugin, notify=False)
mathieui's avatar
mathieui committed
61 62 63
            except:
                pass

64
    def load(self, name, notify=True):
mathieui's avatar
mathieui committed
65 66 67
        """
        Load a plugin.
        """
68
        if name in self.plugins:
69
            self.unload(name)
70 71

        try:
72 73 74 75 76 77
            module = None
            if version_info[1] < 3: # < 3.3
                if name in self.modules:
                    imp.acquire_lock()
                    module = imp.reload(self.modules[name])
                else:
78 79
                    file, filename, info = imp.find_module(name,
                                                           self.load_path)
80 81 82
                    imp.acquire_lock()
                    module = imp.load_module(name, file, filename, info)
            else: # 3.3 & >
83
                loader = self.finder.find_module(name, self.load_path)
84
                if not loader:
85
                    self.core.information('Could not find plugin: %s' % name)
86 87 88
                    return
                module = loader.load_module()

89
        except Exception as e:
90
            log.debug("Could not load plugin %s", name, exc_info=True)
91 92
            self.core.information("Could not load plugin %s: %s" % (name, e),
                                  'Error')
93
        finally:
94
            if version_info[1] < 3 and imp.lock_held():
95
                imp.release_lock()
96 97
            if not module:
                return
98 99 100

        self.modules[name] = module
        self.commands[name] = {}
mathieui's avatar
mathieui committed
101
        self.keys[name] = {}
102
        self.tab_keys[name] = {}
103
        self.tab_commands[name] = {}
104
        self.event_handlers[name] = []
105 106
        try:
            self.plugins[name] = None
107 108
            self.plugins[name] = module.Plugin(self.plugin_api, self.core,
                                               self.plugins_conf_dir)
109 110 111
        except Exception as e:
            log.error('Error while loading the plugin %s', name, exc_info=True)
            if notify:
112 113 114
                self.core.information(_('Unable to load the plugin %s: %s') %
                                        (name, e),
                                      'Error')
115 116 117 118
            self.unload(name, notify=False)
        else:
            if notify:
                self.core.information('Plugin %s loaded' % name, 'Info')
119

120
    def unload(self, name, notify=True):
121 122
        if name in self.plugins:
            try:
123 124
                for command in self.commands[name].keys():
                    del self.core.commands[command]
mathieui's avatar
mathieui committed
125 126
                for key in self.keys[name].keys():
                    del self.core.key_func[key]
127
                for tab in list(self.tab_commands[name].keys()):
128
                    for command in self.tab_commands[name][tab][:]:
129 130
                        self.del_tab_command(name, getattr(tabs, tab),
                                             command[0])
131
                    del self.tab_commands[name][tab]
132
                for tab in list(self.tab_keys[name].keys()):
133
                    for key in self.tab_keys[name][tab][:]:
134 135
                        self.del_tab_key(name, getattr(tabs, tab), key[0])
                    del self.tab_keys[name][tab]
136
                for event_name, handler in self.event_handlers[name][:]:
137
                    self.del_event_handler(name, event_name, handler)
138

139 140
                if self.plugins[name] is not None:
                    self.plugins[name].unload()
141
                del self.plugins[name]
142
                del self.commands[name]
mathieui's avatar
mathieui committed
143
                del self.keys[name]
144
                del self.tab_commands[name]
145
                del self.event_handlers[name]
146 147
                if notify:
                    self.core.information('Plugin %s unloaded' % name, 'Info')
148
            except Exception as e:
149
                log.debug("Could not unload plugin %s", name, exc_info=True)
150 151 152
                self.core.information(_("Could not unload plugin %s: %s") %
                                        (name, e),
                                      'Error')
153

154 155
    def add_command(self, module_name, name, handler, help,
                    completion=None, short='', usage=''):
mathieui's avatar
mathieui committed
156 157 158 159 160 161 162
        """
        Add a global command.
        """
        if name in self.core.commands:
            raise Exception(_("Command '%s' already exists") % (name,))

        commands = self.commands[module_name]
163 164
        commands[name] = core.Command(handler, help, completion, short, usage)
        self.core.commands[name] = commands[name]
mathieui's avatar
mathieui committed
165

166
    def del_command(self, module_name, name):
mathieui's avatar
mathieui committed
167 168 169
        """
        Remove a global command added through add_command.
        """
170 171 172 173 174
        if name in self.commands[module_name]:
            del self.commands[module_name][name]
            if name in self.core.commands:
                del self.core.commands[name]

175 176
    def add_tab_command(self, module_name, tab_type, name, handler, help,
                        completion=None, short='', usage=''):
mathieui's avatar
mathieui committed
177 178 179
        """
        Add a command only for a type of Tab.
        """
180 181
        commands = self.tab_commands[module_name]
        t = tab_type.__name__
mathieui's avatar
mathieui committed
182 183
        if name in tab_type.plugin_commands:
            return
184 185 186
        if not t in commands:
            commands[t] = []
        commands[t].append((name, handler, help, completion))
187 188
        tab_type.plugin_commands[name] = core.Command(handler, help,
                                                      completion, short, usage)
189 190
        for tab in self.core.tabs:
            if isinstance(tab, tab_type):
mathieui's avatar
mathieui committed
191
                tab.update_commands()
192 193

    def del_tab_command(self, module_name, tab_type, name):
mathieui's avatar
mathieui committed
194 195 196
        """
        Remove a command added through add_tab_command.
        """
197 198 199 200 201 202 203 204 205 206 207 208
        commands = self.tab_commands[module_name]
        t = tab_type.__name__
        if not t in commands:
            return
        for command in commands[t]:
            if command[0] == name:
                commands[t].remove(command)
                del tab_type.plugin_commands[name]
                for tab in self.core.tabs:
                    if isinstance(tab, tab_type) and name in tab.commands:
                        del tab.commands[name]

209
    def add_tab_key(self, module_name, tab_type, key, handler):
mathieui's avatar
mathieui committed
210 211 212
        """
        Associate a key binding to a handler only for a type of Tab.
        """
213 214 215 216 217 218 219 220 221 222 223 224 225
        keys = self.tab_keys[module_name]
        t = tab_type.__name__
        if key in tab_type.plugin_keys:
            return
        if not t in keys:
            keys[t] = []
        keys[t].append((key, handler))
        tab_type.plugin_keys[key] = handler
        for tab in self.core.tabs:
            if isinstance(tab, tab_type):
                tab.update_keys()

    def del_tab_key(self, module_name, tab_type, key):
mathieui's avatar
mathieui committed
226 227 228
        """
        Remove a key binding added through add_tab_key.
        """
229 230 231 232 233 234 235 236 237 238 239 240
        keys = self.tab_keys[module_name]
        t = tab_type.__name__
        if not t in keys:
            return
        for _key in keys[t]:
            if _key[0] == key:
                keys[t].remove(_key)
                del tab_type.plugin_keys[key]
                for tab in self.core.tabs:
                    if isinstance(tab, tab_type) and key in tab.key_func:
                        del tab.key_func[key]

mathieui's avatar
mathieui committed
241
    def add_key(self, module_name, key, handler):
mathieui's avatar
mathieui committed
242 243 244 245
        """
        Associate a global key binding to a handler, except if it
        already exists.
        """
mathieui's avatar
mathieui committed
246 247 248 249 250 251 252
        if key in self.core.key_func:
            raise Exception(_("Key '%s' already exists") % (key,))
        keys = self.keys[module_name]
        keys[key] = handler
        self.core.key_func[key] = handler

    def del_key(self, module_name, key):
mathieui's avatar
mathieui committed
253 254 255
        """
        Remove a global key binding added by a plugin.
        """
mathieui's avatar
mathieui committed
256 257 258 259 260
        if key in self.keys[module_name]:
            del self.keys[module_name][key]
            if key in self.core.key_func:
                del self.core.commands[key]

261
    def add_event_handler(self, module_name, event_name, handler, position=0):
mathieui's avatar
mathieui committed
262 263 264 265
        """
        Add an event handler. If event_name isn’t in the event list, assume
        it is a sleekxmpp event.
        """
266 267
        eh = self.event_handlers[module_name]
        eh.append((event_name, handler))
268 269 270 271
        if event_name in self.core.events.events:
            self.core.events.add_event_handler(event_name, handler, position)
        else:
            self.core.xmpp.add_event_handler(event_name, handler)
272 273

    def del_event_handler(self, module_name, event_name, handler):
mathieui's avatar
mathieui committed
274 275 276
        """
        Remove an event handler if it exists.
        """
277 278 279 280
        if event_name in self.core.events.events:
            self.core.events.del_event_handler(None, handler)
        else:
            self.core.xmpp.del_event_handler(event_name, handler)
281
        eh = self.event_handlers[module_name]
mathieui's avatar
mathieui committed
282
        eh = list(filter(lambda e: e != (event_name, handler), eh))
283 284 285 286 287 288 289

    def completion_load(self, the_input):
        """
        completion function that completes the name of the plugins, from
        all .py files in plugins_dir
        """
        try:
mathieui's avatar
mathieui committed
290
            names = set()
291
            for path in self.load_path:
mathieui's avatar
mathieui committed
292 293 294 295 296
                try:
                    add = set(os.listdir(path))
                    names |= add
                except:
                    pass
297 298 299
        except OSError as e:
            self.core.information(_('Completion failed: %s' % e), 'Error')
            return
mathieui's avatar
mathieui committed
300 301 302
        plugins_files = [name[:-3] for name in names if name.endswith('.py')
                and name != '__init__.py' and not name.startswith('.')]
        plugins_files.sort()
303
        position = the_input.get_argument_position(quoted=False)
304 305
        return the_input.new_completion(plugins_files, position, '',
                                        quotify=False)
306 307 308

    def completion_unload(self, the_input):
        """
309
        completion function that completes the name of loaded plugins
310
        """
311
        position = the_input.get_argument_position(quoted=False)
312 313
        return the_input.new_completion(sorted(self.plugins.keys()), position,
                                        '', quotify=False)
314 315

    def on_plugins_dir_change(self, new_value):
316 317 318
        self.plugins_dir = new_value
        self.check_create_plugins_dir()
        self.fill_load_path()
mathieui's avatar
mathieui committed
319 320

    def on_plugins_conf_dir_change(self, new_value):
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
        self.plugins_conf_dir = new_value
        self.check_create_plugins_conf_dir()

    def initial_set_plugins_conf_dir(self):
        """
        Create the plugins_conf_dir
        """
        plugins_conf_dir = config.get('plugins_conf_dir', '')
        if not plugins_conf_dir:
            config_home = os.environ.get('XDG_CONFIG_HOME')
            if not config_home:
                config_home = os.path.join(os.environ.get('HOME'), '.config')
            plugins_conf_dir = os.path.join(config_home, 'poezio', 'plugins')
        self.plugins_conf_dir = os.path.expanduser(plugins_conf_dir)
        self.check_create_plugins_conf_dir()

    def check_create_plugins_conf_dir(self):
        """
        Create the plugins config directory if it does not exist.
        Returns True on success, False on failure.
        """
        if not os.access(self.plugins_conf_dir, os.R_OK | os.X_OK):
            try:
                os.makedirs(self.plugins_conf_dir)
            except OSError:
                log.error('Unable to create the plugin conf dir: %s',
                        plugins_conf_dir, exc_info=True)
                return False
        return True

    def initial_set_plugins_dir(self):
        """
        Set the plugins_dir on start
        """
        plugins_dir = config.get('plugins_dir', '')
        plugins_dir = plugins_dir or\
            os.path.join(os.environ.get('XDG_DATA_HOME') or\
358 359
                             os.path.join(os.environ.get('HOME'),
                                          '.local', 'share'),
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
                         'poezio', 'plugins')
        self.plugins_dir = os.path.expanduser(plugins_dir)
        self.check_create_plugins_dir()

    def check_create_plugins_dir(self):
        """
        Create the plugins directory if it does not exist.
        Returns True on success, False on failure.
        """
        if not os.access(self.plugins_dir, os.R_OK | os.X_OK):
            try:
                os.makedirs(self.plugins_dir, exist_ok=True)
            except OSError:
                log.error('Unable to create the plugins dir: %s',
                        self.plugins_dir, exc_info=True)
                return False
        return True

    def fill_load_path(self):
        """
        Append the global packages and the source directory if available
        """

        self.load_path = []

385 386
        default_plugin_path = path.join(path.dirname(path.dirname(__file__)),
                                        'plugins')
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401

        if os.access(default_plugin_path, os.R_OK | os.X_OK):
            self.load_path.insert(0, default_plugin_path)

        if os.access(self.plugins_dir, os.R_OK | os.X_OK):
            self.load_path.append(self.plugins_dir)

        try:
            import poezio_plugins
        except:
            pass
        else:
            if poezio_plugins.__path__:
                self.load_path.append(list(poezio_plugins.__path__)[0])