diff options
Diffstat (limited to 'tutorius/TProbe.py')
-rw-r--r-- | tutorius/TProbe.py | 313 |
1 files changed, 172 insertions, 141 deletions
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index ec0f9a3..f55547c 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -1,4 +1,5 @@ import logging +LOGGER = logging.getLogger("sugar.tutorius.TProbe") import os import gobject @@ -7,10 +8,12 @@ import dbus import dbus.service import cPickle as pickle -import sugar.tutorius.addon as addon -from sugar.tutorius.services import ObjectStore +from . import addon +from .services import ObjectStore +from .properties import TPropContainer +from .dbustools import remote_call, save_args import copy """ @@ -24,25 +27,15 @@ import copy -------------------- ---------- """ +#TODO Add stub error handling for remote calls in the classes so that it will +# be clearer how errors can be handled in the future. + class TProbe(dbus.service.Object): """ Tutorius Probe Defines an entry point for Tutorius into activities that allows performing actions and registering events onto an activity via a DBUS Interface. - - Exposes the following dbus methods: - void registered(string service) - string ping() -> status - string install(string action) -> address - void update(string address, string action_props) - void uninstall(string address) - string subscribe(string pickled_event) -> address - void unsubscribe(string address) - - Exposes the following dbus Events: - eventOccured(event): - """ def __init__(self, activity_name, activity): @@ -52,9 +45,9 @@ class TProbe(dbus.service.Object): @param activity_name unique activity_id @param activity activity reference, must be a gtk container """ - logging.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid()) - logging.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default())) - logging.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth())) + LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid()) + LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default())) + LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth())) # Moving the ObjectStore assignment here, in the meantime # the reference to the activity shouldn't be share as a # global variable but passed by the Probe to the objects @@ -157,38 +150,24 @@ class TProbe(dbus.service.Object): in_signature='s', out_signature='s') def subscribe(self, pickled_event): """ - Subscribe to a Gtk Widget Event - @param pickled_event string pickled Event + Subscribe to an Event + @param pickled_event string pickled EventFilter @return string unique name of registered event """ - event = pickle.loads(str(pickled_event)) - - # TODO elavoie 2009-07-25 Move to a reference counting implementation - # to avoid duplicating eventfilters when the event signature is the - # same - - # For now we will assume every probe is inserted in a GTK activity, - # however, in the future this should be moved in a subclass - eventfilter = addon.create("GtkWidgetEventFilter") - - # There might be a validation of the Address in source in the future - # and a partial resolution to extract the object_id from the address - eventfilter.object_id = event.source - - # TODO elavoie 2009-07-19 - # There should be a type translation from a tutorius type - # to a GTK type here - eventfilter.event_name = event.type + #TODO Perform event unmapping once Tutorials use abstract events + # instead of concrete EventFilters that are tied to their + # implementation. + eventfilter = pickle.loads(str(pickled_event)) # The callback uses the event defined previously and each # successive call to subscribe will register a different # callback that references a different event def callback(*args): - self.notify(event) + self.notify(eventfilter) eventfilter.install_handlers(callback, activity=self._activity) - name = self._generate_event_reference(event) + name = self._generate_event_reference(eventfilter) self._subscribedEvents[name] = eventfilter return name @@ -215,7 +194,12 @@ class TProbe(dbus.service.Object): # The actual method we will call on the probe to send events def notify(self, event): - self.eventOccured(pickle.dumps(event)) + LOGGER.debug("TProbe :: notify event %s", str(event)) + #Check that this event is even allowed + if event in self._subscribedEvents.values(): + self.eventOccured(pickle.dumps(event)) + else: + raise RuntimeWarning("Attempted to raise an unregistered event") # Return a unique name for this action def _generate_action_reference(self, action): @@ -232,11 +216,14 @@ class TProbe(dbus.service.Object): # Return a unique name for this event def _generate_event_reference(self, event): # TODO elavoie 2009-07-25 Should return a universal address - name = event.type - suffix = 1 + name = event.__class__.__name__ + #Keep the counter to avoid looping all the time + suffix = getattr(self, '_event_ref_suffix', 0 ) + 1 while self._subscribedEvents.has_key(name+str(suffix)): suffix += 1 + + #setattr(self, '_event_ref_suffix', suffix) return name + str(suffix) @@ -246,108 +233,98 @@ class ProbeProxy: It provides an object interface to the TProbe, which requires pickled strings, across a DBus communication. - - Public Methods: - ProbeProxy(string activityName) :: Constructor - string install(Action action) - void update(Action action) - void uninstall(Action action) - void uninstall_all() - string subscribe(Event event, callable callback) - void unsubscribe(Event event, callable callback) - void unsubscribe_all() """ def __init__(self, activityName): """ Constructor - @param activityName unique activity id + @param activityName unique activity id. Must be a valid dbus bus name. """ - logging.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid()) - logging.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default())) - logging.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth())) + LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid()) + LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default())) + LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth())) bus = dbus.SessionBus() self._object = bus.get_object(activityName, "/tutorius/Probe") self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface") self._actions = {} - self._events = {} # We keep those two data structures to be able to have multiple callbacks # for the same event and be able to remove them independently + # _subscribedEvents holds a list of callback addresses's for each event + # _registeredCallbacks holds the functions to call for each address self._subscribedEvents = {} self._registeredCallbacks = {} - def _handle_signal(pickled_event): - event = pickle.loads(str(pickled_event)) - if self._registeredCallbacks.has_key(event): - for callback in self._registeredCallbacks[event].itervalues(): - callback(event) - - self._object.connect_to_signal("eventOccured", _handle_signal, dbus_interface="org.tutorius.ProbeInterface") - + + self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface") + + def _handle_signal(self, pickled_event): + event = pickle.loads(str(pickled_event)) + LOGGER.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items())) + + LOGGER.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks)) + if self._registeredCallbacks.has_key(event): + for callback in self._registeredCallbacks[event].values(): + callback(event) + else: + for event in self._registeredCallbacks.keys(): + LOGGER.debug("==== %s", str(event._props.items())) + LOGGER.debug("ProbeProxy :: Event does not appear to be registered") + def isAlive(self): try: return self._probe.ping() == "alive" except: return False - def install(self, action): + def __update_action(self, action, address): + LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) + self._actions[action] = str(address) + + def __clear_action(self, action): + self._actions.pop(action, None) + + def install(self, action, block=False): """ Install an action on the TProbe's activity @param action Action to install + @param block Force a synchroneous dbus call if True @return None """ - address = str(self._probe.install(pickle.dumps(action))) - self._actions[action] = address + return remote_call(self._probe.install, (pickle.dumps(action),), + save_args(self.__update_action, action), + block=block) - def update(self, action): + def update(self, action, newaction, block=False): """ Update an already installed action's properties and run it again @param action Action to update + @param newaction Action to update it with + @param block Force a synchroneous dbus call if True @return None """ + #TODO review how to make this work well if not action in self._actions: raise RuntimeWarning("Action not installed") - return - self._probe.update(self._actions[action], pickle.dumps(action._props)) + #TODO Check error handling + return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block) - def uninstall(self, action): + def uninstall(self, action, block=False): """ Uninstall an installed action @param action Action to uninstall + @param block Force a synchroneous dbus call if True """ if action in self._actions: - self._probe.uninstall(self._actions.pop(action)) - - def uninstall_all(self): - """ - Uninstall all installed actions - @return None - """ - for action in self._actions.keys(): - self.uninstall(action) - - def subscribe(self, event, callback): - """ - Register an event listener - @param event Event to listen for - @param callback callable that will be called when the event occurs - @return address identifier used for unsubscribing - """ - # TODO elavoie 2009-07-25 When we will allow for patterns both - # for event types and sources, we will need to revise the lookup - # mecanism for which callback function to call - if (event, callback) in self._events: - raise RuntimeError("event already registered for callback") - return + remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block) + def __update_event(self, event, callback, address): + LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) # Since multiple callbacks could be associated to the same # event signature, we will store multiple callbacks # in a dictionary indexed by the unique address # given for this subscribtion and access this # dictionary from another one indexed by event - address = str(self._probe.subscribe(pickle.dumps(event))) - - self._events[(event, callback)] = address + address = str(address) # We use the event object as a key if not self._registeredCallbacks.has_key(event): @@ -371,19 +348,8 @@ class ProbeProxy: return address - def unsubscribe(self, event, callback): - """ - Unregister an event listener - @param address identifier given by subscribe() - @return None - """ - if not (event, callback) in self._events: - raise RuntimeWarning("callback/event not subscribed") - return - - address = self._events.pop((event, callback)) - self._probe.unsubscribe() - + def __clear_event(self, address): + LOGGER.debug("ProbeProxy :: Unregistering adress %s", str(address)) # Cleanup everything if self._subscribedEvents.has_key(address): event = self._subscribedEvents[address] @@ -396,22 +362,69 @@ class ProbeProxy: self._registeredCallbacks.pop(event) self._subscribedEvents.pop(address) + else: + LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address) + + def subscribe(self, event, callback, block=True): + """ + Register an event listener + @param event Event to listen for + @param callback callable that will be called when the event occurs + @param block Force a synchroneous dbus call if True (Not allowed yet) + @return address identifier used for unsubscribing + """ + LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event))) + if not block: + raise RuntimeError("This function does not allow non-blocking mode yet") - def unsubscribe_all(self): + # TODO elavoie 2009-07-25 When we will allow for patterns both + # for event types and sources, we will need to revise the lookup + # mecanism for which callback function to call + return remote_call(self._probe.subscribe, (pickle.dumps(event),), + save_args(self.__update_event, event, callback), + block=block) + + def unsubscribe(self, address, block=True): """ - Unregister all event listeners + Unregister an event listener + @param address identifier given by subscribe() + @param block Force a synchroneous dbus call if True @return None """ - for event, callback in self._events.keys(): - self.unsubscribe(event, callback) + LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address)) + if address in self._subscribedEvents.keys(): + remote_call(self._probe.unsubscribe, (address,), + return_cb=save_args(self.__clear_event, address), + block=block) + else: + LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address) + + def detach(self, block=False): + """ + Detach the ProbeProxy from it's TProbe. All installed actions and + subscribed events should be removed. + """ + for action_addr in self._actions.keys(): + self.uninstall(action_addr, block) + + for address in self._subscribedEvents.keys(): + self.unsubscribe(address, block) + class ProbeManager(object): """ The ProbeManager provides multiplexing across multiple activity ProbeProxies For now, it only handles one at a time, though. + Actually it doesn't do much at all. But it keeps your encapsulation happy """ - def __init__(self): + def __init__(self, proxy_class=ProbeProxy): + """Constructor + @param proxy_class Class to use for creating Proxies to activities. + The class should support the same interface as ProbeProxy. Exists + to make this class unit-testable by replacing the Proxy with a mock + """ + self._ProxyClass = proxy_class self._probes = {} self._current_activity = None @@ -427,9 +440,9 @@ class ProbeManager(object): def attach(self, activity_id): if activity_id in self._probes: raise RuntimeWarning("Activity already attached") - return - self._probes[activity_id] = ProbeProxy(activity_id) + self._probes[activity_id] = self._ProxyClass(activity_id) + #TODO what do we do with this? Raise something? if self._probes[activity_id].isAlive(): print "Alive!" else: @@ -438,48 +451,66 @@ class ProbeManager(object): def detach(self, activity_id): if activity_id in self._probes: probe = self._probes.pop(activity_id) - probe.unsubscribe_all() - probe.uninstall_all() - - def install(self, action): - if self.currentActivity: - return self._probes[self.currentActivity].install(action) - else: - raise RuntimeWarning("No activity attached") + probe.detach() + if self._current_activity == activity_id: + self._current_activity = None - def update(self, action): + def install(self, action, block=False): + """ + Install an action on the current activity + @param action Action to install + @param block Force a synchroneous dbus call if True + @return None + """ if self.currentActivity: - return self._probes[self.currentActivity].update(action) + return self._probes[self.currentActivity].install(action, block) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action): + def update(self, action, newaction, block=False): + """ + Update an already installed action's properties and run it again + @param action Action to update + @param newaction Action to update it with + @param block Force a synchroneous dbus call if True + @return None + """ if self.currentActivity: - return self._probes[self.currentActivity].uninstall(action) + return self._probes[self.currentActivity].update(action, newaction, block) else: raise RuntimeWarning("No activity attached") - def uninstall_all(self): + def uninstall(self, action, block=False): + """ + Uninstall an installed action + @param action Action to uninstall + @param block Force a synchroneous dbus call if True + """ if self.currentActivity: - return self._probes[self.currentActivity].uninstall_all() + return self._probes[self.currentActivity].uninstall(action, block) else: raise RuntimeWarning("No activity attached") def subscribe(self, event, callback): + """ + Register an event listener + @param event Event to listen for + @param callback callable that will be called when the event occurs + @return address identifier used for unsubscribing + """ if self.currentActivity: return self._probes[self.currentActivity].subscribe(event, callback) else: raise RuntimeWarning("No activity attached") - def unsubscribe(self, event, callback): - if self.currentActivity: - return self._probes[self.currentActivity].unsubscribe(event, callback) - else: - raise RuntimeWarning("No activity attached") - - def unsubscribe_all(self): + def unsubscribe(self, address): + """ + Unregister an event listener + @param address identifier given by subscribe() + @return None + """ if self.currentActivity: - return self._probes[self.currentActivity].unsubscribe_all() + return self._probes[self.currentActivity].unsubscribe(address) else: raise RuntimeWarning("No activity attached") |