core3: event framework adjusted for global Qubes object

From now, global events are emitted by qubes.Qubes object and handlers are registered there.
This commit is contained in:
Wojtek Porczyk 2014-12-09 14:14:24 +01:00
parent b623a71d87
commit 855a434879
7 changed files with 167 additions and 55 deletions

View File

@ -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()

View File

@ -1,6 +1,3 @@
from qubes.vm import *
from qubes.ext import *
import qubes.ext
qubes.ext.init()

View File

@ -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)

View File

@ -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__)

View File

@ -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

37
tests/events.py Normal file
View File

@ -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)

View File

@ -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)