Merge remote-tracking branch 'qubesos/pr/185'

* qubesos/pr/185:
  vm: remove doc for non-existing event `monitor-layout-change`
  vm: include tag/feature name in event name
  events: add support for wildcard event handlers
This commit is contained in:
Marek Marczykowski-Górecki 2018-01-22 15:32:57 +01:00
commit 74eb3f3208
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
7 changed files with 77 additions and 53 deletions

View File

@ -62,6 +62,7 @@ Note that your handler will be called for all instances of this class.
.. TODO: extensions
.. TODO: add/remove_handler
.. TODO: wildcards (property-set:*)
Handling events with variable signature

View File

@ -25,6 +25,7 @@ etc.
'''
import asyncio
import collections
import fnmatch
import itertools
@ -35,14 +36,15 @@ def handler(*events):
To hook an event, decorate a method in your plugin class with this
decorator.
Some event handlers may be defined as coroutine. In such a case, *async*
should be set to :py:obj:``True``.
Some event handlers may be defined as coroutine. In such a case
:py:func:`asyncio.coroutine` decorator should be used after this one,
i.e. you should decorate a coroutine.
See appropriate event documentation for details.
.. note::
For hooking events from extensions, see :py:func:`qubes.ext.handler`.
:param str events: events
:param str events: events names, can contain basic wildcards (`*`, `?`)
'''
def decorator(func):
@ -155,9 +157,9 @@ class Emitter(object, metaclass=EmitterMeta):
handlers_dict = i.__handlers__
except AttributeError:
continue
handlers = handlers_dict.get(event, set())
if '*' in handlers_dict:
handlers = handlers_dict['*'] | handlers
handlers = [h_func for h_name, h_func_set in handlers_dict.items()
for h_func in h_func_set
if fnmatch.fnmatch(event, h_name)]
for func in sorted(handlers,
key=(lambda handler: hasattr(handler, 'ha_bound')),
reverse=True):

View File

@ -39,7 +39,7 @@ class ServicesExtension(qubes.ext.Extension):
vm.untrusted_qdb.write('/qubes-service/{}'.format(service),
str(int(bool(value))))
@qubes.ext.handler('domain-feature-set')
@qubes.ext.handler('domain-feature-set:*')
def on_domain_feature_set(self, vm, event, feature, value, oldvalue=None):
'''Update /qubes-service/ QubesDB tree in runtime'''
# pylint: disable=unused-argument
@ -52,7 +52,7 @@ class ServicesExtension(qubes.ext.Extension):
vm.untrusted_qdb.write('/qubes-service/{}'.format(service),
str(int(bool(value))))
@qubes.ext.handler('domain-feature-delete')
@qubes.ext.handler('domain-feature-delete:*')
def on_domain_feature_delete(self, vm, event, feature):
'''Update /qubes-service/ QubesDB tree in runtime'''
# pylint: disable=unused-argument

View File

@ -165,3 +165,29 @@ class TC_00_Emitter(qubes.tests.QubesTestCase):
self.assertCountEqual(effect,
('testvalue1', 'testvalue2', 'testvalue3', 'testvalue4'))
def test_006_wildcard(self):
# need something mutable
testevent_fired = [0]
def on_foobar(subject, event, *args, **kwargs):
# pylint: disable=unused-argument
testevent_fired[0] += 1
def on_foo(subject, event, *args, **kwargs):
# pylint: disable=unused-argument
testevent_fired[0] += 1
emitter = qubes.events.Emitter()
emitter.add_handler('foo:*', on_foo)
emitter.add_handler('foo:bar', on_foobar)
emitter.events_enabled = True
emitter.fire_event('foo:testevent')
self.assertEqual(testevent_fired[0], 1)
emitter.fire_event('foo:bar')
# now foo:bar and foo:* should be executed
self.assertEqual(testevent_fired[0], 3)
emitter.fire_event('foo:')
self.assertEqual(testevent_fired[0], 4)
emitter.fire_event('testevent')
self.assertEqual(testevent_fired[0], 4)

View File

@ -155,37 +155,37 @@ class TC_20_Tags(qubes.tests.QubesTestCase):
def test_000_add(self):
self.tags.add('testtag')
self.assertEventFired(self.vm, 'domain-tag-add',
self.assertEventFired(self.vm, 'domain-tag-add:testtag',
kwargs={'tag': 'testtag'})
def test_001_add_existing(self):
self.tags.add('testtag')
self.vm.fired_events.clear()
self.tags.add('testtag')
self.assertEventNotFired(self.vm, 'domain-tag-add')
self.assertEventNotFired(self.vm, 'domain-tag-add:testtag')
def test_002_remove(self):
self.tags.add('testtag')
self.vm.fired_events.clear()
self.tags.remove('testtag')
self.assertEventFired(self.vm, 'domain-tag-delete',
self.assertEventFired(self.vm, 'domain-tag-delete:testtag',
kwargs={'tag': 'testtag'})
def test_003_remove_not_present(self):
with self.assertRaises(KeyError):
self.tags.remove('testtag')
self.assertEventNotFired(self.vm, 'domain-tag-delete')
self.assertEventNotFired(self.vm, 'domain-tag-delete:testtag')
def test_004_discard_not_present(self):
with self.assertNotRaises(KeyError):
self.tags.discard('testtag')
self.assertEventNotFired(self.vm, 'domain-tag-delete')
self.assertEventNotFired(self.vm, 'domain-tag-delete:testtag')
def test_005_discard_present(self):
self.tags.add('testtag')
with self.assertNotRaises(KeyError):
self.tags.discard('testtag')
self.assertEventFired(self.vm, 'domain-tag-delete',
self.assertEventFired(self.vm, 'domain-tag-delete:testtag',
kwargs={'tag': 'testtag'})
def test_006_clear(self):
@ -193,9 +193,9 @@ class TC_20_Tags(qubes.tests.QubesTestCase):
self.tags.add('testtag2')
self.vm.fired_events.clear()
self.tags.clear()
self.assertEventFired(self.vm, 'domain-tag-delete',
self.assertEventFired(self.vm, 'domain-tag-delete:testtag',
kwargs={'tag': 'testtag'})
self.assertEventFired(self.vm, 'domain-tag-delete',
self.assertEventFired(self.vm, 'domain-tag-delete:testtag2',
kwargs={'tag': 'testtag2'})
def test_007_update(self):
@ -203,9 +203,9 @@ class TC_20_Tags(qubes.tests.QubesTestCase):
self.tags.add('testtag2')
self.vm.fired_events.clear()
self.tags.update(('testtag2', 'testtag3'))
self.assertEventFired(self.vm, 'domain-tag-add',
self.assertEventFired(self.vm, 'domain-tag-add:testtag3',
kwargs={'tag': 'testtag3'})
self.assertEventNotFired(self.vm, 'domain-tag-add',
self.assertEventNotFired(self.vm, 'domain-tag-add:testtag2',
kwargs={'tag': 'testtag2'})
@ -217,14 +217,14 @@ class TC_21_Features(qubes.tests.QubesTestCase):
def test_000_set(self):
self.features['testfeature'] = 'value'
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:testfeature',
kwargs={'feature': 'testfeature', 'value': 'value'})
def test_001_set_existing(self):
self.features['test'] = 'oldvalue'
self.vm.fired_events.clear()
self.features['test'] = 'value'
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:test',
kwargs={'feature': 'test', 'value': 'value', 'oldvalue':
'oldvalue'})
@ -232,29 +232,30 @@ class TC_21_Features(qubes.tests.QubesTestCase):
self.features['test'] = 'value'
self.vm.fired_events.clear()
del self.features['test']
self.assertEventFired(self.vm, 'domain-feature-delete',
self.assertEventFired(self.vm, 'domain-feature-delete:test',
kwargs={'feature': 'test'})
def test_003_unset_not_present(self):
with self.assertRaises(KeyError):
del self.features['test']
self.assertEventNotFired(self.vm, 'domain-feature-delete')
self.assertEventNotFired(self.vm, 'domain-feature-delete:test')
def test_004_set_bool_true(self):
self.features['test'] = True
self.assertTrue(self.features['test'])
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:test',
kwargs={'feature': 'test', 'value': '1'})
def test_005_set_bool_false(self):
self.features['test'] = False
self.assertFalse(self.features['test'])
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:test',
kwargs={'feature': 'test', 'value': ''})
def test_006_set_int(self):
self.features['test'] = 123
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:test',
kwargs={'feature': 'test', 'value': '123'})
def test_007_clear(self):
@ -262,9 +263,9 @@ class TC_21_Features(qubes.tests.QubesTestCase):
self.features['test2'] = 'value2'
self.vm.fired_events.clear()
self.features.clear()
self.assertEventFired(self.vm, 'domain-feature-delete',
self.assertEventFired(self.vm, 'domain-feature-delete:test',
kwargs={'feature': 'test'})
self.assertEventFired(self.vm, 'domain-feature-delete',
self.assertEventFired(self.vm, 'domain-feature-delete:test2',
kwargs={'feature': 'test2'})
def test_008_update(self):
@ -275,8 +276,8 @@ class TC_21_Features(qubes.tests.QubesTestCase):
self.assertEqual(self.features['test2'], 'value3')
self.assertEqual(self.features['test3'], 'value4')
self.assertEqual(self.features['test'], 'value')
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:test2',
kwargs={'feature': 'test2', 'value': 'value3',
'oldvalue': 'value2'})
self.assertEventFired(self.vm, 'domain-feature-set',
self.assertEventFired(self.vm, 'domain-feature-set:test3',
kwargs={'feature': 'test3', 'value': 'value4'})

View File

@ -113,7 +113,7 @@ class Features(dict):
def __delitem__(self, key):
super(Features, self).__delitem__(key)
self.vm.fire_event('domain-feature-delete', feature=key)
self.vm.fire_event('domain-feature-delete:' + key, feature=key)
def __setitem__(self, key, value):
if value is None or isinstance(value, bool):
@ -127,10 +127,11 @@ class Features(dict):
has_oldvalue = False
super(Features, self).__setitem__(key, value)
if has_oldvalue:
self.vm.fire_event('domain-feature-set', feature=key, value=value,
oldvalue=oldvalue)
self.vm.fire_event('domain-feature-set:' + key, feature=key,
value=value, oldvalue=oldvalue)
else:
self.vm.fire_event('domain-feature-set', feature=key, value=value)
self.vm.fire_event('domain-feature-set:' + key, feature=key,
value=value)
def clear(self):
for key in tuple(self):
@ -265,12 +266,12 @@ class Tags(set):
if elem in self:
return
super(Tags, self).add(elem)
self.vm.fire_event('domain-tag-add', tag=elem)
self.vm.fire_event('domain-tag-add:' + elem, tag=elem)
def remove(self, elem):
'''Remove a tag'''
super(Tags, self).remove(elem)
self.vm.fire_event('domain-tag-delete', tag=elem)
self.vm.fire_event('domain-tag-delete:' + elem, tag=elem)
#
# end of overriding

View File

@ -296,40 +296,42 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-restore'``)
.. event:: domain-feature-set (subject, event, feature, value
.. event:: domain-feature-set:feature (subject, event, feature, value
[, oldvalue])
A feature was changed.
A feature was changed. This event is fired before bare
`domain-feature-set` event.
*oldvalue* is present only when there was any.
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-feature-set'``)
:param event: Event name (``'domain-feature-set:' feature``)
:param feature: feature name
:param value: new value
:param oldvalue: old value, if any
.. event:: domain-feature-delete (subject, event, feature)
.. event:: domain-feature-delete:feature (subject, event, feature)
A feature was removed.
A feature was removed. This event is fired before bare
`domain-feature-delete` event.
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-feature-delete'``)
:param event: Event name (``'domain-feature-delete:' feature``)
:param feature: feature name
.. event:: domain-tag-add (subject, event, tag)
.. event:: domain-tag-add:tag (subject, event, tag)
A tag was added.
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-tag-add'``)
:param event: Event name (``'domain-tag-add:' tag``)
:param tag: tag name
.. event:: domain-tag-delete (subject, event, tag)
.. event:: domain-tag-delete:tag (subject, event, tag)
A feature was removed.
:param subject: Event emitter (the qube object)
:param event: Event name (``'domain-tag-delete'``)
:param event: Event name (``'domain-tag-delete:' tag``)
:param tag: tag name
.. event:: feature-request (subject, event, *, untrusted_features)
@ -349,15 +351,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
ranging from plainly ignoring the request to verbatim copy into
:py:attr:`features` with only minimal sanitisation.
.. event:: monitor-layout-change (subject, event, monitor_layout)
Desktop layout was changed, probably because a display was plugged
in or out.
:param subject: Event emitter (the qube object)
:param event: Event name (``'monitor-layout-change'``)
:param monitor_layout: The new layout
.. event:: firewall-changed (subject, event)
Firewall was changed.