From 5a39e777089d8bde6d0a620830a898c1cf3dd924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 6 Jan 2018 00:40:19 +0100 Subject: [PATCH 1/3] events: add support for wildcard event handlers Support registering handlers for more flexible wildcard events: not only '*', but also 'something*'. This allows to register handlers for 'property-set:*' and such. --- doc/qubes-events.rst | 1 + qubes/events.py | 14 ++++++++------ qubes/tests/events.py | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/doc/qubes-events.rst b/doc/qubes-events.rst index 8857d549..269072bf 100644 --- a/doc/qubes-events.rst +++ b/doc/qubes-events.rst @@ -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 diff --git a/qubes/events.py b/qubes/events.py index 6f950b06..34bcd9fd 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -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): diff --git a/qubes/tests/events.py b/qubes/tests/events.py index 82a7ce23..2264b894 100644 --- a/qubes/tests/events.py +++ b/qubes/tests/events.py @@ -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) From 50d34755fa532c9eb7b5c00b9b206d83128666a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 6 Jan 2018 15:05:34 +0100 Subject: [PATCH 2/3] vm: include tag/feature name in event name Rename events: - domain-feature-set -> domain-feature-set:feature - domain-feature-delete -> domain-feature-delete:feature - domain-tag-add -> domain-tag-add:tag - domain-tag-delete -> domain-tag-delete:tag Make it consistent with property-* events. It makes more sense to include tag/feature name in event name, so handler can watch a single tag/feature - which is the most common case. Otherwise, most handlers would begin with `if feature == '...'` anyway, wasting time on most events. In cases where multiple features/tags should be handled by a single handler, it is now possible to register a handler with wildcard, for example `domain-feature-set:*`. --- qubes/ext/services.py | 4 ++-- qubes/tests/vm/init.py | 41 +++++++++++++++++++++-------------------- qubes/vm/__init__.py | 13 +++++++------ qubes/vm/qubesvm.py | 22 ++++++++++++---------- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/qubes/ext/services.py b/qubes/ext/services.py index 6d82d9f3..77e94cdb 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -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 diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py index f5eb4a4f..0158313b 100644 --- a/qubes/tests/vm/init.py +++ b/qubes/tests/vm/init.py @@ -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'}) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 0a16392b..33b49a96 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -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 diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 6450b90d..2544a364 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -292,40 +292,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) From f0fe02998bd58783a7c9bb3271b3445cd78bd701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 6 Jan 2018 15:10:54 +0100 Subject: [PATCH 3/3] vm: remove doc for non-existing event `monitor-layout-change` --- qubes/vm/qubesvm.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 2544a364..fbc369bc 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -347,15 +347,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.