diff --git a/shell/Owner.py b/shell/Owner.py index b6590589..b4781f9f 100644 --- a/shell/Owner.py +++ b/shell/Owner.py @@ -1,9 +1,11 @@ import os import random import base64 +import time import conf from sugar import env +import logging from sugar.p2p import Stream from sugar.presence import PresenceService from Friends import Friends @@ -15,7 +17,7 @@ class ShellOwner(object): """Class representing the owner of this machine/instance. This class runs in the shell and serves up the buddy icon and other stuff. It's the server portion of the Owner, paired with the client portion in Buddy.py.""" - def __init__(self): + def __init__(self, shell): profile = conf.get_profile() self._nick = profile.get_nick_name() @@ -34,6 +36,12 @@ class ShellOwner(object): self._friends = Friends() self._invites = Invites() + self._shell = shell + self._shell.connect('activity-changed', self.__activity_changed_cb) + self._last_activity_update = time.time() + self._pending_activity_update_timer = None + self._pending_activity_update = None + def get_friends(self): return self._friends @@ -43,10 +51,14 @@ class ShellOwner(object): def announce(self): # Create and announce our presence color = conf.get_profile().get_color() - props = { 'color': color.to_string() } + props = {'color':color.to_string()} + activity = self._shell.get_current_activity() + if activity is not None: + props['cur_activity':activity.get_id()] + self._last_activity_update = time.time() self._service = self._pservice.register_service(self._nick, PRESENCE_SERVICE_TYPE, properties=props) - print "Owner '%s' using port %d" % (self._nick, self._service.get_port()) + logging.debug("Owner '%s' using port %d" % (self._nick, self._service.get_port())) self._icon_stream = Stream.Stream.new_from_service(self._service) self._icon_stream.register_reader_handler(self._handle_buddy_icon_request, "get_buddy_icon") self._icon_stream.register_reader_handler(self._handle_invite, "invite") @@ -61,3 +73,31 @@ class ShellOwner(object): """XMLRPC method, called when the owner is invited to an activity.""" self._invites.add_invite(issuer, bundle_id, activity_id) return '' + + def __update_advertised_current_activity_cb(self): + self._last_activity_update = time.time() + self._pending_activity_update_timer = None + logging.debug("*** Updating current activity to %s" % self._pending_activity_update) + return False + + def __activity_changed_cb(self, shell, activity): + """Update our presence service with the latest activity, but no + more frequently than every 30 seconds""" + self._pending_activity_update = activity.get_id() + # If there's no pending update, we must not have updated it in the + # last 30 seconds (except for the initial update, hence we also check + # for the last update) + if not self._pending_activity_update_timer or time.time() - self._last_activity_update > 30: + self.__update_advertised_current_activity_cb() + return + + # If we have a pending update already, we have nothing left to do + if self._pending_activity_update_timer: + return + + # Otherwise, we start a timer to update the activity at the next + # interval, which should be 30 seconds from the last update, or if that + # is in the past already, then now + next = 30 - max(30, time.time() - self._last_activity_update) + self._pending_activity_update_timer = gobject.timeout_add(next * 1000, + self.__update_advertised_current_activity_cb) diff --git a/shell/PresenceService/PresenceService.py b/shell/PresenceService/PresenceService.py index 1534ae9f..c04084c2 100644 --- a/shell/PresenceService/PresenceService.py +++ b/shell/PresenceService/PresenceService.py @@ -56,6 +56,38 @@ class ServiceAdv(object): raise ValueError("Can't reset to resolve pending from resolved.") self._state = state +class RegisteredServiceType(object): + def __init__(self, stype): + self._stype = stype + self._refcount = 1 + + def get_type(self): + return self._stype + + def ref(self): + self._refcount += 1 + + def unref(self): + self._refcount -= 1 + return self._refcount + +def _txt_to_dict(txt): + """Convert an avahi-returned TXT record formatted + as nested arrays of integers (from dbus) into a dict + of key/value string pairs.""" + prop_dict = {} + props = avahi.txt_array_to_string_array(txt) + for item in props: + key = value = None + if '=' not in item: + # No = means a boolean value of true + key = item + value = True + else: + (key, value) = item.split('=') + prop_dict[key] = value + return prop_dict + _PRESENCE_SERVICE = "org.laptop.Presence" _PRESENCE_DBUS_INTERFACE = "org.laptop.Presence" @@ -169,8 +201,9 @@ class PresenceServiceDBusHelper(dbus.service.Object): return owner.object_path() @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="os", out_signature="o") - def joinActivity(self, activity_op, stype): + in_signature="os", out_signature="o", + sender_keyword="sender") + def joinActivity(self, activity_op, stype, sender): found_activity = None acts = self._parent.get_activities() for act in acts: @@ -179,29 +212,34 @@ class PresenceServiceDBusHelper(dbus.service.Object): break if not found_activity: raise NotFoundError("The activity %s was not found." % activity_op) - return self._parent.join_activity(found_activity, stype) + return self._parent.join_activity(found_activity, stype, sender) @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="ssa{ss}sis", out_signature="o") - def shareActivity(self, activity_id, stype, properties, address, port, domain): + in_signature="ssa{ss}sis", out_signature="o", + sender_keyword="sender") + def shareActivity(self, activity_id, stype, properties, address, port, + domain, sender=None): if not len(address): address = None service = self._parent.share_activity(activity_id, stype, properties, address, - port, domain) + port, domain, sender) return service.object_path() @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="ssa{ss}sis", out_signature="o") - def registerService(self, name, stype, properties, address, port, domain): + in_signature="ssa{ss}sis", out_signature="o", + sender_keyword="sender") + def registerService(self, name, stype, properties, address, port, domain, + sender=None): if not len(address): address = None service = self._parent.register_service(name, stype, properties, address, - port, domain) + port, domain, sender) return service.object_path() @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="o", out_signature="") - def unregisterService(self, service_op): + in_signature="o", out_signature="", + sender_keyword="sender") + def unregisterService(self, service_op, sender): found_serv = None services = self._parent.get_services() for serv in services: @@ -210,7 +248,7 @@ class PresenceServiceDBusHelper(dbus.service.Object): break if not found_serv: raise NotFoundError("The activity %s was not found." % service_op) - return self._parent.unregister_service(found_serv) + return self._parent.unregister_service(found_serv, sender) @dbus.service.method(_PRESENCE_DBUS_INTERFACE, in_signature="s", out_signature="") @@ -424,9 +462,10 @@ class PresenceService(object): key = (full_name, stype) if not self._services.has_key(key): objid = self._get_next_object_id() + props = _txt_to_dict(txt) service = Service.Service(self._bus_name, objid, name=full_name, stype=stype, domain=domain, address=address, port=port, - properties=txt, source_address=address, local=adv.is_local()) + properties=props, source_address=address, local=adv.is_local()) self._services[key] = service else: # Already tracking this service; likely we were the one that shared it @@ -588,19 +627,22 @@ class PresenceService(object): def _new_domain_cb_glue(self, interface, protocol, domain, flags=0): gobject.idle_add(self._new_domain_cb, interface, protocol, domain, flags) - def join_activity(self, activity, stype): + def join_activity(self, activity, stype, sender): services = activity.get_services_of_type(stype) if not len(services): - raise NotFoundError("The service type %s was not present within the activity %s" % (stype, activity.object_path())) + raise NotFoundError("The service type %s was not present within " \ + "the activity %s" % (stype, activity.object_path())) act_service = services[0] props = act_service.get_properties() color = activity.get_color() if color: props['color'] = color return self._share_activity(activity.get_id(), stype, properties, - act_service.get_address(), act_service.get_port(), act_service.get_domain()) + act_service.get_address(), act_service.get_port(), + act_service.get_domain(), sender) - def share_activity(self, activity_id, stype, properties=None, address=None, port=-1, domain=u"local"): + def share_activity(self, activity_id, stype, properties=None, address=None, + port=-1, domain=u"local", sender=None): """Convenience function to share an activity with other buddies.""" if not util.validate_activity_id(activity_id): raise ValueError("invalid activity id") @@ -621,14 +663,19 @@ class PresenceService(object): if color: properties['color'] = color - logging.debug('Share activity %s, type %s, address %s, port %d, properties %s' % (activity_id, stype, address, port, properties)) - return self.register_service(real_name, stype, properties, address, port, domain) + logging.debug('Share activity %s, type %s, address %s, port %d, " \ + "properties %s' % (activity_id, stype, address, port, + properties)) + return self.register_service(real_name, stype, properties, address, + port, domain, sender) - def register_service(self, name, stype, properties={}, address=None, port=-1, domain=u"local"): + def register_service(self, name, stype, properties={}, address=None, + port=-1, domain=u"local", sender=None): """Register a new service, advertising it to other Buddies on the network.""" (actid, person_name) = Service.decompose_service_name(name) if self.get_owner() and person_name != self.get_owner().get_name(): - raise RuntimeError("Tried to register a service that didn't have Owner nick as the service name!") + raise RuntimeError("Tried to register a service that didn't have" \ + " Owner nick as the service name!") if not domain or not len(domain): domain = u"local" if not port or port == -1: @@ -647,12 +694,14 @@ class PresenceService(object): objid = self._get_next_object_id() service = Service.Service(self._bus_name, objid, name=name, stype=stype, domain=domain, address=address, port=port, - properties=properties, source_address=None, local=True) + properties=properties, source_address=None, local=True, + local_publisher=sender) self._services[(name, stype)] = service port = service.get_port() logging.debug("PS: Will register service with name='%s', stype='%s'," \ - " domain='%s', address='%s', port=%d, info='%s'" % (name, stype, domain, address, port, info)) + " domain='%s', address='%s', port=%d, info='%s'" % (name, stype, + domain, address, port, info)) group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, dbus.String(name), dbus.String(stype), dbus.String(domain), dbus.String(""), # let Avahi figure the 'host' out dbus.UInt16(port), info) @@ -667,7 +716,10 @@ class PresenceService(object): self.register_service_type(stype) return service - def unregister_service(self, service): + def unregister_service(self, service, sender=None): + local_publisher = service.get_local_publisher() + if sender is not None and local_publisher != sender: + raise ValueError("Service was not not registered by requesting process!") group = service.get_avahi_entry_group() if not group: raise ValueError("Service was not a local service provided by this laptop!") @@ -678,9 +730,16 @@ class PresenceService(object): a certain mDNS service types.""" if type(stype) != type(u""): raise ValueError("service type must be a unicode string.") - if stype in self._registered_service_types: - return - self._registered_service_types.append(stype) + + # If we've already registered it as a service type, ref it and return + for item in self._registered_service_types: + if item.get_type() == stype: + item.ref() + return + + # Otherwise track this type now + obj = RegisteredServiceType(stype) + self._registered_service_types.append(obj) # Find unresolved services that match the service type # we're now interested in, and resolve them @@ -698,8 +757,15 @@ class PresenceService(object): """Stop tracking a certain mDNS service.""" if type(stype) != type(u""): raise ValueError("service type must be a unicode string.") - if stype in self._registered_service_types: - self._registered_service_types.remove(stype) + item = None + for item in self._registered_service_types: + if item.get_type() == stype: + break + # if it was found, unref it and possibly remove it + if item is not None: + if item.unref() <= 0: + self._registered_service_types.remove(item) + del item def main(): diff --git a/shell/PresenceService/Service.py b/shell/PresenceService/Service.py index 3c65d99d..0c236005 100644 --- a/shell/PresenceService/Service.py +++ b/shell/PresenceService/Service.py @@ -5,23 +5,6 @@ from sugar import util import dbus, dbus.service import random -def _txt_to_dict(txt): - """Convert an avahi-returned TXT record formatted - as nested arrays of integers (from dbus) into a dict - of key/value string pairs.""" - prop_dict = {} - props = avahi.txt_array_to_string_array(txt) - for item in props: - key = value = None - if '=' not in item: - # No = means a boolean value of true - key = item - value = True - else: - (key, value) = item.split('=') - prop_dict[key] = value - return prop_dict - def compose_service_name(name, activity_id): if type(name) == type(""): name = unicode(name) @@ -67,6 +50,11 @@ class ServiceDBusHelper(dbus.service.Object): self._object_path = object_path dbus.service.Object.__init__(self, bus_name, self._object_path) + @dbus.service.signal(SERVICE_DBUS_INTERFACE, + signature="as") + def PublishedValueChanged(self, keylist): + pass + @dbus.service.method(SERVICE_DBUS_INTERFACE, in_signature="", out_signature="a{sv}") def getProperties(self): @@ -90,14 +78,33 @@ class ServiceDBusHelper(dbus.service.Object): return pary @dbus.service.method(SERVICE_DBUS_INTERFACE, - in_signature="s", out_signature="s") + in_signature="s") def getPublishedValue(self, key): """Return the value belonging to the requested key from the service's TXT records.""" - value = self._parent.get_one_property(key) - if type(value) == type(True): - value = str(value) - return value + val = self._parent.get_one_property(key) + if not val: + raise KeyError("Value was not found.") + return val + + @dbus.service.method(SERVICE_DBUS_INTERFACE, + in_signature="", out_signature="a{sv}") + def getPublishedValues(self): + pary = {} + props = self._parent.get_properties() + for key, value in props.items(): + pary[key] = str(value) + return dbus.Dictionary(pary) + + @dbus.service.method(SERVICE_DBUS_INTERFACE, + sender_keyword="sender") + def setPublishedValue(self, key, value, sender): + self._parent.set_property(key, value, sender) + + @dbus.service.method(SERVICE_DBUS_INTERFACE, + in_signature="a{sv}", sender_keyword="sender") + def setPublishedValues(self, values, sender): + self._parent.set_properties(values, sender) class Service(object): @@ -105,7 +112,7 @@ class Service(object): service as advertised on the network.""" def __init__(self, bus_name, object_id, name, stype, domain=u"local", address=None, port=-1, properties=None, source_address=None, - local=False): + local=False, local_publisher=None): if not bus_name: raise ValueError("DBus bus name must be valid") if not object_id or type(object_id) != type(1): @@ -137,7 +144,7 @@ class Service(object): self._port = -1 self.set_port(port) self._properties = {} - self.set_properties(properties) + self._internal_set_properties(properties, first_time=True) self._avahi_entry_group = None self._local = local @@ -159,13 +166,19 @@ class Service(object): if self._properties.has_key(_ACTIVITY_ID_TAG): prop_actid = self._properties[_ACTIVITY_ID_TAG] if (prop_actid and not actid) or (prop_actid != actid): - raise ValueError("ActivityID property specified, but the service names's activity ID didn't match it: %s, %s" % (prop_actid, actid)) + raise ValueError("ActivityID property specified, but the " \ + "service names's activity ID didn't match it: %s," \ + " %s" % (prop_actid, actid)) self._activity_id = actid if actid and not self._properties.has_key(_ACTIVITY_ID_TAG): self._properties[_ACTIVITY_ID_TAG] = actid self._owner = None + # ID of the D-Bus connection that published this service, if any. + # We only let the local publisher modify the service. + self._local_publisher = local_publisher + # register ourselves with dbus self._object_id = object_id self._object_path = SERVICE_DBUS_OBJECT_PATH + str(self._object_id) @@ -182,6 +195,9 @@ class Service(object): raise RuntimeError("Can only set a service's owner once") self._owner = owner + def get_local_publisher(self): + return self._local_publisher + def is_local(self): return self._local @@ -207,19 +223,55 @@ class Service(object): properties.""" return self._properties - def set_properties(self, properties): + def set_property(self, key, value, sender=None): + """Set one service property""" + if sender is not None and self._local_publisher != sender: + raise ValueError("Service was not not registered by requesting process!") + + if type(key) != type(u""): + raise ValueError("Key must be a unicode string.") + if type(value) != type(u"") or type(value) != type(True): + raise ValueError("Key must be a unicode string or a boolean.") + + # Ignore setting the key to it's current value + if self._properties.has_key(key): + if self._properties[key] == value: + return + + # Blank value means remove key + remove = False + if type(value) == type(u"") and len(value) == 0: + remove = True + if type(value) == type(False) and value == False: + remove = True + + if remove: + # If the key wasn't present, return without error + if self._properties.has_key(key): + del self._properties[key] + else: + # Otherwise set it + if type(value) == type(True): + value = "" + self._properties[key] = value + self._dbus_helper.PublishedValueChanged(key) + + def set_properties(self, properties, sender=None): + """Set all service properties in one call""" + if sender is not None and self._local_publisher != sender: + raise ValueError("Service was not not registered by requesting process!") + self._internal_set_properties(properties) + + def _internal_set_properties(self, properties, first_time=False): """Set the service's properties from either an Avahi TXT record (a list of lists of integers), or a python dictionary.""" + if type(properties) != type({}): + raise ValueError("Properties must be a dictionary.") self._properties = {} - props = {} - if type(properties) == type([]): - props = _txt_to_dict(properties) - elif type(properties) == type({}): - props = properties # Set key/value pairs on internal property list - for key, value in props.items(): + for key, value in properties.items(): if len(key) == 0: continue tmp_key = key @@ -230,6 +282,9 @@ class Service(object): tmp_val = unicode(tmp_val) self._properties[tmp_key] = tmp_val + if not first_time: + self._dbus_helper.PublishedValueChanged(self._properties.keys()) + def get_type(self): """Return the service's service type.""" return self._stype diff --git a/shell/Shell.py b/shell/Shell.py index efe6a5a0..277e2f90 100755 --- a/shell/Shell.py +++ b/shell/Shell.py @@ -73,7 +73,7 @@ class Shell(gobject.GObject): PresenceService.start() self._pservice = PresenceService.get_instance() - self._owner = ShellOwner() + self._owner = ShellOwner(self) self._owner.announce() self._home_window.set_owner(self._owner) diff --git a/sugar/presence/Service.py b/sugar/presence/Service.py index eea7fef1..01169e4d 100644 --- a/sugar/presence/Service.py +++ b/sugar/presence/Service.py @@ -2,11 +2,29 @@ import gobject import dbus +def __one_dict_differs(dict1, dict2): + for key, value in dict1.items(): + if not dict2.has_key(key) or dict2[key] != value: + return True + return False + +def __dicts_differ(dict1, dict2): + if __one_dict_differs(dict1, dict2): + return True + if __one_dict_differs(dict2, dict1): + return True + return False + class Service(gobject.GObject): _PRESENCE_SERVICE = "org.laptop.Presence" _SERVICE_DBUS_INTERFACE = "org.laptop.Presence.Service" + __gsignals__ = { + 'published-value-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): gobject.GObject.__init__(self) self._object_path = object_path @@ -14,17 +32,40 @@ class Service(gobject.GObject): self._ps_del_object = del_obj_cb sobj = bus.get_object(self._PRESENCE_SERVICE, object_path) self._service = dbus.Interface(sobj, self._SERVICE_DBUS_INTERFACE) - self._service.connect_to_signal('PropertyChanged', self._property_changed_cb) + self._service.connect_to_signal('PropertyChanged', self.__property_changed_cb) + self._service.connect_to_signal('PublishedValueChanged', + self.__published_value_changed_cb) self._props = self._service.getProperties() + self._pubvals = self._service.getPublishedValues() def object_path(self): return self._object_path - def _property_changed_cb(self, prop_list): + def __property_changed_cb(self, prop_list): self._props = self._service.getProperties() def get_published_value(self, key): - return self._service.getPublishedValue(key) + return self._pubvals[key] + + def get_published_values(self): + self._pubvals = self._service.getPublishedValues() + + def set_published_value(self, key, value): + if self._pubvals.has_key(key): + if self._pubvals[key] == value: + return + self._pubvals[key] = value + self._service.setPublishedValue(key, value) + + def set_published_values(self, vals): + self._service.setPublishedValues(vals) + self._pubvals = vals + + def __published_value_changed_cb(self, keys): + oldvals = self._pubvals + self.get_published_values() + if __dicts_differ(oldvals, self._pubvals): + self.emit('published-value-changed', keys) def get_name(self): return self._props['name']