Procházet zdrojové kódy

core3: basic global events and their documentation

Wojtek Porczyk před 9 roky
rodič
revize
1a032ecf2a
5 změnil soubory, kde provedl 156 přidání a 42 odebrání
  1. 11 1
      doc/conf.py
  2. 102 40
      qubes/__init__.py
  3. 32 0
      qubes/dochelpers.py
  4. 5 0
      qubes/events.py
  5. 6 1
      qubes/vm/__init__.py

+ 11 - 1
doc/conf.py

@@ -28,7 +28,17 @@ sys.path.insert(0, os.path.abspath('../'))
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'qubes.dochelpers']
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.autosummary',
+    'sphinx.ext.coverage',
+    'sphinx.ext.doctest',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.todo',
+    'sphinx.ext.viewcode',
+
+    'qubes.dochelpers',
+]
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']

+ 102 - 40
qubes/__init__.py

@@ -330,6 +330,9 @@ class VMCollection(object):
         if not isinstance(value, qubes.vm.BaseVM):
             raise TypeError('{} holds only BaseVM instances'.format(self.__class__.__name__))
 
+        if not hasattr(value, 'qid'):
+            value.qid = self.domains.get_new_unused_qid()
+
         if value.qid in self:
             raise ValueError('This collection already holds VM that has qid={!r} (!r)'.format(
                 value.qid, self[value.qid]))
@@ -338,6 +341,7 @@ class VMCollection(object):
                 value.name, self[value.name]))
 
         self._dict[value.qid] = value
+        self.app.fire_event('domain-added', value)
 
 
     def __getitem__(self, key):
@@ -359,7 +363,9 @@ class VMCollection(object):
 
 
     def __delitem__(self, key):
-        del self._dict[self[key].qid]
+        vm = self[key]
+        del self._dict[vm.qid]
+        self.app.fire_event('domain-deleted', vm)
 
 
     def __contains__(self, key):
@@ -467,12 +473,28 @@ class property(object):
 
 
     def __set__(self, instance, value):
+        try:
+            oldvalue = getattr(instance, self.__name__)
+            has_oldvalue = True
+        except AttributeError:
+            has_oldvalue = False
+
         if self._setter is not None:
             value = self._setter(instance, self, value)
         if self._type is not None:
             value = self._type(value)
+
         instance._init_property(self, value)
 
+        if has_oldvalue:
+            instance.fire_event('property-set:' + self.__name__, value, oldvalue)
+        else:
+            instance.fire_event('property-set:' + self.__name__, value)
+
+
+    def __delete__(self, instance):
+        delattr(instance, self._attr_name)
+
 
     def __repr__(self):
         return '<{} object at {:#x} name={!r} default={!r}>'.format(
@@ -505,8 +527,27 @@ class property(object):
             prop.__name__, self.__class__.__name__))
 
 
-class PropertyHolder(object):
-    '''Abstract class for holding :py:class:`qubes.property`'''
+class PropertyHolder(qubes.events.Emitter):
+    '''Abstract class for holding :py:class:`qubes.property`
+
+    Events fired by instances of this class:
+
+        .. event:: property-load (subject, event)
+
+            Fired once after all properties are loaded from XML. Individual
+            ``property-set`` events are not fired.
+
+        .. event:: property-set:<propname> (subject, event, name, newvalue[, oldvalue])
+
+            Fired when property changes state. Signature is variable, *oldvalue* is
+            present only if there was an old value.
+
+            :param name: Property name
+            :param newvalue: New value of the property
+            :param oldvalue: Old value of the property
+
+    Members:
+    '''
 
     def __init__(self, xml, *args, **kwargs):
         super(PropertyHolder, self).__init__(*args, **kwargs)
@@ -545,11 +586,14 @@ class PropertyHolder(object):
     def load_properties(self, load_stage=None):
         '''Load properties from immediate children of XML node.
 
+        ``property-set`` events are not fired for each individual property.
+
         :param lxml.etree._Element xml: XML node reference
         '''
 
 #       sys.stderr.write('<{}>.load_properties(load_stage={}) xml={!r}\n'.format(hex(id(self)), load_stage, self.xml))
 
+        self.events_enabled = False
         all_names = set(prop.__name__ for prop in self.get_props_list(load_stage))
 #       sys.stderr.write('  all_names={!r}\n'.format(all_names))
         for node in self.xml.xpath('./properties/property'):
@@ -563,6 +607,9 @@ class PropertyHolder(object):
                         name, self.__class__))
 
             setattr(self, name, value)
+
+        self.events_enabled = True
+        self.fire_event('property-loaded')
 #       sys.stderr.write('  load_properties return\n')
 
 
@@ -630,22 +677,45 @@ class Qubes(PropertyHolder):
 
     :param str store: path to ``qubes.xml``
 
-    The store is loaded in stages.
+    The store is loaded in stages:
+
+    1.  In the first stage there are loaded some basic features from store
+        (currently labels).
+
+    2.  In the second stage stubs for all VMs are loaded. They are filled
+        with their basic properties, like ``qid`` and ``name``.
+
+    3.  In the third stage all global properties are loaded. They often
+        reference VMs, like default netvm, so they should be filled after
+        loading VMs.
+
+    4.  In the fourth stage all remaining VM properties are loaded. They
+        also need all VMs loaded, because they represent dependencies
+        between VMs like aforementioned netvm.
+
+    5.  In the fifth stage there are some fixups to ensure sane system
+        operation.
 
-    In the first stage there are loaded some basic features from store
-    (currently labels).
+    This class emits following events:
 
-    In the second stage stubs for all VMs are loaded. They are filled with
-    their basic properties, like ``qid`` and ``name``.
+        .. event:: domain-added (subject, event, vm)
 
-    In the third stage all global properties are loaded. They often reference
-    VMs, like default netvm, so they should be filled after loading VMs.
+            When domain is added.
 
-    In the fourth stage all remaining VM properties are loaded. They also need
-    all VMs loaded, because they represent dependencies between VMs like
-    aforementioned netvm.
+            :param subject: Event emitter
+            :param event: Event name (``'domain-added'``)
+            :param vm: Domain object
 
-    In the fifth stage there are some fixups to ensure sane system operation.
+        .. event:: domain-deleted (subject, event, vm)
+
+            When domain is deleted. VM still has reference to ``app`` object,
+            but is not contained within VMCollection.
+
+            :param subject: Event emitter
+            :param event: Event name (``'domain-deleted'``)
+            :param vm: Domain object
+
+    Methods and attributes:
     '''
 
     default_netvm = VMProperty('default_netvm', load_stage=3,
@@ -822,22 +892,16 @@ class Qubes(PropertyHolder):
         return labels
 
 
+
     def add_new_vm(self, vm):
         '''Add new Virtual Machine to colletion
 
         '''
 
-        if not hasattr(vm, 'qid'):
-            vm.qid = self.domains.get_new_unused_qid()
-
         self.domains.add(vm)
 
-        #
-        # XXX
-        # all this will be moved to an event handler
-        # and deduplicated with self.load()
-        #
-
+    @qubes.events.handler('domain-added')
+    def on_domain_addedd(self, event, vm):
         # make first created NetVM the default one
         if not hasattr(self, 'default_fw_netvm') \
                 and vm.provides_network \
@@ -866,23 +930,21 @@ class Qubes(PropertyHolder):
                 and hasattr(vm, 'netvm'):
             self.default_clockvm = vm
 
-        # XXX don't know if it should return self
-        return vm
-
-    # XXX This was in QubesVmCollection, will be in an event
-#   def pop(self, qid):
-#       if self.default_netvm_qid == qid:
-#           self.default_netvm_qid = None
-#       if self.default_fw_netvm_qid == qid:
-#           self.default_fw_netvm_qid = None
-#       if self.clockvm_qid == qid:
-#           self.clockvm_qid = None
-#       if self.updatevm_qid == qid:
-#           self.updatevm_qid = None
-#       if self.default_template_qid == qid:
-#           self.default_template_qid = None
-#
-#       return super(QubesVmCollection, self).pop(qid)
+
+    @qubes.events.handler('domain-deleted')
+    def on_domain_deleted(self, event, vm):
+        if self.default_netvm == vm:
+            del self.default_netvm
+        if self.default_fw_netvm == vm:
+            del self.default_fw_netvm
+        if self.clockvm == vm:
+            del self.clockvm
+        if self.updatevm == vm:
+            del self.updatevm
+        if self.default_template == vm:
+            del self.default_template
+
+        return super(QubesVmCollection, self).pop(qid)
 
 
 # load plugins

+ 32 - 0
qubes/dochelpers.py

@@ -10,6 +10,7 @@ particulary our custom Sphinx extension.
 
 import csv
 import posixpath
+import re
 import sys
 import urllib2
 
@@ -18,7 +19,9 @@ import docutils.nodes
 import docutils.parsers.rst
 import docutils.parsers.rst.roles
 import docutils.statemachine
+import sphinx
 import sphinx.locale
+import sphinx.util.docfields
 
 def fetch_ticket_info(uri):
     '''Fetch info about particular trac ticket given
@@ -116,6 +119,30 @@ class VersionCheck(docutils.parsers.rst.Directive):
         return [node]
 
 
+#
+# this is lifted from sphinx' own conf.py
+#
+
+event_sig_re = re.compile(r'([a-zA-Z-]+)\s*\((.*)\)')
+
+def parse_event(env, sig, signode):
+    m = event_sig_re.match(sig)
+    if not m:
+        signode += sphinx.addnodes.desc_name(sig, sig)
+        return sig
+    name, args = m.groups()
+    signode += sphinx.addnodes.desc_name(name, name)
+    plist = sphinx.addnodes.desc_parameterlist()
+    for arg in args.split(','):
+        arg = arg.strip()
+        plist += sphinx.addnodes.desc_parameter(arg, arg)
+    signode += plist
+    return name
+
+#
+# end of codelifting
+#
+
 def setup(app):
     app.add_role('ticket', ticket)
     app.add_config_value('ticket_base_uri', 'https://wiki.qubes-os.org/ticket/', 'env')
@@ -124,5 +151,10 @@ def setup(app):
         man=(visit, depart))
     app.add_directive('versioncheck', VersionCheck)
 
+    fdesc = sphinx.util.docfields.GroupedField('parameter', label='Parameters',
+                         names=['param'], can_collapse=True)
+    app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
+                        doc_field_types=[fdesc])
+
 
 # vim: ts=4 sw=4 et

+ 5 - 0
qubes/events.py

@@ -57,6 +57,8 @@ class Emitter(object):
 
     def __init__(self, *args, **kwargs):
         super(Emitter, self).__init__(*args, **kwargs)
+        self.events_enabled = True
+
         try:
             propnames = set(prop.__name__ for prop in self.get_props_list())
         except AttributeError:
@@ -95,6 +97,9 @@ class Emitter(object):
         different events.
         '''
 
+        if not self.events_enabled:
+            return
+
         for handler in self.__handlers__[event]:
             if hasattr(handler, 'ha_bound'):
                 # this is our (bound) method, self is implicit

+ 6 - 1
qubes/vm/__init__.py

@@ -68,7 +68,7 @@ class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
         cls.__hooks__ = collections.defaultdict(list)
 
 
-class BaseVM(qubes.PropertyHolder, qubes.events.Emitter):
+class BaseVM(qubes.PropertyHolder):
     '''Base class for all VMs
 
     :param app: Qubes application context
@@ -90,6 +90,7 @@ class BaseVM(qubes.PropertyHolder, qubes.events.Emitter):
         self.devices = collections.defaultdict(list) if devices is None else devices
         self.tags = tags
 
+        self.events_enabled = False
         all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2))
         for key in list(kwargs.keys()):
             if not key in all_names:
@@ -101,6 +102,10 @@ class BaseVM(qubes.PropertyHolder, qubes.events.Emitter):
 
         super(BaseVM, self).__init__(xml, *args, **kwargs)
 
+        self.events_enabled = True
+        self.fire_event('property-load')
+
+
     def add_new_vm(self, vm):
         '''Add new Virtual Machine to colletion