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:
parent
b623a71d87
commit
855a434879
@ -29,6 +29,9 @@ import __builtin__
|
|||||||
import lxml.etree
|
import lxml.etree
|
||||||
import xml.parsers.expat
|
import xml.parsers.expat
|
||||||
|
|
||||||
|
import qubes.ext
|
||||||
|
|
||||||
|
|
||||||
if os.name == 'posix':
|
if os.name == 'posix':
|
||||||
import fcntl
|
import fcntl
|
||||||
elif os.name == 'nt':
|
elif os.name == 'nt':
|
||||||
@ -661,6 +664,8 @@ class Qubes(PropertyHolder):
|
|||||||
|
|
||||||
|
|
||||||
def __init__(self, store='/var/lib/qubes/qubes.xml'):
|
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
|
#: collection of all VMs managed by this Qubes instance
|
||||||
self.domains = VMCollection()
|
self.domains = VMCollection()
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
from qubes.vm import *
|
from qubes.vm import *
|
||||||
from qubes.ext import *
|
from qubes.ext import *
|
||||||
|
|
||||||
import qubes.ext
|
|
||||||
|
|
||||||
qubes.ext.init()
|
|
||||||
|
@ -9,37 +9,28 @@ etc.
|
|||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
import qubes.vm
|
|
||||||
|
|
||||||
#: collection of system-wide hooks
|
def handler(event):
|
||||||
system_hooks = collections.defaultdict(list)
|
'''Event handler decorator factory.
|
||||||
|
|
||||||
def hook(event, vm=None, system=False):
|
|
||||||
'''Decorator factory.
|
|
||||||
|
|
||||||
To hook an event, decorate a method in your plugin class with this
|
To hook an event, decorate a method in your plugin class with this
|
||||||
decorator.
|
decorator.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
For hooking events from extensions, see :py:func:`qubes.ext.handler`.
|
||||||
|
|
||||||
:param str event: event type
|
: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):
|
def decorator(f):
|
||||||
f.ho_event = event
|
f.ha_event = event
|
||||||
|
f.ha_bound = True
|
||||||
if system:
|
|
||||||
f.ho_vm = None
|
|
||||||
elif vm is None:
|
|
||||||
f.ho_vm = qubes.vm.BaseVM
|
|
||||||
else:
|
|
||||||
f.ho_vm = vm
|
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def ishook(o):
|
|
||||||
|
def ishandler(o):
|
||||||
'''Test if a method is hooked to an event.
|
'''Test if a method is hooked to an event.
|
||||||
|
|
||||||
:param object o: suspected hook
|
:param object o: suspected hook
|
||||||
@ -48,24 +39,67 @@ def ishook(o):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
return callable(o) \
|
return callable(o) \
|
||||||
and hasattr(o, 'ho_event') \
|
and hasattr(o, 'ha_event')
|
||||||
and hasattr(o, 'ho_vm')
|
|
||||||
|
|
||||||
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):
|
def __init__(self, *args, **kwargs):
|
||||||
'''Fire system-wide hooks.
|
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]:
|
self.add_handler(attr.ha_event, attr)
|
||||||
hook(self, *args, **kwargs)
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
@ -33,22 +33,56 @@ class ExtensionPlugin(qubes.plugins.Plugin):
|
|||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
class Extension(object):
|
class Extension(object):
|
||||||
'''Base class for all extensions'''
|
'''Base class for all extensions
|
||||||
|
|
||||||
|
:param qubes.Qubes app: application object
|
||||||
|
'''
|
||||||
|
|
||||||
__metaclass__ = ExtensionPlugin
|
__metaclass__ = ExtensionPlugin
|
||||||
def __init__(self):
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
for name in dir(self):
|
for name in dir(self):
|
||||||
attr = getattr(self, name)
|
attr = getattr(self, name)
|
||||||
if not ishook(attr):
|
if not qubes.events.ishandler(attr):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if attr.ho_vm is not None:
|
if attr.ha_vm is not None:
|
||||||
attr.ho_vm.add_hook(event, attr)
|
attr.ha_vm.add_hook(attr.ha_event, attr)
|
||||||
else:
|
else:
|
||||||
# global hook
|
# 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__)
|
__all__ = qubes.plugins.load(__file__)
|
||||||
|
@ -12,7 +12,7 @@ Main public classes
|
|||||||
Helper classes and functions
|
Helper classes and functions
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
.. autoclass:: VMPlugin
|
.. autoclass:: BaseVMMeta
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
@ -57,19 +57,22 @@ import dateutil.parser
|
|||||||
import lxml.etree
|
import lxml.etree
|
||||||
|
|
||||||
import qubes
|
import qubes
|
||||||
|
import qubes.events
|
||||||
import qubes.plugins
|
import qubes.plugins
|
||||||
|
|
||||||
|
|
||||||
class VMPlugin(qubes.plugins.Plugin):
|
class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
|
||||||
'''Metaclass for :py:class:`.BaseVM`'''
|
'''Metaclass for :py:class:`.BaseVM`'''
|
||||||
def __init__(cls, name, bases, dict_):
|
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)
|
cls.__hooks__ = collections.defaultdict(list)
|
||||||
|
|
||||||
|
|
||||||
class BaseVM(qubes.PropertyHolder):
|
class BaseVM(qubes.PropertyHolder, qubes.events.Emitter):
|
||||||
'''Base class for all VMs
|
'''Base class for all VMs
|
||||||
|
|
||||||
|
:param app: Qubes application context
|
||||||
|
:type app: :py:class:`qubes.Qubes`
|
||||||
:param xml: xml node from which to deserialise
|
:param xml: xml node from which to deserialise
|
||||||
:type xml: :py:class:`lxml.etree._Element` or :py:obj:`None`
|
: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`.
|
: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.app = app
|
||||||
self.xml = xml
|
|
||||||
self.services = services
|
self.services = services
|
||||||
self.devices = collections.defaultdict(list) if devices is None else devices
|
self.devices = collections.defaultdict(list) if devices is None else devices
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
|
||||||
all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2))
|
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:
|
if not key in all_names:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
'No property {!r} found in {!r}'.format(
|
'No property {!r} found in {!r}'.format(
|
||||||
key, self.__class__))
|
key, self.__class__))
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
|
del kwargs[key]
|
||||||
|
|
||||||
|
super(BaseVM, self).__init__(xml, *args, **kwargs)
|
||||||
|
|
||||||
def add_new_vm(self, vm):
|
def add_new_vm(self, vm):
|
||||||
'''Add new Virtual Machine to colletion
|
'''Add new Virtual Machine to colletion
|
||||||
|
37
tests/events.py
Normal file
37
tests/events.py
Normal 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)
|
@ -104,8 +104,8 @@ class TC_11_VMCollection(unittest.TestCase):
|
|||||||
# XXX passing None may be wrong in the future
|
# XXX passing None may be wrong in the future
|
||||||
self.vms = qubes.VMCollection(None)
|
self.vms = qubes.VMCollection(None)
|
||||||
|
|
||||||
self.testvm1 = TestVM(None, qid=1, name='testvm1')
|
self.testvm1 = TestVM(None, None, qid=1, name='testvm1')
|
||||||
self.testvm2 = TestVM(None, qid=2, name='testvm2')
|
self.testvm2 = TestVM(None, None, qid=2, name='testvm2')
|
||||||
|
|
||||||
def test_000_contains(self):
|
def test_000_contains(self):
|
||||||
self.vms._dict = {1: self.testvm1}
|
self.vms._dict = {1: self.testvm1}
|
||||||
@ -132,8 +132,8 @@ class TC_11_VMCollection(unittest.TestCase):
|
|||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
self.vms.add(object())
|
self.vms.add(object())
|
||||||
|
|
||||||
testvm_qid_collision = TestVM(None, name='testvm2', qid=1)
|
testvm_qid_collision = TestVM(None, None, name='testvm2', qid=1)
|
||||||
testvm_name_collision = TestVM(None, name='testvm1', qid=2)
|
testvm_name_collision = TestVM(None, None, name='testvm1', qid=2)
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
self.vms.add(testvm_qid_collision)
|
self.vms.add(testvm_qid_collision)
|
||||||
|
Loading…
Reference in New Issue
Block a user