From 7035c6e281332b1688c59877ac78516a0dd4635d Mon Sep 17 00:00:00 2001 From: JCTutorius Date: Fri, 06 Nov 2009 01:05:12 +0000 Subject: Merge branch 'master' of gitorious@git.sugarlabs.org:tutorius/mainline --- (limited to 'tutorius') diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index dbab86a..0c79690 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -38,45 +38,65 @@ class TProbe(dbus.service.Object): a DBUS Interface. """ - def __init__(self, activity_name, activity): + def __init__(self, activity, service_proxy=None): """ Create and register a TProbe for an activity. - @param activity_name unique activity_id @param activity activity reference, must be a gtk container + @param service_proxy A Service proxy object to do the registering """ - LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid()) - LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default())) - LOGGER.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 # that requires it self._activity = activity + + if service_proxy == None: + from .service import ServiceProxy + + self._service_proxy = service_proxy or ServiceProxy() ObjectStore().activity = activity - self._activity_name = activity_name + self._activity_name = activity.get_bundle_id() + self._unique_id = activity.get_id() + + LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", self._activity_name, os.getpid()) + LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default())) + LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth())) self._session_bus = dbus.SessionBus() # Giving a new name because _name is already used by dbus - self._name2 = dbus.service.BusName(activity_name, self._session_bus) - dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe") + self._name2 = dbus.service.BusName(self._activity_name, self._session_bus) + dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe/"+str(self._unique_id)) # Add the dictionary we will use to store which actions and events # are known self._installedActions = {} self._subscribedEvents = {} + LOGGER.debug("TProbe :: registering '%s' with unique_id '%s'", self._activity_name, activity.get_id()) + self._service_proxy.register_probe(self._activity_name, self._unique_id) + + + def start(self): """ Optional method to call if the probe is not inserted into an existing activity. Starts a gobject mainloop """ mainloop = gobject.MainLoop() - print "Starting Probe for " + self._activity_name mainloop.run() + def stop(self): + """ + Clean up the probe when finished. Should be called just + before a process ends + """ + from .service import ServiceProxy + LOGGER.debug("TProbe :: unregistering '%s' with unique_id '%s'", self._activity_name, self._unique_id) + ServiceProxy().unregister_probe(self._unique_id) + @dbus.service.method("org.tutorius.ProbeInterface", in_signature='s', out_signature='') def registered(self, service): @@ -234,16 +254,17 @@ class ProbeProxy: It provides an object interface to the TProbe, which requires pickled strings, across a DBus communication. """ - def __init__(self, activityName): + def __init__(self, activityName, unique_id): """ Constructor - @param activityName unique activity id. Must be a valid dbus bus name. + @param activityName generic activity name. Must be a valid dbus bus name. + @param unique_id unique id specific to an instance of an activity """ LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid()) LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default())) LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth())) bus = dbus.SessionBus() - self._object = bus.get_object(activityName, "/tutorius/Probe") + self._object = bus.get_object(activityName, "/tutorius/Probe/"+str(unique_id)) self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface") self._actions = {} @@ -397,7 +418,7 @@ class ProbeProxy: return_cb=save_args(self.__clear_event, address), block=block) else: - LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address) + LOGGER.debug("ProbeProxy :: unsubscribe address %s failed : not registered", address) def detach(self, block=False): """ @@ -418,16 +439,22 @@ class ProbeManager(object): For now, it only handles one at a time, though. Actually it doesn't do much at all. But it keeps your encapsulation happy """ + _LOGGER = logging.getLogger("sugar.tutorius.ProbeManager") + def __init__(self, proxy_class=ProbeProxy): """Constructor @param proxy_class Class to use for creating Proxies to activities. The class should support the same interface as ProbeProxy. Exists to make this class unit-testable by replacing the Proxy with a mock """ + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + self._ProxyClass = proxy_class self._probes = {} self._current_activity = None + ProbeManager._LOGGER.debug("__init__()") + def setCurrentActivity(self, activity_id): if not activity_id in self._probes: raise RuntimeError("Activity not attached") @@ -437,23 +464,6 @@ class ProbeManager(object): 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") - - self._probes[activity_id] = self._ProxyClass(activity_id) - #TODO what do we do with this? Raise something? - 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.detach() - if self._current_activity == activity_id: - self._current_activity = None def install(self, action, block=False): """ @@ -463,7 +473,7 @@ class ProbeManager(object): @return None """ if self.currentActivity: - return self._probes[self.currentActivity].install(action, block) + return self._first_proxy(self.currentActivity).install(action, block) else: raise RuntimeWarning("No activity attached") @@ -476,7 +486,7 @@ class ProbeManager(object): @return None """ if self.currentActivity: - return self._probes[self.currentActivity].update(action, newaction, block) + return self._first_proxy(self.currentActivity).update(action, newaction, block) else: raise RuntimeWarning("No activity attached") @@ -487,7 +497,7 @@ class ProbeManager(object): @param block Force a synchroneous dbus call if True """ if self.currentActivity: - return self._probes[self.currentActivity].uninstall(action, block) + return self._first_proxy(self.currentActivity).uninstall(action, block) else: raise RuntimeWarning("No activity attached") @@ -499,7 +509,7 @@ class ProbeManager(object): @return address identifier used for unsubscribing """ if self.currentActivity: - return self._probes[self.currentActivity].subscribe(event, callback) + return self._first_proxy(self.currentActivity).subscribe(event, callback) else: raise RuntimeWarning("No activity attached") @@ -510,7 +520,67 @@ class ProbeManager(object): @return None """ if self.currentActivity: - return self._probes[self.currentActivity].unsubscribe(address) + return self._first_proxy(self.currentActivity).unsubscribe(address) else: raise RuntimeWarning("No activity attached") + def register_probe(self, process_name, unique_id): + """ Adds a probe to the known probes, to be used by a tutorial. + + A generic name for a process (like an Activity) is passed + so that the execution of a tutorial will use that generic + name. However, a unique id is also passed to differentiate + between many instances of the same process. + + @param process_name The generic name of a process + @param unique_id The unique identification associated to this + process + """ + ProbeManager._LOGGER.debug("register_probe(%s,%s)", process_name, unique_id) + if process_name not in self._probes: + self._probes[process_name] = [(unique_id,self._ProxyClass(process_name, unique_id))] + else: + self._probes[process_name].append((unique_id,self._ProxyClass(process_name, unique_id))) + + + def unregister_probe(self, unique_id): + """ Remove a probe from the known probes. + + @param unique_id The unique identification associated to this + process + """ + ProbeManager._LOGGER.debug("unregister_probe(%s)", unique_id) + for process_name, proxies in self._probes.items(): + for id, proxy in proxies: + if unique_id == id: + proxy.detach() + proxies.remove((id,proxy)) + if len(proxies) == 0: + self._probes.pop(process_name) + + def get_registered_probes_list(self, process_name=None): + if process_name == None: + probe_list = [] + for probes in self._probes.itervalues(): + probe_list.extend(probes) + return probe_list + else: + if process_name in self._probes: + return self._probes[process_name] + else: + return [] + + + + def _first_proxy(self, process_name): + """ + Returns the oldest probe connected under the process_name + @param process_name The generic process name under which the probe + is connected + """ + if process_name in self._probes: + return self._probes[process_name][0][1] + else: + raise RuntimeWarning("No activity attached under '%s'", process_name) + + diff --git a/tutorius/engine.py b/tutorius/engine.py index e77a018..c945e49 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -4,17 +4,130 @@ 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 + + +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 + + #State + self._state = None + self._sEvents = set() #Subscribed Events + + #Cached objects + self._actions = {} + + #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 + + def start(self): + self.setCurrentActivity() #Temp Hack until activity in events/actions + self.enterState(self._tutorial.INIT) + + def stop(self): + self.setCurrentActivity() #Temp Hack until activity in events/actions + self.enterState(self._tutorial.END) + self._teardownState() + self._state = None + + def _handleEvent(self, next_state, event): + #FIXME sanity check, log event that was not installed and ignore + self.enterState(next_state) + + def _teardownState(self): + if self._state is None: + #No state, no teardown + return + + #Clear the current actions + for action in self._actions.values(): + self._pM.uninstall(action) + self._actions = {} + + #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) + transitions = self._tutorial.get_transition_dict(self._state) + + for (event, next_state) in transitions.values(): + if isinstance(event, AutomaticTransitionEvent): + state_name = next_state + break + + self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state))) + + for action in self._actions.values(): + self._pM.install(action) + + return state_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. + + When entering the state, 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 + + # Recursive base case + if state_name == self._state: + #Nothing to do + return + + 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()) + + + class Engine: """ Driver for the execution of tutorials """ - def __init__(self): - # FIXME Probe management should be in the probe manager + 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): @@ -22,25 +135,33 @@ class Engine: @param tutorialID unique tutorial identifier used to retrieve it from the disk """ if self._tutorial: - self._tutorial.detach() - self._tutorial = None + self.stop() + + self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager) #Get the active activity from the shell activity = self._shell.get_active_activity() - self._tutorial = Vault.loadTutorial(tutorialID) - #TProbes automatically use the bundle id, available from the ActivityBundle bundle = ActivityBundle(activity.get_bundle_path()) - self._tutorial.attach(bundle.get_bundle_id()) - def stop(self): + 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 """ - self._tutorial.detach() + if tutorialID is None: + logging.warning( + "stop() without a tutorialID will become deprecated") + self._tutorial.stop() self._tutorial = None - def pause(self): + 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") diff --git a/tutorius/service.py b/tutorius/service.py index eb246a1..11a94a5 100644 --- a/tutorius/service.py +++ b/tutorius/service.py @@ -2,6 +2,9 @@ import dbus from .engine import Engine from .dbustools import remote_call +from .TProbe import ProbeManager +import logging +LOGGER = logging.getLogger("sugar.tutorius.service") _DBUS_SERVICE = "org.tutorius.Service" _DBUS_PATH = "/org/tutorius/Service" @@ -19,11 +22,13 @@ class Service(dbus.service.Object): self._engine = None + self._probeMgr = ProbeManager() + def start(self): """ Start the service itself """ # For the moment there is nothing to do - pass + LOGGER.debug("Service.start()") @dbus.service.method(_DBUS_SERVICE_IFACE, @@ -33,7 +38,7 @@ class Service(dbus.service.Object): @param tutorialID unique tutorial identifier used to retrieve it from the disk """ if self._engine == None: - self._engine = Engine() + self._engine = Engine(self._probeMgr) self._engine.launch(tutorialID) @dbus.service.method(_DBUS_SERVICE_IFACE, @@ -50,6 +55,35 @@ class Service(dbus.service.Object): """ self._engine.pause() + @dbus.service.method(_DBUS_SERVICE_IFACE, + in_signature="ss", out_signature="") + def register_probe(self, process_name, unique_id): + """ Adds a probe to the known probes, to be used by a tutorial. + + A generic name for a process (like an Activity) is passed + so that the execution of a tutorial will use that generic + name. However, a unique id is also passed to differentiate + between many instances of the same process. + + @param process_name The generic name of a process + @param unique_id The unique identification associated to this + process + """ + LOGGER.debug("Service.register_probe(%s,%s)", process_name, unique_id) + self._probeMgr.register_probe(process_name, unique_id) + + @dbus.service.method(_DBUS_SERVICE_IFACE, + in_signature="s", out_signature="") + def unregister_probe(self, unique_id): + """ Remove a probe from the known probes. + + @param process_name The generic name of a process + @param unique_id The unique identification associated to this + process + """ + LOGGER.debug("Service.unregister_probe(%s)", unique_id) + self._probeMgr.unregister_probe(unique_id) + class ServiceProxy: """ Proxy to connect to the Service object, abstracting the DBus interface""" @@ -74,6 +108,33 @@ class ServiceProxy: """ remote_call(self._service.pause, (), block=False) + def register_probe(self, process_name, unique_id): + """ Adds a probe to the known probes, to be used by a tutorial. + + A generic name for a process (like an Activity) is passed + so that the execution of a tutorial will use that generic + name. However, a unique id is also passed to differentiate + between many instances of the same process. + + @param process_name The generic name of a process + @param unique_id The unique identification associated to this + process + """ + remote_call(self._service.register_probe, (process_name,unique_id), block=False) + + def unregister_probe(self, unique_id): + """ Remove a probe from the known probes. + + @param process_name The generic name of a process + @param unique_id The unique identification associated to this + process + """ + # We make it synchronous because otherwise on closing, + # activities kill the dbus session bus too fast for the + # asynchronous call to be completed + self._service.unregister_probe(unique_id) + + if __name__ == "__main__": import dbus.mainloop.glib import gobject -- cgit v0.9.1