Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius/engine.py
diff options
context:
space:
mode:
Diffstat (limited to 'tutorius/engine.py')
-rw-r--r--tutorius/engine.py265
1 files changed, 221 insertions, 44 deletions
diff --git a/tutorius/engine.py b/tutorius/engine.py
index c945e49..198fa11 100644
--- a/tutorius/engine.py
+++ b/tutorius/engine.py
@@ -1,4 +1,21 @@
+# 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
@@ -7,7 +24,21 @@ 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):
"""
@@ -21,12 +52,27 @@ class TutorialRunner(object):
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
- self._sEvents = set() #Subscribed Events
#Cached objects
- self._actions = {}
+ 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
@@ -34,86 +80,216 @@ class TutorialRunner(object):
#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.enterState(self._tutorial.END)
self._teardownState()
self._state = None
+ self._runner_state = RUNNER_STATE_IDLE
def _handleEvent(self, next_state, event):
- #FIXME sanity check, log event that was not installed and ignore
- self.enterState(next_state)
+ # 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 in self._actions.values():
- self._pM.uninstall(action)
- self._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 in self._sEvents:
- self._pM.unsubscribe(event)
- self._sEvents.clear()
-
- def _setupState(self):
- if self._state is None:
- raise RuntimeError("Attempting to setupState without a state")
-
- # Handle the automatic event
- state_name = self._state
-
- self._actions = self._tutorial.get_action_dict(self._state)
+ 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)
- for (event, next_state) in transitions.values():
- if isinstance(event, AutomaticTransitionEvent):
- state_name = next_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)
- self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state)))
+ def _setupState(self):
+ if self._state is None:
+ raise RuntimeError("Attempting to setupState without a state")
- for action in self._actions.values():
- self._pM.install(action)
+ actions = self._tutorial.get_action_dict(self._state)
+
+ if len(actions) == 0:
+ self.all_actions_installed()
+ return
- return state_name
+ 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 until
- no automatic transition are found and will wait for an external
- event to occur.
+ 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 state, actions and events from the previous
+ 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
- # Recursive base case
- if state_name == self._state:
- #Nothing to do
- return
+ 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 = state_name
-
- # Recursively call the enterState in case there was an automatic
- # transition in the state definition
- self.enterState(self._setupState())
-
-
+ self._state = real_next_state
+ self._setupState()
class Engine:
"""
@@ -137,7 +313,8 @@ class Engine:
if self._tutorial:
self.stop()
- self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager)
+ 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()