diff options
author | Ignacio Rodriguez <ignacio@sugarlabs.org> | 2013-12-24 15:43:34 (GMT) |
---|---|---|
committer | Ignacio Rodriguez <ignacio@sugarlabs.org> | 2013-12-24 15:43:34 (GMT) |
commit | b7b9e614e0bf1c87fcdaeb782065f2b72cfb501a (patch) | |
tree | 6f2c8ef140fa82142a9dc6d1e2ffa2123e4ea291 | |
parent | 40973e98e044510861f38cba4ab578d90f369bee (diff) |
Add collaboration for sugargames, port of mesh.py (olpcgames) to gtk3
-rw-r--r-- | sugargame/__init__.py | 8 | ||||
-rw-r--r-- | sugargame/dbusproxy.py | 100 | ||||
-rw-r--r-- | sugargame/mesh.py | 435 | ||||
-rw-r--r-- | sugargame/util.py | 68 |
4 files changed, 611 insertions, 0 deletions
diff --git a/sugargame/__init__.py b/sugargame/__init__.py index 439eb0c..4c083fe 100644 --- a/sugargame/__init__.py +++ b/sugargame/__init__.py @@ -1 +1,9 @@ __version__ = '1.1' + +(CAMERA_LOAD, CAMERA_LOAD_FAIL, CONNECT, PARTICIPANT_ADD, PARTICIPANT_REMOVE, + MESSAGE_UNI, MESSAGE_MULTI) = range(25, 32) + +(FILE_READ_REQUEST, FILE_WRITE_REQUEST) = range(2 ** 16, 2 ** 16 + 2) + +ACTIVITY = None +widget = WIDGET = None diff --git a/sugargame/dbusproxy.py b/sugargame/dbusproxy.py new file mode 100644 index 0000000..d210114 --- /dev/null +++ b/sugargame/dbusproxy.py @@ -0,0 +1,100 @@ +"""Spike test for a safer networking system for DBUS-based objects""" + +from sugargame import util +from dbus import proxies + + +def wrap(value, tube=None, path=None): + """Wrap object with any required pygame-side proxies""" + if isinstance(value, proxies._ProxyMethod): + return DBUSMethod(value, tube=tube, path=path) + elif isinstance(value, proxies._DeferredMethod): + value._proxy_method = DBUSMethod(value._proxy_method, tube=tube, + path=path) + return value + elif isinstance(value, proxies.ProxyObject): + return DBUSProxy(value, tube=tube, path=path) + else: + return value + + +class DBUSProxy(object): + """Proxy for the DBUS Proxy object""" + def __init__(self, proxy, tube=None, path=None): + self.__proxy = proxy + self.__tube = tube + self.__path = path + + def __getattr__(self, key): + """Retrieve attribute of given key""" + return wrap(getattr(self.__proxy, key)) + + def add_signal_receiver(self, callback, eventName, interface, path=None, + sender_keyword='sender'): + """ + Add a new signal handler (which will be called many times) + for given signal + """ + self.__tube.add_signal_receiver( + Callback(callback), + eventName, + interface, + path=path or self.__path, + sender_keyword=sender_keyword, + ) + + +class DBUSMethod(object): + """DBUS method which does callbacks in the Pygame (eventwrapper) thread""" + def __init__(self, proxy, tube, path): + self.__proxy = proxy + self.__tube = tube + self.__path = path + + def __call__(self, *args, **named): + """Perform the asynchronous call""" + callback, errback = named.get('reply_handler'), \ + named.get('error_handler') + if not callback: + raise TypeError("""Require a reply_handler named argument to do + any asynchronous call""") + else: + callback = Callback(callback) + if not errback: + errback = defaultErrback + else: + errback = Callback(errback) + named['reply_handler'] = callback + named['error_handler'] = errback + return self.__proxy(*args, **named) + + +def defaultErrback(error): + """Log the error to stderr/log""" + pass + + +class Callback(object): + """ + PyGTK-side callback which generates a CallbackResult to process on the + Pygame side + """ + def __init__(self, callable, callContext=None): + """Initialize the callback to process results""" + self.callable = callable + if callContext is None: + callContext = util.get_traceback(None) + self.callContext = callContext + + def __call__(self, *args, **named): + """PyGTK-side callback operation""" + from olpcgames import eventwrap + args = [wrap(a) for a in args] + named = dict([ + (k, wrap(v)) for k, v in named.items() + ]) + eventwrap.post( + eventwrap.CallbackResult( + self.callable, args, named, callContext=self.callContext + ) + ) diff --git a/sugargame/mesh.py b/sugargame/mesh.py new file mode 100644 index 0000000..1fbf7ca --- /dev/null +++ b/sugargame/mesh.py @@ -0,0 +1,435 @@ +import sugargame + +from sugar3.presence.tubeconn import TubeConnection +from dbus.gobject_service import ExportedGObject +from dbus.service import method, signal + +import telepathy +import sugar3.presence.presenceservice +import pygame.event as PEvent + + +class OfflineError(Exception): + """Raised when we cannot complete an operation due to being offline""" + +DBUS_IFACE = "org.laptop.games.pygame" +DBUS_PATH = "/org/laptop/games/pygame" +DBUS_SERVICE = None + + +### NEW PYGAME EVENTS ### + +CONNECT = sugargame.CONNECT +PARTICIPANT_ADD = sugargame.PARTICIPANT_ADD +PARTICIPANT_REMOVE = sugargame.PARTICIPANT_REMOVE +MESSAGE_UNI = sugargame.MESSAGE_UNI +MESSAGE_MULTI = sugargame.MESSAGE_MULTI + + +# Private objects for useful purposes! +pygametubes = [] +text_chan, tubes_chan = (None, None) +conn = None +initiating = False +joining = False + +connect_callback = None + + +def is_initiating(): + '''A version of is_initiator that's a bit less goofy, and can be used + before the Tube comes up.''' + global initiating + return initiating + + +def is_joining(): + '''Returns True if the activity was started up by means of the + Neighbourhood mesh view.''' + global joining + return joining + + +def set_connect_callback(cb): + '''Just the same as the Pygame event loop can listen for CONNECT, + this is just an ugly callback that the glib side can use to be aware + of when the Tube is ready.''' + global connect_callback + connect_callback = cb + + +def activity_shared(activity): + '''Called when the user clicks Share.''' + + global initiating + initiating = True + + _setup(activity) + + channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + if hasattr(channel, 'OfferDBusTube'): + channel.OfferDBusTube(DBUS_SERVICE, {}) + else: + channel.OfferTube(telepathy.TUBE_TYPE_DBUS, DBUS_SERVICE, {}) + + global connect_callback + if connect_callback is not None: + connect_callback() + + +def activity_joined(activity): + '''Called at the startup of our Activity, + when the user started it via Neighborhood intending to join + an existing activity.''' + + global initiating + global joining + initiating = False + joining = True + + _setup(activity) + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=_list_tubes_reply_cb, + error_handler=_list_tubes_error_cb) + + global connect_callback + if connect_callback is not None: + connect_callback() + + +def _getConn(activity): + global conn + if conn: + return conn + else: + if hasattr(activity.shared_activity, 'telepathy_conn'): + conn = activity.shared_activity.telepathy_conn + else: + pservice = _get_presence_service() + name, path = pservice.get_preferred_connection() + conn = telepathy.client.Connection(name, path) + return conn + + +def _setup(activity): + '''Determines text and tube channels for the current Activity. If no tube +channel present, creates one. Updates text_chan and tubes_chan. + +setup(sugar.activity.Activity, telepathy.client.Connection)''' + global text_chan, tubes_chan, DBUS_SERVICE + if not DBUS_SERVICE: + DBUS_SERVICE = activity.get_bundle_id() + if not activity.get_shared(): + raise "Failure" + + if hasattr(activity.shared_activity, 'telepathy_tubes_chan'): + _getConn(activity) + conn = activity.shared_activity.telepathy_conn + tubes_chan = activity.shared_activity.telepathy_tubes_chan + text_chan = activity.shared_activity.telepathy_text_chan + else: + bus_name, conn_path, channel_paths = \ + activity.shared_activity.get_channels() + _getConn(activity) + + # Work out what our room is called and whether we have Tubes already + room = None + tubes_chan = None + text_chan = None + for channel_path in channel_paths: + channel = telepathy.client.Channel(bus_name, channel_path) + htype, handle = channel.GetHandle() + if htype == telepathy.HANDLE_TYPE_ROOM: + room = handle + ctype = channel.GetChannelType() + if ctype == telepathy.CHANNEL_TYPE_TUBES: + tubes_chan = channel + elif ctype == telepathy.CHANNEL_TYPE_TEXT: + text_chan = channel + + if room is None: + raise "Failure" + + if text_chan is None: + raise "Failure" + + # Make sure we have a Tubes channel - PS doesn't yet provide one + if tubes_chan is None: + tubes_chan = conn.request_channel(telepathy.CHANNEL_TYPE_TUBES, + telepathy.HANDLE_TYPE_ROOM, room, True) + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', + new_tube_cb) + + return (text_chan, tubes_chan) + + +def new_tube_cb(id, initiator, type, service, params, state): + if (type == telepathy.TUBE_TYPE_DBUS and service == DBUS_SERVICE): + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + if hasattr(channel, 'AcceptDBusTube'): + channel.AcceptDBusTube(id) + else: + channel.AcceptTube(id) + + tube_conn = TubeConnection(conn, + tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, group_iface=text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + + global pygametubes, initiating + pygametubes.append(PygameTube(tube_conn, initiating, len(pygametubes))) + + +def _list_tubes_reply_cb(tubes): + for tube_info in tubes: + new_tube_cb(*tube_info) + + +def _list_tubes_error_cb(e): + pass + + +def lookup_buddy(dbus_handle, callback, errback=None): + """Do a lookup on the buddy information, callback with the information + + Calls callback(buddy) with the result of the lookup, or errback(error) with + a dbus description of the error in the lookup process. + + returns None + """ + + cs_handle = instance().tube.bus_name_to_handle[dbus_handle] + group = text_chan[telepathy.CHANNEL_INTERFACE_GROUP] + if not errback: + def errback(error): + pass + + def with_my_csh(my_csh): + + def _withHandle(handle): + """process the results of the handle values""" + # XXX: we're assuming that we have Buddy objects for all contacts - + # this might break when the server becomes scalable. + pservice = _get_presence_service() + name, path = pservice.get_preferred_connection() + callback(pservice.get_buddy_by_telepathy_handle(name, path, handle)) + if my_csh == cs_handle: + conn.GetSelfHandle(reply_handler=_withHandle, + error_handler=errback) + elif group.GetGroupFlags() & \ + telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + _withHandle(handle) + else: + handle = cs_handle + _withHandle(handle) + group.GetSelfHandle(reply_handler=with_my_csh, error_handler=errback) + + +def get_buddy(dbus_handle): + """DEPRECATED: Get a Buddy from a handle + + THIS API WAS NOT THREAD SAFE! It has been removed to avoid + extremely hard-to-debug failures in activities. Use lookup_buddy + instead! + + Code that read: + + get_buddy(handle) + doSomething(handle, buddy) + doSomethingElse(buddy) + + Translates to: + + def withBuddy(buddy): + doSomething(handle, buddy) + doSomethingElse(buddy) + lookup_buddy(handle, callback=withBuddy) + """ + raise RuntimeError( + """get_buddy is not thread safe and will crash your activity (hard). + Use lookup_buddy.""" + ) + + +def _get_presence_service(): + """Attempt to retrieve the presence service (check for offline condition) + + The presence service, when offline, has no preferred connection type, + so we check that before returning the object... + """ + + try: + pservice = sugar3.presence.presenceservice.get_instance() + try: + name, path = pservice.get_preferred_connection() + except (TypeError, ValueError): + raise OfflineError("""Unable to retrieve buddy information, + currently offline""") + else: + return pservice + except Exception: + pass + + +def instance(idx=0): + return pygametubes[idx] + + +class PygameTube(ExportedGObject): + '''The object whose instance is shared across D-bus + + Call instance() to get the instance of this object for + your activity service. + Its 'tube' property contains the underlying D-bus Connection. + ''' + def __init__(self, tube, is_initiator, tube_id): + super(PygameTube, self).__init__(tube, DBUS_PATH) + self.tube = tube + self.is_initiator = is_initiator + self.entered = False + self.ordered_bus_names = [] + PEvent.post(PEvent.Event(CONNECT, id=tube_id)) + + if not self.is_initiator: + self.tube.add_signal_receiver(self.new_participant_cb, + 'NewParticipants', DBUS_IFACE, path=DBUS_PATH) + self.tube.watch_participants(self.participant_change_cb) + self.tube.add_signal_receiver(self.broadcast_cb, 'Broadcast', + DBUS_IFACE, path=DBUS_PATH, sender_keyword='sender') + + def participant_change_cb(self, added, removed): + for handle, bus_name in added: + dbus_handle = self.tube.participants[handle] + self.ordered_bus_names.append(dbus_handle) + PEvent.post(PEvent.Event(PARTICIPANT_ADD, handle=dbus_handle)) + + for handle in removed: + dbus_handle = self.tube.participants[handle] + self.ordered_bus_names.remove(dbus_handle) + PEvent.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle)) + + if self.is_initiator: + if not self.entered: + # Initiator will broadcast a new ordered_bus_names each time + # a participant joins. + self.ordered_bus_names = [self.tube.get_unique_name()] + self.NewParticipants(self.ordered_bus_names) + + self.entered = True + + @signal(dbus_interface=DBUS_IFACE, signature='as') + def NewParticipants(self, ordered_bus_names): + '''This is the NewParticipants signal, + sent when the authoritative list of ordered_bus_names changes.''' + pass + + @signal(dbus_interface=DBUS_IFACE, signature='s') + def Broadcast(self, content): + '''This is the Broadcast signal; + it sends a message to all other activity participants.''' + pass + + @method(dbus_interface=DBUS_IFACE, in_signature='s', out_signature='', + sender_keyword='sender') + def Tell(self, content, sender=None): + '''This is the targeted-message interface; + called when a message is received that was sent directly to me.''' + PEvent.post(PEvent.Event(MESSAGE_UNI, handle=sender, content=content)) + + def broadcast_cb(self, content, sender=None): + '''This is the Broadcast callback, fired when someone sends a Broadcast + signal along the bus.''' + PEvent.post(PEvent.Event(MESSAGE_MULTI, handle=sender, content=content)) + + def new_participant_cb(self, new_bus_names): + '''This is the NewParticipants callback, + fired when someone joins or leaves.''' + if self.ordered_bus_names != new_bus_names: + self.ordered_bus_names = new_bus_names + + +def send_to(handle, content=""): + '''Sends the given message to the given buddy identified by handle.''' + remote_proxy = dbus_get_object(handle, DBUS_PATH) + remote_proxy.Tell(content, reply_handler=dbus_msg, error_handler=dbus_err) + + +def dbus_msg(): + pass + + +def dbus_err(e): + pass + + +def broadcast(content=""): + '''Sends the given message to all participants.''' + instance().Broadcast(content) + + +def my_handle(): + '''Returns the handle of this user + + Note, you can get a DBusException from this if you have + not yet got a unique ID assigned by the bus. You may need + to delay calling until you are sure you are connected. + ''' + return instance().tube.get_unique_name() + + +def is_initiator(): + '''Returns the handle of this user.''' + return instance().is_initiator + + +def get_participants(): + '''Returns the list of active participants, in order of arrival. + List is maintained by the activity creator; + if that person leaves it may not stay in sync.''' + + try: + return instance().ordered_bus_names[:] + except IndexError: + return [] # no participants yet, as we don't yet have a connection + + +def dbus_get_object(handle, path, warning=True): + '''Get a D-bus object from another participant + + Note: this *must* be called *only* from the GTK mainloop, calling + it from Pygame will cause crashes! If you are *sure* you only ever + want to call methods on this proxy from GTK, you can use + warning=False to silence the warning log message. + ''' + return instance().tube.get_object(handle, path) + + +def get_object(handle, path): + '''Get a D-BUS proxy object from another participant for use in Pygame + + This is how you can communicate with other participants using + arbitrary D-bus objects without having to manage the participants + yourself. You can use the returned proxy's methods from Pygame, + with your callbacks occuring in the Pygame thread, rather than + in the DBUS/GTK event loop. + + Simply define a D-bus class with an interface and path that you + choose; when you want a reference to the corresponding remote + object on a participant, call this method. + + returns an sugargame.dbusproxy.DBUSProxy() object wrapping + the DBUSProxy object. + + The dbus_get_object_proxy name is deprecated + ''' + from sugargame import dbusproxy + return dbusproxy.DBUSProxy( + instance().tube.get_object(handle, path), + tube=instance().tube, + path=path + ) + +dbus_get_object_proxy = get_object diff --git a/sugargame/util.py b/sugargame/util.py new file mode 100644 index 0000000..63685c8 --- /dev/null +++ b/sugargame/util.py @@ -0,0 +1,68 @@ +"""Abstraction layer for working outside the Sugar environment""" +import traceback +import cStringIO +import os +import os.path +from sugar3.activity.activity import get_bundle_path + +NON_SUGAR_ROOT = '~/.sugar/default/olpcgames' + + +def get_activity_root(): + """Return the activity root for data storage operations + + If the activity is present, returns the activity's root, + otherwise returns NON_SUGAR_ROOT as the directory. + """ + import sugargame + if sugargame.ACTIVITY: + return sugargame.ACTIVITY.get_activity_root() + else: + return os.path.expanduser(NON_SUGAR_ROOT) + + +def data_path(file_name): + """Return the full path to a file in the data sub-directory of the bundle""" + return os.path.join(get_bundle_path(), 'data', file_name) + + +def tmp_path(file_name): + """Return the full path to a file in the temporary directory""" + return os.path.join(get_activity_root(), 'tmp', file_name) + + +def get_traceback(error): + """Get formatted traceback from current exception + + error -- Exception instance raised + + Attempts to produce a 10-level traceback as a string + that you can log off. Use like so: + + try: + doSomething() + except Exception, err: + log.error( + '''Failure during doSomething with X,Y,Z parameters: %s''', + util.get_traceback(err), + ) + """ + if error is None: + error = [] + for (f, l, func, statement) in traceback.extract_stack()[:-2]: + if statement: + statement = ': %s' % (statement) + if func: + error.append('%s.%s (%s)%s' % (f, func, l, statement)) + else: + error.append('%s (%s)%s' % (f, l, statement)) + return "\n".join(error) + else: + exception = str(error) + file_path = cStringIO.StringIO() + try: + traceback.print_exc(limit=10, file=file_path) + exception = file.getvalue() + finally: + file.close() + return exception |