Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--addons/readfile.py55
-rw-r--r--tests/probetests.py4
-rw-r--r--tutorius/TProbe.py169
-rw-r--r--tutorius/constraints.py2
-rw-r--r--tutorius/core.py53
5 files changed, 234 insertions, 49 deletions
diff --git a/addons/readfile.py b/addons/readfile.py
new file mode 100644
index 0000000..4aa054e
--- /dev/null
+++ b/addons/readfile.py
@@ -0,0 +1,55 @@
+# 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 2 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 os
+
+from sugar.tutorius.actions import Action
+from sugar.tutorius.properties import TFileProperty
+from sugar.tutorius.services import ObjectStore
+
+class ReadFile(Action):
+ filename = TFileProperty(None)
+
+ def __init__(self, filename=None):
+ """
+ Calls activity.read_file to load a specified state
+ @param filename Path to the file to read
+ """
+ Action.__init__(self)
+
+ if filename:
+ self.filename=filename
+
+ def do(self):
+ """
+ Perform the action, call read_file on the activity
+ """
+ if os.path.isfile(str(self.filename)):
+ ObjectStore().activity.read_file(self.filename)
+
+ def undo(self):
+ """
+ Not undoable
+ """
+ pass
+
+__action__ = {
+ "name" : "ReadFile",
+ "display_name" : "Read File",
+ "icon" : "message-bubble", #FIXME
+ "class" : ReadFile,
+ "mandatory_props" : ["filename"]
+}
diff --git a/tests/probetests.py b/tests/probetests.py
index 3a55f82..a440334 100644
--- a/tests/probetests.py
+++ b/tests/probetests.py
@@ -24,6 +24,7 @@ import gtk
import time
from dbus.mainloop.glib import DBusGMainLoop
+import dbus
from sugar.tutorius.TProbe import TProbe, ProbeProxy
@@ -45,8 +46,9 @@ class FakeActivity(object):
class ProbeTest(unittest.TestCase):
def test_ping(self):
- pid = os.fork()
m = DBusGMainLoop(set_as_default=True)
+ dbus.set_default_main_loop(m)
+
activity = FakeActivity()
probe = TProbe("localhost.unittest.ProbeTest", activity.top)
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index 6dd3afb..c4fae81 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -1,6 +1,10 @@
+import logging
+import os
+
import gobject
import dbus
+import dbus.service
import cPickle as pickle
import sugar.tutorius.addon as addon
@@ -10,10 +14,13 @@ from sugar.tutorius.services import ObjectStore
import copy
"""
-The TProbe module defines two connected classes, TProbe and ProbeProxy.
-
+ --------------------
+ | ProbeManager |
+ --------------------
+ |
+ V
-------------------- ----------
- | ProbeProxy |----- DBus ---->| TProbe |
+ | ProbeProxy |<---- DBus ---->| TProbe |
-------------------- ----------
"""
@@ -45,6 +52,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()))
# 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
@@ -230,8 +240,6 @@ class TProbe(dbus.service.Object):
return name + str(suffix)
-
-
class ProbeProxy:
"""
ProbeProxy is a Proxy class for connecting to a remote TProbe.
@@ -241,21 +249,28 @@ class ProbeProxy:
Public Methods:
ProbeProxy(string activityName) :: Constructor
- string install(Action action) -> action address
- void update(string address, Action action)
- void uninstall(string address)
- string subscribe(Event event, callable callback) -> event address
- void unsubscribe(string address)
+ 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
"""
+ 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()))
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
self._subscribedEvents = {}
@@ -269,31 +284,47 @@ class ProbeProxy:
self._object.connect_to_signal("eventOccured", _handle_signal, dbus_interface="org.tutorius.ProbeInterface")
+ def isAlive(self):
+ try:
+ return self._probe.ping() == "alive"
+ except:
+ return False
+
def install(self, action):
"""
Install an action on the TProbe's activity
@param action Action to install
- @return address identifier used for update and uninstall
+ @return None
"""
address = str(self._probe.install(pickle.dumps(action)))
- return address
+ self._actions[action] = address
def update(self, address, action):
"""
Update an already installed action's properties and run it again
- @param address identifier returned by the action install
- @param action Action to get properties from
+ @param action Action to update
@return None
"""
- self._probe.update(address, pickle.dumps(action._props))
+ if not action in self._actions:
+ raise RuntimeWarning("Action not installed")
+ return
+ self._probe.update(self._actions[action], pickle.dumps(action._props))
- def uninstall(self, address):
+ def uninstall(self, action):
+ """
+ Uninstall an installed action
+ @param action Action to uninstall
+ """
+ if action in self._actions:
+ self._probe.uninstall(self._actions.pop(action))
+
+ def uninstall_all(self):
"""
- Uninstall an installd action
- @param address identifier returned by the action install
+ Uninstall all installed actions
@return None
"""
- self._probe.uninstall(address)
+ for action in self._actions.keys():
+ self.uninstall(action)
def subscribe(self, event, callback):
"""
@@ -305,7 +336,9 @@ class ProbeProxy:
# 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
# Since multiple callbacks could be associated to the same
# event signature, we will store multiple callbacks
@@ -314,6 +347,8 @@ class ProbeProxy:
# dictionary from another one indexed by event
address = str(self._probe.subscribe(pickle.dumps(event)))
+ self._events[(event, callback)] = address
+
# We use the event object as a key
if not self._registeredCallbacks.has_key(event):
self._registeredCallbacks[event] = {}
@@ -336,13 +371,18 @@ class ProbeProxy:
return address
- def unsubscribe(self, address):
+ def unsubscribe(self, event, callback):
"""
Unregister an event listener
@param address identifier given by subscribe()
@return None
"""
- self._probe.unsubscribe(address)
+ if not (event, callback) in self._events:
+ raise RuntimeWarning("callback/event not subscribed")
+ return
+
+ address = self._events.pop((event, callback))
+ self._probe.unsubscribe()
# Cleanup everything
if self._subscribedEvents.has_key(address):
@@ -357,4 +397,89 @@ class ProbeProxy:
self._subscribedEvents.pop(address)
+ def unsubscribe_all(self):
+ """
+ Unregister all event listeners
+ @return None
+ """
+ for event, callback in self._events.keys():
+ self.unsubscribe(event, callback)
+
+class ProbeManager(object):
+ """
+ The ProbeManager provides multiplexing across multiple activity ProbeProxies
+
+ For now, it only handles one at a time, though.
+ """
+ def __init__(self):
+ self._probes = {}
+ self._current_activity = None
+
+ def setCurrentActivity(self, activity_id):
+ if not activity_id in self._probes:
+ raise RuntimeError("Activity not attached")
+ self._current_activity = activity_id
+
+ def getCurrentActivity(self):
+ return self._current_activity
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ if activity_id in self._probes:
+ raise RuntimeWarning("Activity already attached")
+ return
+
+ self._probes[activity_id] = ProbeProxy(activity_id)
+ if self._probes[activity_id].isAlive():
+ print "Alive!"
+ else:
+ print "FAil!"
+
+ 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")
+
+ def update(self, action):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].update(action)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall(self, action):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].uninstall(action)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall_all(self):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].uninstall_all()
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def subscribe(self, event, callback):
+ 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):
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe_all()
+ else:
+ raise RuntimeWarning("No activity attached")
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 36abdfb..2bc27aa 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -201,6 +201,8 @@ class FileConstraint(Constraint):
# TODO : Decide on the architecture for file retrieval on disk
# Relative paths? From where? Support macros?
#
+ if value is None:
+ return
if not os.path.isfile(value):
raise FileConstraintError("Non-existing file : %s"%value)
return
diff --git a/tutorius/core.py b/tutorius/core.py
index dd2435e..8ab0b51 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -21,14 +21,11 @@ This module contains the core classes for tutorius
"""
-import gtk
import logging
-import copy
import os
-from sugar.tutorius.dialog import TutoriusDialog
-from sugar.tutorius.gtkutils import find_widget
-from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.TProbe import ProbeManager
+from sugar.tutorius import addon
logger = logging.getLogger("tutorius")
@@ -37,7 +34,7 @@ class Tutorial (object):
Tutorial Class, used to run through the FSM.
"""
- def __init__(self, name, fsm,filename= None):
+ def __init__(self, name, fsm, filename=None):
"""
Creates an unattached tutorial.
"""
@@ -51,21 +48,25 @@ class Tutorial (object):
self.state = None
self.handlers = []
- self.activity = None
+ self._probeMgr = ProbeManager()
+ self._activity_id = None
#Rest of initialisation happens when attached
- def attach(self, activity):
+ probeManager = property(lambda self: self._probeMgr)
+ activityId = property(lambda self: self._activity_id)
+
+ def attach(self, activity_id):
"""
Attach to a running activity
- @param activity the activity to attach to
+ @param activity_id the id of the activity to attach to
"""
#For now, absolutely detach if a previous one!
- if self.activity:
+ if self._activity_id:
self.detach()
- self.activity = activity
- ObjectStore().activity = activity
- ObjectStore().tutorial = self
+ self._activity_id = activity_id
+ self._probeMgr.attach(activity_id)
+ self._probeMgr.currentActivity = activity_id
self._prepare_activity()
self.state_machine.set_state("INIT")
@@ -77,9 +78,10 @@ class Tutorial (object):
# Uninstall the whole FSM
self.state_machine.teardown()
- #FIXME There should be some amount of resetting done here...
- self.activity = None
-
+ #FIXME (Old) There should be some amount of resetting done here...
+ if not self._activity_id is None:
+ self._probeMgr.detach(self._activity_id)
+ self._activity_id = None
def set_state(self, name):
"""
@@ -112,9 +114,9 @@ class Tutorial (object):
#of the activity root directory
filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\
self.activity_init_state_filename
- if os.path.exists(filename):
- self.activity.read_file(filename)
-
+ readfile = addon.create("ReadFile", filename=filename)
+ if readfile:
+ self._probeMgr.install(self._activity_id, readfile)
class State(object):
"""
@@ -169,11 +171,10 @@ class State(object):
and then triggering the actions.
"""
for eventfilter in self._event_filters:
- eventfilter.install_handlers(self._event_filter_state_done_cb,
- activity=self.tutorial.activity)
+ self.tutorial.probeManager.subscribe(eventfilter, self._event_filter_state_done_cb )
for action in self._actions:
- action.do()
+ self.tutorial.probeManager.install(action)
def teardown(self):
"""
@@ -183,11 +184,11 @@ class State(object):
"""
# Remove the handlers for the all of the state's event filters
for event_filter in self._event_filters:
- event_filter.remove_handlers()
+ self.tutorial.probeManager.unsubscribe(event_filter, self._event_filter_state_done_cb )
# Undo all the actions related to this state
for action in self._actions:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
def _event_filter_state_done_cb(self, event_filter):
"""
@@ -349,7 +350,7 @@ class FiniteStateMachine(State):
self._fsm_setup_done = True
# Execute all the FSM level actions
for action in self.actions:
- action.do()
+ self.tutorial.probeManager.install(action)
# Then, we need to run the setup of the current state
self.current_state.setup()
@@ -414,7 +415,7 @@ class FiniteStateMachine(State):
self._fsm_teardown_done = True
# Undo all the FSM level actions here
for action in self.actions:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
# TODO : It might be nice to have a start() and stop() method for the
# FSM.