# Copyright (C) 2009, Tutorius.org # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 1 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import logging from heapq import heappush, heappop import dbus.mainloop.glib from jarabe.model import shell from sugar.bundle.activitybundle import ActivityBundle from .vault import Vault from .TProbe import ProbeManager from .dbustools import save_args from .tutorial import Tutorial, AutomaticTransitionEvent from .translator import ResourceTranslator # Priority values for queuable messages STOP_MSG_PRIORITY = 5 EVENT_NOTIFICATION_MSG_PRIORITY = 10 # List of runner states RUNNER_STATE_IDLE = "idle" RUNNER_STATE_SETUP_ACTIONS = "setup_actions" RUNNER_STATE_SETUP_EVENTS = "setup_events" RUNNER_STATE_AWAITING_NOTIFICATIONS = "awaiting_notification" RUNNER_STATE_UNINSTALLING_ACTIONS = "uninstalling_actions" RUNNER_STATE_UNSUBSCRIBING_EVENTS = "unsubscribing_events" LOGGER = logging.getLogger("sugar.tutorius.engine") class TutorialRunner(object): """ Driver for the execution of one tutorial """ def __init__(self, tutorial, probeManager): """Constructor @param tutorial Tutorial to execute @param probeManager probeManager to use """ self._tutorial = tutorial self._pM = probeManager # The tutorial runner's state. For more details, see : # https://docs.google.com/Doc?docid=0AVT_nzmWT2B2ZGN3dDd2MzRfNTBka3J4bW1kaA&hl=en self._runner_state = RUNNER_STATE_IDLE # The message queue is a heap, so only heap operations should be done # on it like heappush, heappop, etc... # The stocked messages are actually a list of parameters that should be # passed to the appropriate function. E.g. When raising an event notification, # it saves the (next_state, event) in the message. self._message_queue = [] #State self._state = None #Cached objects self._installed_actions = {} self._installation_errors = {} # Subscribed Events self._subscribed_events = {} self._subscription_errors = {} #Temp FIX until event/actions have an activity id self._activity_id = None #Temp FIX until event, actions have an activity id def setCurrentActivity(self): self._pM.currentActivity = self._activity_id ########################################################################### # Incoming messages def start(self): self.setCurrentActivity() #Temp Hack until activity in events/actions self.enterState(self._tutorial.INIT) def stop(self): if self._runner_state == RUNNER_STATE_SETUP_ACTIONS or \ self._runner_state == RUNNER_STATE_SETUP_EVENTS: heappush(self._message_queue, (STOP_MSG_PRIORITY, None)) elif self._runner_state != RUNNER_STATE_IDLE: self._execute_stop() def action_installed(self, action_name, address): LOGGER.debug("TutorialRunner :: Action %s received address %s"%(action_name, address)) self._installed_actions[action_name] = address # Verify if we just completed the installation of the actions for this state self._verify_action_install_state() def install_error(self, action_name, action, exception): # TODO : Fix this as it doesn't warn the user about the problem or anything LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action), str(exception))) self._installation_errors[action_name] = exception self._verify_action_install_state() def event_subscribed(self, event_name, event_address): LOGGER.debug("TutorialRunner :: Event %s was subscribed to, located at address %s"%(event_name, event_address)) self._subscribed_events[event_name] = event_address # Verify if we just completed the subscription of all the events for this state self._verify_event_install_state() def subscribe_error(self, event_name, exception): # TODO : Do correct error handling here LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception))) self._subscription_errors[event_name] = exception # Verify if we just completed the subscription of all the events for this state self._verify_event_install_state() def all_actions_installed(self): self._runner_state = RUNNER_STATE_SETUP_EVENTS # Process the messages that might have been stored self._process_pending_messages() # If we processed a message that changed the runner state, we need to stop # processing if self._runner_state != RUNNER_STATE_SETUP_EVENTS: return # Start subscribing to events transitions = self._tutorial.get_transition_dict(self._state) # If there are no transitions, raise the All Events Subscribed message if len(transitions) == 0: self.all_events_subscribed() return # Send all the event registration for (event_name, (event, next_state)) in transitions.items(): self._pM.subscribe(event, save_args(self._handleEvent, next_state), save_args(self.event_subscribed, event_name), save_args(self.subscribe_error, event_name)) def all_events_subscribed(self): self._runner_state = RUNNER_STATE_AWAITING_NOTIFICATIONS self._process_pending_messages() ########################################################################### # Helper functions def _execute_stop(self): self.setCurrentActivity() #Temp Hack until activity in events/actions self._teardownState() self._state = None self._runner_state = RUNNER_STATE_IDLE def _handleEvent(self, next_state, event): # Look if we are actually receiving notifications if self._runner_state == RUNNER_STATE_AWAITING_NOTIFICATIONS: LOGGER.debug("TutorialRunner :: Received event notification in AWAITING_NOTIFICATIONS for %s"%str(event)) transitions = self._tutorial.get_transition_dict(self._state) for (this_event, this_next_state_name) in transitions.values(): if event == this_event and next_state == this_next_state_name: self.enterState(next_state) break elif self._runner_state == RUNNER_STATE_SETUP_EVENTS: LOGGER.debug("TutorialRunner :: Queuing event notification to go to state %s"%next_state) # Push the message on the queue heappush(self._message_queue, (EVENT_NOTIFICATION_MSG_PRIORITY, (next_state, event))) # Ignore the message for all other states def _teardownState(self): if self._state is None: #No state, no teardown return self._remove_installed_actions() self._remove_subscribed_events() def _remove_installed_actions(self): #Clear the current actions for (action_name, action_address) in self._installed_actions.items(): LOGGER.debug("TutorialRunner :: Uninstalling action %s with address %s"%(action_name, action_address)) self._pM.uninstall(action_address) self._installed_actions.clear() self._installation_errors.clear() def _remove_subscribed_events(self): #Clear the EventFilters for (event_name, event_address) in self._subscribed_events.items(): self._pM.unsubscribe(event_address) self._subscribed_events.clear() self._subscription_errors.clear() def _verify_action_install_state(self): actions = self._tutorial.get_action_dict(self._state) # Do the check to see if we have finished installing all the actions by either having # received a address for it or an error message install_complete = True for (this_action_name, this_action) in actions.items(): if not this_action_name in self._installed_actions.keys() and \ not this_action_name in self._installation_errors.keys(): # There's at least one uninstalled action, so we still wait install_complete = False break if install_complete: LOGGER.debug("TutorialRunner :: All actions installed!") # Raise the All Actions Installed event for the TutorialRunner state self.all_actions_installed() def _verify_event_install_state(self): transitions = self._tutorial.get_transition_dict(self._state) # Check to see if we completed all the event subscriptions subscribe_complete = True for (this_event_name, (this_event, next_state)) in transitions.items(): if not this_event_name in self._subscribed_events.keys() and \ not this_event_name in self._subscription_errors.keys(): subscribe_complete = False break if subscribe_complete: LOGGER.debug("TutorialRunner : Subscribed to all events!") self.all_events_subscribed() def _process_pending_messages(self): while len(self._message_queue) != 0: (priority, message) = heappop(self._message_queue) if priority == STOP_MSG_PRIORITY: LOGGER.debug("TutorialRunner :: Stop message taken from message queue") # We can safely ignore the rest of the events self._message_queue = [] self._execute_stop() elif priority == EVENT_NOTIFICATION_MSG_PRIORITY: LOGGER.debug("TutorialRunner :: Handling stored event notification for next_state %s"%message[0]) self._handle_event(*message) def _setupState(self): if self._state is None: raise RuntimeError("Attempting to setupState without a state") actions = self._tutorial.get_action_dict(self._state) if len(actions) == 0: self.all_actions_installed() return for (action_name, action) in actions.items(): LOGGER.debug("TutorialRunner :: Installed action %s"%(action_name)) self._pM.install(action, save_args(self.action_installed, action_name), save_args(self.install_error, action_name)) def enterState(self, state_name): """ Starting from the state_name, the runner execute states from the tutorial until no automatic transitions are found and will wait for an external event to occur. When entering the sate, actions and events from the previous state are respectively uninstalled and unsubscribed and actions and events from the state_name will be installed and subscribed. @param state_name The name of the state to enter in """ self.setCurrentActivity() #Temp Hack until activity in events/actions # Set the runner state to actions setup self._runner_state = RUNNER_STATE_SETUP_ACTIONS real_next_state = None skip_to_state = state_name # As long as we do have automatic transitions, skip them to go to the # next state while skip_to_state != real_next_state: real_next_state = skip_to_state transitions = self._tutorial.get_transition_dict(skip_to_state) for (event, next_state) in transitions.values(): if isinstance(event, AutomaticTransitionEvent): skip_to_state = next_state break self._teardownState() self._state = real_next_state self._setupState() class Engine: """ Driver for the execution of tutorials """ def __init__(self, probeManager=None): """Constructor @param probeManager (optional) ProbeManager instance to use """ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) #FIXME shell.get_model() will only be useful in the shell process self._shell = shell.get_model() self._probeManager = probeManager or ProbeManager() self._tutorial = None def launch(self, tutorialID): """ Launch a tutorial @param tutorialID unique tutorial identifier used to retrieve it from the disk """ if self._tutorial: self.stop() translator_decorator = ResourceTranslator(self._probeManager, tutorialID) self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), translator_decorator) #Get the active activity from the shell activity = self._shell.get_active_activity() #TProbes automatically use the bundle id, available from the ActivityBundle bundle = ActivityBundle(activity.get_bundle_path()) self._tutorial._activity_id = bundle.get_bundle_id() #HACK until we have activity id's in action/events self._tutorial.start() def stop(self, tutorialID=None): """ Stop the current tutorial """ if tutorialID is None: logging.warning( "stop() without a tutorialID will become deprecated") self._tutorial.stop() self._tutorial = None def pause(self, tutorialID=None): """ Interrupt the current tutorial and save its state in the journal """ if tutorialID is None: logging.warning( \ "pause() without a tutorialID will become deprecated") raise NotImplementedError("Unable to store tutorial state")