From 855a434879198eeb98569a9d7bdcb1258120f7b9 Mon Sep 17 00:00:00 2001 From: Wojtek Porczyk Date: Tue, 9 Dec 2014 14:14:24 +0100 Subject: [PATCH] core3: event framework adjusted for global Qubes object From now, global events are emitted by qubes.Qubes object and handlers are registered there. --- qubes/__init__.py | 5 +++ qubes/_pluginloader.py | 3 -- qubes/events.py | 96 ++++++++++++++++++++++++++++-------------- qubes/ext/__init__.py | 52 +++++++++++++++++++---- qubes/vm/__init__.py | 21 +++++---- tests/events.py | 37 ++++++++++++++++ tests/init.py | 8 ++-- 7 files changed, 167 insertions(+), 55 deletions(-) create mode 100644 tests/events.py diff --git a/qubes/__init__.py b/qubes/__init__.py index 38fbf7ad..524223b0 100644 --- a/qubes/__init__.py +++ b/qubes/__init__.py @@ -29,6 +29,9 @@ import __builtin__ import lxml.etree import xml.parsers.expat +import qubes.ext + + if os.name == 'posix': import fcntl elif os.name == 'nt': @@ -661,6 +664,8 @@ class Qubes(PropertyHolder): def __init__(self, store='/var/lib/qubes/qubes.xml'): + self._extensions = set(ext(self) for ext in qubes.ext.Extension.register.values()) + #: collection of all VMs managed by this Qubes instance self.domains = VMCollection() diff --git a/qubes/_pluginloader.py b/qubes/_pluginloader.py index 4c9c7a58..fe333d56 100644 --- a/qubes/_pluginloader.py +++ b/qubes/_pluginloader.py @@ -1,6 +1,3 @@ from qubes.vm import * from qubes.ext import * -import qubes.ext - -qubes.ext.init() diff --git a/qubes/events.py b/qubes/events.py index e1fe13fb..c8cad69b 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -9,37 +9,28 @@ etc. import collections -import qubes.vm -#: collection of system-wide hooks -system_hooks = collections.defaultdict(list) - -def hook(event, vm=None, system=False): - '''Decorator factory. +def handler(event): + '''Event handler decorator factory. To hook an event, decorate a method in your plugin class with this decorator. + .. note:: + For hooking events from extensions, see :py:func:`qubes.ext.handler`. + :param str event: event type - :param type vm: VM to hook (leave as None to hook all VMs) - :param bool system: when :py:obj:`True`, hook is system-wide (not attached to any VM) ''' def decorator(f): - f.ho_event = event - - if system: - f.ho_vm = None - elif vm is None: - f.ho_vm = qubes.vm.BaseVM - else: - f.ho_vm = vm - + f.ha_event = event + f.ha_bound = True return f return decorator -def ishook(o): + +def ishandler(o): '''Test if a method is hooked to an event. :param object o: suspected hook @@ -48,24 +39,67 @@ def ishook(o): ''' return callable(o) \ - and hasattr(o, 'ho_event') \ - and hasattr(o, 'ho_vm') + and hasattr(o, 'ha_event') -def add_system_hook(event, f): - '''Add system-wide hook. - :param callable f: function to call +class EmitterMeta(type): + '''Metaclass for :py:class:`Emitter`''' + def __init__(cls, name, bases, dict_): + super(type, cls).__init__(name, bases, dict_) + cls.__handlers__ = collections.defaultdict(set) + + +class Emitter(object): + '''Subject that can emit events ''' - global_hooks[event].append(f) + __metaclass__ = EmitterMeta -def fire_system_hooks(event, *args, **kwargs): - '''Fire system-wide hooks. + def __init__(self, *args, **kwargs): + super(Emitter, self).__init__(*args, **kwargs) + try: + propnames = set(prop.__name__ for prop in self.get_props_list()) + except AttributeError: + propnames = set() - :param str event: event type + for attr in dir(self): + if attr in propnames: + # we have to be careful, not to getattr() on properties which + # may be unset + continue - *args* and *kwargs* are passed to all hooks. - ''' + attr = getattr(self, attr) + if not ishandler(attr): + continue - for hook in system_hooks[event]: - hook(self, *args, **kwargs) + self.add_handler(attr.ha_event, attr) + + + @classmethod + def add_handler(cls, event, handler): + '''Add event handler to subject's class + + :param str event: event identificator + :param collections.Callable handler: handler callable + ''' + + cls.__handlers__[event].add(handler) + + + def fire_event(self, event, *args, **kwargs): + '''Call all handlers for an event + + :param str event: event identificator + + All *args* and *kwargs* are passed verbatim. They are different for + different events. + ''' + + for handler in self.__handlers__[event]: + if hasattr(handler, 'ha_bound'): + # this is our (bound) method, self is implicit + handler(event, *args, **kwargs) + else: + # this is from extension or hand-added, so we see method as + # unbound, therefore we need to pass self + handler(self, event, *args, **kwargs) diff --git a/qubes/ext/__init__.py b/qubes/ext/__init__.py index 01353e78..b9c1e469 100644 --- a/qubes/ext/__init__.py +++ b/qubes/ext/__init__.py @@ -33,22 +33,56 @@ class ExtensionPlugin(qubes.plugins.Plugin): return cls._instance class Extension(object): - '''Base class for all extensions''' + '''Base class for all extensions + + :param qubes.Qubes app: application object + ''' + __metaclass__ = ExtensionPlugin - def __init__(self): + + def __init__(self, app): + self.app = app + for name in dir(self): attr = getattr(self, name) - if not ishook(attr): + if not qubes.events.ishandler(attr): continue - if attr.ho_vm is not None: - attr.ho_vm.add_hook(event, attr) + if attr.ha_vm is not None: + attr.ha_vm.add_hook(attr.ha_event, attr) else: # global hook - qubes.events.add_system_hook(event, attr) + self.app.add_hook(attr.ha_event, attr) + + +def handler(event, vm=None, system=False): + '''Event handler decorator factory. + + To hook an event, decorate a method in your plugin class with this + decorator. You may hook both per-vm-class and global events. + + .. note:: + This decorator is intended only for extensions! For regular use in the + core, see py:func:`qubes.events.handler`. + + :param str event: event type + :param type vm: VM to hook (leave as None to hook all VMs) + :param bool system: when :py:obj:`True`, hook is system-wide (not attached to any VM) + ''' + + def decorator(f): + f.ho_event = event + + if system: + f.ha_vm = None + elif vm is None: + f.ha_vm = qubes.vm.BaseVM + else: + f.ha_vm = vm + + return f + + return decorator -def init(): - for ext in Extension.register.values(): - instance = ext() __all__ = qubes.plugins.load(__file__) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 7c7ff150..109197f9 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -12,7 +12,7 @@ Main public classes Helper classes and functions ---------------------------- -.. autoclass:: VMPlugin +.. autoclass:: BaseVMMeta :members: :show-inheritance: @@ -57,19 +57,22 @@ import dateutil.parser import lxml.etree import qubes +import qubes.events import qubes.plugins -class VMPlugin(qubes.plugins.Plugin): +class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta): '''Metaclass for :py:class:`.BaseVM`''' def __init__(cls, name, bases, dict_): - super(VMPlugin, cls).__init__(name, bases, dict_) + super(BaseVMMeta, cls).__init__(name, bases, dict_) cls.__hooks__ = collections.defaultdict(list) -class BaseVM(qubes.PropertyHolder): +class BaseVM(qubes.PropertyHolder, qubes.events.Emitter): '''Base class for all VMs + :param app: Qubes application context + :type app: :py:class:`qubes.Qubes` :param xml: xml node from which to deserialise :type xml: :py:class:`lxml.etree._Element` or :py:obj:`None` @@ -78,23 +81,25 @@ class BaseVM(qubes.PropertyHolder): :py:class:`qubes.vm.qubesvm.QubesVM`. ''' - __metaclass__ = VMPlugin + __metaclass__ = BaseVMMeta - def __init__(self, app, xml=None, load_stage=2, services={}, devices=None, tags={}, **kwargs): + def __init__(self, app, xml, load_stage=2, services={}, devices=None, + tags={}, *args, **kwargs): self.app = app - self.xml = xml self.services = services self.devices = collections.defaultdict(list) if devices is None else devices self.tags = tags all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2)) - for key in kwargs: + for key in list(kwargs.keys()): if not key in all_names: raise AttributeError( 'No property {!r} found in {!r}'.format( key, self.__class__)) setattr(self, key, kwargs[key]) + del kwargs[key] + super(BaseVM, self).__init__(xml, *args, **kwargs) def add_new_vm(self, vm): '''Add new Virtual Machine to colletion diff --git a/tests/events.py b/tests/events.py new file mode 100644 index 00000000..75d94a93 --- /dev/null +++ b/tests/events.py @@ -0,0 +1,37 @@ +#!/usr/bin/python2 -O + +import sys +import unittest + +sys.path.insert(0, '..') +import qubes.events + +class TC_Emitter(unittest.TestCase): + def test_000_add_handler(self): + # need something mutable + testevent_fired = [False] + + def on_testevent(subject, event): + if event == 'testevent': + testevent_fired[0] = True + + emitter = qubes.events.Emitter() + emitter.add_handler('testevent', on_testevent) + emitter.fire_event('testevent') + self.assertTrue(testevent_fired[0]) + + + def test_001_decorator(self): + class TestEmitter(qubes.events.Emitter): + def __init__(self): + super(TestEmitter, self).__init__() + self.testevent_fired = False + + @qubes.events.handler('testevent') + def on_testevent(self, event): + if event == 'testevent': + self.testevent_fired = True + + emitter = TestEmitter() + emitter.fire_event('testevent') + self.assertTrue(emitter.testevent_fired) diff --git a/tests/init.py b/tests/init.py index 13d4c9c9..936d0812 100644 --- a/tests/init.py +++ b/tests/init.py @@ -104,8 +104,8 @@ class TC_11_VMCollection(unittest.TestCase): # XXX passing None may be wrong in the future self.vms = qubes.VMCollection(None) - self.testvm1 = TestVM(None, qid=1, name='testvm1') - self.testvm2 = TestVM(None, qid=2, name='testvm2') + self.testvm1 = TestVM(None, None, qid=1, name='testvm1') + self.testvm2 = TestVM(None, None, qid=2, name='testvm2') def test_000_contains(self): self.vms._dict = {1: self.testvm1} @@ -132,8 +132,8 @@ class TC_11_VMCollection(unittest.TestCase): with self.assertRaises(TypeError): self.vms.add(object()) - testvm_qid_collision = TestVM(None, name='testvm2', qid=1) - testvm_name_collision = TestVM(None, name='testvm1', qid=2) + testvm_qid_collision = TestVM(None, None, name='testvm2', qid=1) + testvm_name_collision = TestVM(None, None, name='testvm1', qid=2) with self.assertRaises(ValueError): self.vms.add(testvm_qid_collision)