Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIgnacio Rodriguez <ignacio@sugarlabs.org>2013-12-24 15:43:34 (GMT)
committer Ignacio Rodriguez <ignacio@sugarlabs.org>2013-12-24 15:43:34 (GMT)
commitb7b9e614e0bf1c87fcdaeb782065f2b72cfb501a (patch)
tree6f2c8ef140fa82142a9dc6d1e2ffa2123e4ea291
parent40973e98e044510861f38cba4ab578d90f369bee (diff)
Add collaboration for sugargames, port of mesh.py (olpcgames) to gtk3
-rw-r--r--sugargame/__init__.py8
-rw-r--r--sugargame/dbusproxy.py100
-rw-r--r--sugargame/mesh.py435
-rw-r--r--sugargame/util.py68
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