From 7a58997f7aefd0724041f019f3b5bd547c977816 Mon Sep 17 00:00:00 2001 From: Vincent Vinet Date: Wed, 30 Sep 2009 12:40:42 +0000 Subject: WIP for running tutorials with the ProbeProxy --- 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. -- cgit v0.9.1