From 0928acd3d2d845fd6cd28cd848652aedecae0bdb Mon Sep 17 00:00:00 2001 From: Vincent Vinet Date: Tue, 13 Oct 2009 01:28:23 +0000 Subject: run tutorials through the dbus service, currently for calculate only heh --- diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index ec0f9a3..6c0883a 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -10,7 +10,9 @@ import cPickle as pickle import sugar.tutorius.addon as addon from sugar.tutorius.services import ObjectStore +from sugar.tutorius.properties import TPropContainer +from sugar.tutorius.dbustools import remote_call, save_args import copy """ @@ -157,38 +159,21 @@ 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 + 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 +200,14 @@ 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)) + logging.debug("TProbe :: notify event %s", str(event)) + #HACK: reinstanciate the event with it's properties, to clear + # any internal state from getting pickled + if isinstance(event, TPropContainer): + newevent = type(event)(**event._props) + else: + newevent = event + self.eventOccured(pickle.dumps(newevent)) # Return a unique name for this action def _generate_action_reference(self, action): @@ -232,7 +224,7 @@ 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 + name = event.__class__.__name__ suffix = 1 while self._subscribedEvents.has_key(name+str(suffix)): @@ -270,36 +262,51 @@ class ProbeProxy: 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)) + logging.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items())) + + logging.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks)) + if self._registeredCallbacks.has_key(event): + for callback in self._registeredCallbacks[event].itervalues(): + callback(event) + else: + for event in self._registeredCallbacks.keys(): + logging.debug("==== %s", str(event._props.items())) + logging.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): + 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 @return None """ - address = str(self._probe.install(pickle.dumps(action))) - self._actions[action] = address + remote_call(self._probe.install, (pickle.dumps(action),), + save_args(self.__update_action, action), + block=block) - def update(self, action): + def update(self, action, block=False): """ Update an already installed action's properties and run it again @param action Action to update @@ -308,46 +315,32 @@ class ProbeProxy: if not action in self._actions: raise RuntimeWarning("Action not installed") return - self._probe.update(self._actions[action], pickle.dumps(action._props)) + remote_call(self._probe.update, (self._actions[action], pickle.dumps(action._props)), block=block) - def uninstall(self, action): + def uninstall(self, action, block=False): """ Uninstall an installed action @param action Action to uninstall """ if action in self._actions: - self._probe.uninstall(self._actions.pop(action)) + remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block) - def uninstall_all(self): + def uninstall_all(self, block=False): """ 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 + self.uninstall(action, block) + def __update_event(self, event, callback, address): + logging.debug("ProbeProxy :: Registered event %s with address %s", str(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 +364,7 @@ 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): # Cleanup everything if self._subscribedEvents.has_key(address): event = self._subscribedEvents[address] @@ -397,13 +378,43 @@ class ProbeProxy: self._subscribedEvents.pop(address) - def unsubscribe_all(self): + 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 + @return address identifier used for unsubscribing + """ + if not block: + raise RuntimeError("This function does not allow non-blocking mode yet") + + # 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=False): + """ + Unregister an event listener + @param address identifier given by subscribe() + @return None + """ + if address in self._subscribedEvents.keys(): + remote_call(self._probe.unsubscribe, (address,), + return_cb=save_args(self.__clear_event, address), + block=block) + else: + logging.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address) + + def unsubscribe_all(self, block=False): """ Unregister all event listeners @return None """ - for event, callback in self._events.keys(): - self.unsubscribe(event, callback) + for address in self._subscribedEvents.keys(): + self.unsubscribe(address, block) class ProbeManager(object): """ @@ -471,9 +482,9 @@ class ProbeManager(object): else: raise RuntimeWarning("No activity attached") - def unsubscribe(self, event, callback): + def unsubscribe(self, address): if self.currentActivity: - return self._probes[self.currentActivity].unsubscribe(event, callback) + return self._probes[self.currentActivity].unsubscribe(address) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/core.py b/tutorius/core.py index 8ab0b51..f51c5fb 100644 --- a/tutorius/core.py +++ b/tutorius/core.py @@ -25,6 +25,7 @@ import logging import os from sugar.tutorius.TProbe import ProbeManager +from sugar.tutorius.dbustools import save_args from sugar.tutorius import addon logger = logging.getLogger("tutorius") @@ -92,17 +93,6 @@ class Tutorial (object): self.state_machine.set_state(name) - # Currently unused -- equivalent function is in each state - def _eventfilter_state_done(self, eventfilter): - """ - Callback handler for eventfilter to notify - when we must go to the next state. - """ - #XXX Tests should be run here normally - - #Swith to the next state pointed by the eventfilter - self.set_state(eventfilter.get_next_state()) - def _prepare_activity(self): """ Prepare the activity for the tutorial by loading the saved state and @@ -116,7 +106,8 @@ class Tutorial (object): self.activity_init_state_filename readfile = addon.create("ReadFile", filename=filename) if readfile: - self._probeMgr.install(self._activity_id, readfile) + self._probeMgr.install(readfile) + self._probeMgr.uninstall(readfile) class State(object): """ @@ -146,7 +137,9 @@ class State(object): # Unused for now #self.tests = [] - self._event_filters = event_filter_list or [] + self._transitions= dict(event_filter_list or []) + + self._installedEvents = set() self.tutorial = tutorial @@ -170,8 +163,8 @@ class State(object): Install the state itself, by first registering the event filters and then triggering the actions. """ - for eventfilter in self._event_filters: - self.tutorial.probeManager.subscribe(eventfilter, self._event_filter_state_done_cb ) + for (event, next_state) in self._transitions.items(): + self._installedEvents.add(self.tutorial.probeManager.subscribe(event, save_args(self._event_filter_state_done_cb, next_state ))) for action in self._actions: self.tutorial.probeManager.install(action) @@ -183,24 +176,25 @@ class State(object): removing dialogs that were displayed, removing highlights, etc... """ # Remove the handlers for the all of the state's event filters - for event_filter in self._event_filters: - self.tutorial.probeManager.unsubscribe(event_filter, self._event_filter_state_done_cb ) + while len(self._installedEvents) > 0: + self.tutorial.probeManager.unsubscribe(self._installedEvents.pop()) # Undo all the actions related to this state for action in self._actions: self.tutorial.probeManager.uninstall(action) - def _event_filter_state_done_cb(self, event_filter): + def _event_filter_state_done_cb(self, next_state, event): """ Callback for event filters. This function needs to inform the tutorial that the state is over and tell it what is the next state. - @param event_filter The event filter that was called + @param next_state The next state for the transition + @param event The event that occured """ # Run the tests here, if need be # Warn the higher level that we wish to change state - self.tutorial.set_state(event_filter.get_next_state()) + self.tutorial.set_state(next_state) # Model manipulation # These functions are used to simplify the creation of states @@ -230,19 +224,21 @@ class State(object): Removes all the action associated with this state. A cleared state will not do anything when entered or exited. """ + #FIXME What if the action is currently installed? self._actions = [] - def add_event_filter(self, event_filter): + def add_event_filter(self, event, next_state): """ Adds an event filter that will cause a transition from this state. The same event filter may not be added twice. - @param event_filter The new event filter that will trigger a transition + @param event The event that will trigger a transition + @param next_state The state to which the transition will lead @return True if added, False otherwise """ - if event_filter not in self._event_filters: - self._event_filters.append(event_filter) + if event not in self._transitions.keys(): + self._transitions[event]=next_state return True return False @@ -250,7 +246,7 @@ class State(object): """ @return The list of event filters associated with this state. """ - return self._event_filters + return self._transitions.items() def clear_event_filters(self): """ @@ -258,7 +254,7 @@ class State(object): was just cleared will become a sink and will be the end of the tutorial. """ - self._event_filters = [] + self._transitions = {} class FiniteStateMachine(State): """ @@ -471,9 +467,9 @@ class FiniteStateMachine(State): #TODO : Move this code inside the State itself - we're breaking # encap :P - for event_filter in st._event_filters: - if event_filter.get_next_state() == state_name: - st._event_filters.remove(event_filter) + for event, state in st._transitions: + if state == state_name: + del st._transitions[event] # Remove the state from the dictionary del self._states[state_name] @@ -491,8 +487,8 @@ class FiniteStateMachine(State): next_states = set() - for event_filter in state._event_filters: - next_states.add(event_filter.get_next_state()) + for event, state in state._transitions: + next_states.add(state) return tuple(next_states) @@ -514,9 +510,9 @@ class FiniteStateMachine(State): states = [] # Walk through the list of states for st in self._states.itervalues(): - for event_filter in st._event_filters: - if event_filter.get_next_state() == state_name: - states.append(event_filter.get_next_state()) + for event, state in st._transitions: + if state == state_name: + states.append(state) continue return tuple(states) diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py new file mode 100644 index 0000000..ce28d98 --- /dev/null +++ b/tutorius/dbustools.py @@ -0,0 +1,24 @@ +import logging + +def save_args(callable, *xargs, **xkwargs): + def __call(*args, **kwargs): + kw = dict() + kw.update(kwargs) + kw.update(xkwargs) + return callable(*(xargs+args), **kw) + return __call + +def ignore(*args): + logging.debug("Unhandled asynchronous dbus call response with arguments: %s", str(args)) + +def logError(error): + logging.error("Unhandled asynchronous dbus call error: %s", error) + +def remote_call(callable, args, return_cb=None, error_cb=None, block=False): + reply_cb = return_cb or ignore + errhandler_cb = error_cb or logError + if block: + return reply_cb(callable(*args)) + else: + callable(*args, reply_handler=reply_cb, error_handler=errhandler_cb) + diff --git a/tutorius/engine.py b/tutorius/engine.py index 57c08e4..f695de6 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -1,6 +1,8 @@ +import logging import dbus.mainloop.glib -from sugar.tutorius.TProbe import ProbeProxy -import sugar.tutorius.addon as addon +from jarabe.model import shell + +from sugar.tutorius.bundler import TutorialStore class Engine: """ @@ -10,30 +12,40 @@ class Engine: def __init__(self): # FIXME Probe management should be in the probe manager dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - self._probe = ProbeProxy("org.laptop.Calculate") - self._bm = None + #FIXME shell.get_model() will only be useful in the shell process + self._shell = shell.get_model() + self._tutorial = None def launch(self, tutorialID): """ Launch a tutorial @param tutorialID unique tutorial identifier used to retrieve it from the disk """ - if self._bm == None: - self._bm = addon.create("BubbleMessage") - self._bm.position = (300,300) - self._bm.message = "Tutorial Started" + if self._tutorial: + self._tutorial.detach() + self._tutorial = None + + store = TutorialStore() + + #FIXME Cleanup the handling of 'aliases' + activity = self._shell.get_active_activity() + self._tutorial = store.load_tutorial(tutorialID, bundle_path=activity.get_bundle_path()) + self._tutorial.attach("org.laptop.Calculate") +# if activity in self._activities: +# self._tutorial.attach(self._activities[activity]) +# else: +# raise RuntimeError("Current activity alias unknown") - self._probe.install(self._bm) def stop(self): """ Stop the current tutorial """ - self._probe.uninstall(self._bm) + self._tutorial.detach() + self._tutorial = None def pause(self): """ Interrupt the current tutorial and save its state in the journal """ - self._bm.message = "Tutorial State would be saved" - self._probe.update(self._bm) + raise NotImplementedError("Unable to store tutorial state") diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py index 91b11f4..71349ed 100644 --- a/tutorius/linear_creator.py +++ b/tutorius/linear_creator.py @@ -45,7 +45,7 @@ class LinearCreator(object): """ self.current_actions.append(action) - def event(self, event_filter): + def event(self, event): """ Adds a transition to another state. When executing this, all the actions previously called will be bundled in a single state, with the exit @@ -58,9 +58,8 @@ class LinearCreator(object): # Set the next state name - there is no way the caller should have # to deal with that. next_state_name = "State %d" % (self.nb_state+1) - event_filter.set_next_state(next_state_name) state = State(self.state_name, action_list=self.current_actions, - event_filter_list=[event_filter]) + event_filter_list=[(event, next_state_name),]) self.state_name = next_state_name self.nb_state += 1 diff --git a/tutorius/properties.py b/tutorius/properties.py index d9c68b1..8593e00 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -95,6 +95,19 @@ class TPropContainer(object): """ return object.__getattribute__(self, "_props").keys() + # Providing the hash methods necessary to use TPropContainers + # in a dictionary, according to their properties + def __hash__(self): + try: + #Return a hash of properties (key, value) sorted by key + return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0]))))) + except TypeError: + #FIXME For list properties (and maybe others), hashing will fail, fallback to id + return id(self) + + def __eq__(self, e2): + return self._props.items() == e2._props.items() + class TutoriusProperty(object): """ The base class for all actions' properties. The interface is the following : diff --git a/tutorius/service.py b/tutorius/service.py index 61c6526..c52b7cd 100644 --- a/tutorius/service.py +++ b/tutorius/service.py @@ -1,6 +1,8 @@ from engine import Engine import dbus +from dbustools import remote_call + _DBUS_SERVICE = "org.tutorius.Service" _DBUS_PATH = "/org/tutorius/Service" _DBUS_SERVICE_IFACE = "org.tutorius.Service" @@ -57,13 +59,13 @@ class ServiceProxy: self._service = dbus.Interface(self._object, _DBUS_SERVICE_IFACE) def launch(self, tutorialID): - self._service.launch(tutorialID) + remote_call(self._service.launch, (tutorialID, ), block=False) def stop(self): - self._service.stop() + remote_call(self._service.stop, (), block=False) def pause(self): - self._service.pause() + remote_call(self._service.pause, (), block=False) if __name__ == "__main__": import dbus.mainloop.glib -- cgit v0.9.1