Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/shell
diff options
context:
space:
mode:
authorDan Williams <dcbw@localhost.localdomain>2006-07-18 14:51:04 (GMT)
committer Dan Williams <dcbw@localhost.localdomain>2006-07-18 14:51:04 (GMT)
commit79007cfb0af7dd3454ff75796203d0586f00f9c8 (patch)
tree26f99b28e79e46bdb51657458d1d9ae9b4385507 /shell
parent46d2f1c532278bee5b5c8550d83d4ceb064ccc95 (diff)
Add most bits of new presence service code
Diffstat (limited to 'shell')
-rw-r--r--shell/PresenceService/Activity.py29
-rw-r--r--shell/PresenceService/Buddy.py253
-rw-r--r--shell/PresenceService/PresenceService.py259
-rw-r--r--shell/PresenceService/Service.py452
4 files changed, 993 insertions, 0 deletions
diff --git a/shell/PresenceService/Activity.py b/shell/PresenceService/Activity.py
new file mode 100644
index 0000000..5fbe8a5
--- /dev/null
+++ b/shell/PresenceService/Activity.py
@@ -0,0 +1,29 @@
+import dbus
+
+
+class ActivityDBusHelper(dbus.service.Object):
+ def __init__(self, parent, bus_name, object_path):
+ self._parent = parent
+ self._bus_name = bus_name
+ self._object_path = object_path
+ dbus.service.Object.__init__(self, bus_name, self._object_path)
+
+
+class Activity(object):
+ def __init__(self, bus_name, object_id, activity_id):
+ self._activity_id = activity_id
+
+ self._buddies = []
+ self._services = {} # service type -> Service
+
+ self._object_id = object_id
+ self._object_path = "/org/laptop/Presence/Activities/%d" % self._object_id
+ self._dbus_helper = ActivityDBusHelper(self, bus_name, self._object_path)
+
+ def get_id(self):
+ return self._activity_id
+
+ def get_service_of_type(self, stype):
+ if self._services.has_key(stype):
+ return self._services[stype]
+ return None
diff --git a/shell/PresenceService/Buddy.py b/shell/PresenceService/Buddy.py
new file mode 100644
index 0000000..c3d17e1
--- /dev/null
+++ b/shell/PresenceService/Buddy.py
@@ -0,0 +1,253 @@
+import base64
+import logging
+
+import gtk
+import gobject
+
+from sugar.p2p import Stream
+from sugar.p2p import network
+from sugar.presence import Service
+
+PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp"
+BUDDY_DBUS_INTERFACE = "org.laptop.Presence.Buddy"
+
+class NotFoundError(Exception):
+ pass
+
+class BuddyDBusHelper(dbus.service.Object):
+ def __init__(self, parent, bus_name, object_path):
+ self._parent = parent
+ self._bus_name = bus_name
+ self._object_path = object_path
+ dbus.service.Object.__init__(self, bus_name, self._object_path)
+
+ @dbus.service.signal(BUDDY_DBUS_INTERFACE)
+ def ServiceAppeared(self, object_path):
+ pass
+
+ @dbus.service.signal(BUDDY_DBUS_INTERFACE)
+ def ServiceDisappeared(self, object_path):
+ pass
+
+ @dbus.service.signal(BUDDY_DBUS_INTERFACE)
+ def IconChanged(self):
+ pass
+
+ @dbus.service.signal(BUDDY_DBUS_INTERFACE)
+ def JoinedActivity(self, object_path):
+ pass
+
+ @dbus.service.signal(BUDDY_DBUS_INTERFACE)
+ def LeftActivity(self, object_path):
+ pass
+
+ @dbus.service.method(BUDDY_DBUS_INTERFACE,
+ in_signature="", out_signature="ay")
+ def getIcon(self):
+ icon = self._parent.get_icon()
+ if not icon:
+ return ""
+ return icon
+
+ @dbus.service.method(BUDDY_DBUS_INTERFACE,
+ in_signature="", out_signature="o")
+ def getServiceOfType(self, stype):
+ service = self._parent.get_service_of_type(stype)
+ if not service:
+ raise NotFoundError("Not found")
+ return service
+
+ @dbus.service.method(BUDDY_DBUS_INTERFACE,
+ in_signature="", out_signature="ao")
+ def getJoinedActivities(self):
+ acts = []
+ return acts
+
+ @dbus.service.method(BUDDY_DBUS_INTERFACE,
+ in_signature="", out_signature="a{sv}")
+ def getProperties(self):
+ props = {}
+ props['name'] = self._parent.get_nick_name()
+ props['ip4_address'] = self._parent.get_address()
+ props['owner'] = self._parent.is_owner()
+ return props
+
+
+class Buddy(object):
+ """Represents another person on the network and keeps track of the
+ activities and resources they make available for sharing."""
+
+ def __init__(self, bus_name, object_id, service):
+ if not bus_name:
+ raise ValueError("DBus bus name must be valid")
+ if not object_id or type(object_id) != type(1):
+ raise ValueError("object id must be a valid number")
+
+ self._services = {}
+ self._activities = {}
+
+ self._nick_name = service.get_name()
+ self._address = service.get_publisher_address()
+ self._valid = False
+ self._icon = None
+ self._icon_tries = 0
+ self._owner = False
+ self.add_service(service)
+
+ self._object_id = object_id
+ self._object_path = "/org/laptop/Presence/Buddies/%d" % self._object_id
+ self._dbus_helper = BuddyDBusHelper(self, bus_name, self._object_path)
+
+ def object_path(self):
+ return self._object_path
+
+ def _request_buddy_icon_cb(self, result_status, response, user_data):
+ """Callback when icon request has completed."""
+ icon = response
+ service = user_data
+ if result_status == network.RESULT_SUCCESS:
+ if icon and len(icon):
+ icon = base64.b64decode(icon)
+ print "Buddy icon for '%s' is size %d" % (self._nick_name, len(icon))
+ self._set_icon(icon)
+
+ if (result_status == network.RESULT_FAILED or not icon) and self._icon_tries < 3:
+ self._icon_tries = self._icon_tries + 1
+ print "Failed to retrieve buddy icon for '%s' on try %d of %d" % (self._nick_name, \
+ self._icon_tries, 3)
+ gobject.timeout_add(1000, self._request_buddy_icon, service)
+ return False
+
+ def _request_buddy_icon(self, service):
+ """Contact the buddy to retrieve the buddy icon."""
+ buddy_stream = Stream.Stream.new_from_service(service, start_reader=False)
+ writer = buddy_stream.new_writer(service)
+ success = writer.custom_request("get_buddy_icon", self._request_buddy_icon_cb, service)
+ if not success:
+ del writer, buddy_stream
+ gobject.timeout_add(1000, self._request_buddy_icon, service)
+ return False
+
+ def add_service(self, service):
+ """Adds a new service to this buddy's service list, returning
+ True if the service was successfully added, and False if it was not."""
+ if service.get_name() != self._nick_name:
+ return False
+ publisher_addr = service.get_publisher_address()
+ if publisher_addr != self._address:
+ logging.error('Service publisher and buddy address doesnt match: %s %s' % (publisher_addr, self._address))
+ return False
+ stype = service.get_type()
+ if stype in self._services.keys():
+ return False
+ self._services[stype] = service
+ if self._valid:
+ self._dbus_helper.ServiceAppeared(dbus.ObjectPath(service.object_path()))
+
+ # If this is the first service we've seen that's owned by
+ # a particular activity, send out the 'joined-activity' signal
+ actid = service.get_activity_id()
+ if actid is not None:
+ found = False
+ for serv in self._services.values():
+ if serv.get_activity_id() == actid and serv.get_type() != stype:
+ found = True
+ break
+ if not found:
+ print "Buddy (%s) joined activity %s." % (self._nick_name, actid)
+ self._dbus_helper.JoinedActivity(dbus.ObjectPath(activity.object_path()))
+
+ if stype == PRESENCE_SERVICE_TYPE:
+ # A buddy isn't valid until its official presence
+ # service has been found and resolved
+ self._valid = True
+ print 'Requesting buddy icon %s' % self._nick_name
+ self._request_buddy_icon(service)
+ return True
+
+ def remove_service(self, service):
+ """Remove a service from a buddy; ie, the activity was closed
+ or the buddy went away."""
+ if service.get_publisher_address() != self._address:
+ return
+ if service.get_name() != self._nick_name:
+ return
+ stype = service.get_type()
+ if self._services.has_key(stype):
+ if self._valid:
+ self._dbus_helper.ServiceDisappeared(dbus.ObjectPath(service.object_path()))
+ del self._services[stype]
+
+ # If this is the lase service owned by a particular activity,
+ # and it's just been removed, send out the 'left-actvity' signal
+ actid = service.get_activity_id()
+ if actid is not None:
+ found = False
+ for serv in self._services.values():
+ if serv.get_activity_id() == actid:
+ found = True
+ break
+ if not found:
+ print "Buddy (%s) left activity %s." % (self._nick_name, actid)
+ self._dbus_helper.LeftActivity(dbus.ObjectPath(activity.object_path()))
+
+ if stype == PRESENCE_SERVICE_TYPE:
+ self._valid = False
+
+ def get_service_of_type(self, stype=None, activity=None):
+ """Return a service of a certain type, or None if the buddy
+ doesn't provide that service."""
+ if not stype:
+ raise RuntimeError("Need to specify a service type.")
+
+ if activity:
+ actid = activity.get_id()
+ for service in self._services.values():
+ if service.get_type() == stype and service.get_activity_id() == actid:
+ return service
+ if self._services.has_key(stype):
+ return self._services[stype]
+ return None
+
+ def is_valid(self):
+ """Return whether the buddy is valid or not. A buddy is
+ not valid until its official presence service has been found
+ and successfully resolved."""
+ return self._valid
+
+ def get_icon_pixbuf(self):
+ if self._icon:
+ pbl = gtk.gdk.PixbufLoader()
+ pbl.write(self._icon)
+ pbl.close()
+ return pbl.get_pixbuf()
+ else:
+ return None
+
+ def get_icon(self):
+ """Return the buddies icon, if any."""
+ return self._icon
+
+ def get_address(self):
+ return self._address
+
+ def get_nick_name(self):
+ return self._nick_name
+
+ def _set_icon(self, icon):
+ """Can only set icon for other buddies. The Owner
+ takes care of setting it's own icon."""
+ if icon != self._icon:
+ self._icon = icon
+ self._dbus_helper.IconChanged()
+
+ def is_owner(self):
+ return self._owner
+
+
+class Owner(Buddy):
+ """Class representing the owner of the machine. This is the client
+ portion of the Owner, paired with the server portion in Owner.py."""
+ def __init__(self, service):
+ Buddy.__init__(self, service)
+ self._owner = True
diff --git a/shell/PresenceService/PresenceService.py b/shell/PresenceService/PresenceService.py
new file mode 100644
index 0000000..cd5bd7f
--- /dev/null
+++ b/shell/PresenceService/PresenceService.py
@@ -0,0 +1,259 @@
+import avahi, dbus, dbus.glib, dbus.dbus_bindings, gobject
+import Buddy
+import Service
+import random
+import logging
+from sugar import env
+
+
+def _get_local_ip_address(ifname):
+ """Call Linux specific bits to retrieve our own IP address."""
+ import socket
+ import sys
+ import fcntl
+
+ addr = None
+ SIOCGIFADDR = 0x8915
+ sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ ifreq = (ifname + '\0'*32)[:32]
+ result = fcntl.ioctl(sockfd.fileno(), SIOCGIFADDR, ifreq)
+ addr = socket.inet_ntoa(result[20:24])
+ except IOError, exc:
+ print "Error getting IP address: %s" % exc
+ sockfd.close()
+ return addr
+
+
+class ServiceAdv(object):
+ """Wrapper class to track services from Avahi."""
+ def __init__(self, interface, protocol, name, stype, domain):
+ self._interface = interface
+ self._protocol = protocol
+ if type(name) != type(u""):
+ raise ValueError("service advertisement name must be unicode.")
+ self._name = name
+ if type(stype) != type(u""):
+ raise ValueError("service advertisement type must be unicode.")
+ self._stype = stype
+ if type(domain) != type(u""):
+ raise ValueError("service advertisement domain must be unicode.")
+ self._domain = domain
+ self._service = None
+ self._resolved = False
+
+ def interface(self):
+ return self._interface
+ def protocol(self):
+ return self._protocol
+ def name(self):
+ return self._name
+ def stype(self):
+ return self._stype
+ def domain(self):
+ return self._domain
+ def service(self):
+ return self._service
+ def set_service(self, service):
+ if not isinstance(service, Service.Service):
+ raise ValueError("must be a valid service.")
+ self._service = service
+ def resolved(self):
+ return self._resolved
+ def set_resolved(self, resolved):
+ self._resolved = resolved
+
+
+_PRESENCE_INTERFACE = "org.laptop.Presence"
+
+class PresenceService(dbus.service.Object):
+ def __init__(self):
+ # interface -> IP address: interfaces we've gotten events on so far
+ self._local_addrs = {}
+
+ # Our owner object
+ self._owner = None
+
+ self._buddies = {} # nick -> Buddy
+ self._services = {} # (name, type) -> Service
+ self._activities = {} # activity id -> Activity
+
+ # Keep track of stuff we're already browsing with ZC
+ self._service_type_browsers = {}
+ self._service_browsers = {}
+ self._resolve_queue = [] # Track resolve requests
+
+ # Resolved service list
+ self._service_advs = []
+
+ # Set up the dbus service we provide
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName('org.laptop.Presence', bus=session_bus)
+ dbus.service.Object.__init__(self, bus_name, '/org/laptop/Presence')
+
+ # Connect to Avahi for mDNS stuff
+ self._system_bus = dbus.SystemBus()
+ self._mdns_service = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME,
+ avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)
+ # Start browsing the local mDNS domain
+ self._start()
+
+ def _start(self):
+ # Always browse .local
+ self._new_domain_cb(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local")
+
+ # Connect to Avahi and start looking for stuff
+ domain_browser = self._mdns_service.DomainBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC,
+ "", avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0))
+ db = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, domain_browser), avahi.DBUS_INTERFACE_DOMAIN_BROWSER)
+ db.connect_to_signal('ItemNew', self._new_domain_cb_glue)
+
+ def _find_service_adv(self, interface=None, protocol=None, name=None, stype=None, domain=None):
+ """Search a list of service advertisements for ones matching certain criteria."""
+ adv_list = []
+ for adv in self._service_advs:
+ if interface and adv.interface() != interface:
+ continue
+ if protocol and adv.protocol() != protocol:
+ continue
+ if name and adv.name() != name:
+ continue
+ if stype and adv.stype() != stype:
+ continue
+ if domain and adv.domain() != domain:
+ continue
+ adv_list.append(adv)
+ return adv_list
+
+ def _service_appeared_cb(self, interface, protocol, full_name, stype, domain, flags):
+ logging.debug("found service '%s' (%d) of type '%s' in domain '%s' on %i.%i." % (full_name, flags, stype, domain, interface, protocol))
+
+ # Add the service to our unresolved services list
+ adv_list = self._find_service_adv(interface=interface, protocol=protocol,
+ name=full_name, stype=stype, domain=domain)
+ adv = None
+ if not adv_list:
+ adv = ServiceAdv(interface=interface, protocol=protocol, name=full_name,
+ stype=stype, domain=domain)
+ self._service_advs.append(adv)
+ else:
+ adv = adv_list[0]
+
+ # Find out the IP address of this interface, if we haven't already
+ if interface not in self._local_addrs.keys():
+ ifname = self._server.GetNetworkInterfaceNameByIndex(interface)
+ if ifname:
+ addr = _get_local_ip_address(ifname)
+ if addr:
+ self._local_addrs[interface] = addr
+
+ # Decompose service name if we can
+ (actid, buddy_name) = Service._decompose_service_name(full_name)
+
+ # FIXME: find a better way of letting the StartPage get everything
+ self.emit('new-service-adv', actid, stype)
+
+ # If we care about the service right now, resolve it
+ resolve = False
+ if actid is not None or stype in self._allowed_service_types:
+ resolve = True
+ if self._is_special_service_type(stype):
+ resolve = True
+ if resolve and not adv in self._resolve_queue:
+ self._resolve_queue.append(adv)
+ gobject.idle_add(self._resolve_service, adv)
+ else:
+ logging.debug("Do not resolve service '%s' of type '%s', we don't care about it." % (full_name, stype))
+
+ return False
+
+ def _service_appeared_cb_glue(self, interface, protocol, name, stype, domain, flags):
+ gobject.idle_add(self._service_appeared_cb, interface, protocol, name, stype, domain, flags)
+
+ def _service_disappeared_cb(self, interface, protocol, full_name, stype, domain, flags):
+ logging.debug("service '%s' of type '%s' in domain '%s' on %i.%i disappeared." % (full_name, stype, domain, interface, protocol))
+
+ # If it's an unresolved service, remove it from our unresolved list
+ adv_list = self._find_service_adv(interface=interface, protocol=protocol,
+ name=full_name, stype=stype, domain=domain)
+ if not adv_list:
+ return False
+
+ # Get the service object; if none, we have nothing left to do
+ adv = adv_list[0]
+ if adv in self._resolve_queue:
+ self._resolve_queue.remove(adv)
+ service = adv.service()
+ if not service:
+ return False
+
+ # Decompose service name if we can
+ (actid, buddy_name) = Service._decompose_service_name(full_name)
+
+ # Remove the service from the buddy
+ try:
+ buddy = self._buddies[buddy_name]
+ except KeyError:
+ pass
+ else:
+ buddy.remove_service(service)
+ self.emit('service-disappeared', buddy, service)
+ if not buddy.is_valid():
+ self.emit("buddy-disappeared", buddy)
+ del self._buddies[buddy_name]
+ self._handle_remove_service_for_activity(service, buddy)
+
+ return False
+
+ def _service_disappeared_cb_glue(self, interface, protocol, name, stype, domain, flags):
+ gobject.idle_add(self._service_disappeared_cb, interface, protocol, name, stype, domain, flags)
+
+ def _new_service_type_cb(self, interface, protocol, stype, domain, flags):
+ # Are we already browsing this domain for this type?
+ if self._service_browsers.has_key((interface, protocol, stype, domain)):
+ return
+
+ # Start browsing for all services of this type in this domain
+ s_browser = self._mdns_service.ServiceBrowserNew(interface, protocol, stype, domain, dbus.UInt32(0))
+ browser_obj = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, s_browser),
+ avahi.DBUS_INTERFACE_SERVICE_BROWSER)
+ logging.debug("now browsing for services of type '%s' in domain '%s' on %i.%i ..." % (stype, domain, interface, protocol))
+ browser_obj.connect_to_signal('ItemNew', self._service_appeared_cb_glue)
+ browser_obj.connect_to_signal('ItemRemove', self._service_disappeared_cb_glue)
+
+ self._service_browsers[(interface, protocol, stype, domain)] = browser_obj
+ return False
+
+ def _new_service_type_cb_glue(self, interface, protocol, stype, domain, flags):
+ gobject.idle_add(self._new_service_type_cb, interface, protocol, stype, domain, flags)
+
+ def _new_domain_cb(self, interface, protocol, domain, flags=0):
+ """Callback from Avahi when a new domain has been found. Start
+ browsing the new domain."""
+ # Only use .local for now...
+ if domain != "local":
+ return
+
+ # Are we already browsing this domain?
+ if self._service_type_browsers.has_key((interface, protocol, domain)):
+ return
+
+ # Start browsing this domain for the services its members offer
+ try:
+ st_browser = self._mdns_service.ServiceTypeBrowserNew(interface, protocol, domain, dbus.UInt32(0))
+ browser_obj = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, st_browser),
+ avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER)
+ except dbus.DBusException, exc:
+ logging.error("got exception %s while attempting to browse domain %s on %i.%i" % (domain, interface, protocol))
+ str_exc = str(exc)
+ if str_exc.find("The name org.freedesktop.Avahi was not provided by any .service files") >= 0:
+ raise Exception("Avahi does not appear to be running. '%s'" % str_exc)
+ else:
+ raise exc
+ logging.debug("now browsing domain '%s' on %i.%i ..." % (domain, interface, protocol))
+ browser_obj.connect_to_signal('ItemNew', self._new_service_type_cb_glue)
+ self._service_type_browsers[(interface, protocol, domain)] = browser_obj
+ return False
+
+ def _new_domain_cb_glue(self, interface, protocol, domain, flags=0):
+ gobject.idle_add(self._new_domain_cb, interface, protocol, domain, flags)
diff --git a/shell/PresenceService/Service.py b/shell/PresenceService/Service.py
new file mode 100644
index 0000000..6475567
--- /dev/null
+++ b/shell/PresenceService/Service.py
@@ -0,0 +1,452 @@
+import avahi
+import sys, os
+sys.path.insert(0, os.path.abspath("../../"))
+from sugar import util
+import dbus, dbus.service
+
+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 not name:
+ raise ValueError("name must be a valid string.")
+ if not activity_id:
+ return name
+ if type(name) != type(u""):
+ raise ValueError("name must be in unicode.")
+ composed = "%s [%s]" % (name, activity_id)
+ return composed.encode()
+
+def _decompose_service_name(name):
+ """Break a service name into the name and activity ID, if we can."""
+ if type(name) != type(u""):
+ raise ValueError("name must be a valid unicode string.")
+ name_len = len(name)
+ if name_len < util.ACTIVITY_ID_LEN + 5:
+ return (None, name)
+ # check for activity id end marker
+ if name[name_len - 1] != "]":
+ return (None, name)
+ start = name_len - 1 - util.ACTIVITY_ID_LEN
+ end = name_len - 1
+ # check for activity id start marker
+ if name[start - 1] != "[" or name[start - 2] != " ":
+ return (None, name)
+ activity_id = name[start:end]
+ if not util.validate_activity_id(activity_id):
+ return (None, name)
+ return (activity_id, name[:start - 2])
+
+def is_multicast_address(address):
+ """Simple numerical check for whether an IP4 address
+ is in the range for multicast addresses or not."""
+ if not address:
+ return False
+ if address[3] != '.':
+ return False
+ first = int(address[:3])
+ if first >= 224 and first <= 239:
+ return True
+ return False
+
+def deserialize(sdict):
+ try:
+ name = sdict['name']
+ if type(name) != type(u""):
+ raise ValueError("name must be unicode.")
+ stype = sdict['stype']
+ if type(stype) != type(u""):
+ raise ValueError("type must be unicode.")
+ domain = sdict['domain']
+ if type(domain) != type(u""):
+ raise ValueError("domain must be unicode.")
+ port = sdict['port']
+ properties = sdict['properties']
+ except KeyError, exc:
+ raise ValueError("Serialized service object was not valid.")
+
+ address = None
+ try:
+ address = sdict['address']
+ if type(address) != type(u""):
+ raise ValueError("address must be unicode.")
+ except KeyError:
+ pass
+
+ activity_id = None
+ try:
+ activity_id = sdict['activity_id']
+ if type(activity_id) != type(u""):
+ raise ValueError("activity id must be unicode.")
+ except KeyError:
+ pass
+
+ if activity_id is not None:
+ name = compose_service_name(name, activity_id)
+
+ return Service(name, stype, domain, address=address,
+ port=port, properties=properties)
+
+
+_ACTIVITY_ID_TAG = "ActivityID"
+SERVICE_DBUS_INTERFACE = "org.laptop.Presence.Service"
+
+class ServiceDBusHelper(dbus.service.Object):
+ """Handle dbus requests and signals for Service objects"""
+ def __init__(self, parent, bus_name, object_path):
+ self._parent = parent
+ self._bus_name = bus_name
+ self._object_path = object_path
+ dbus.service.Object.__init__(self, bus_name, self._object_path)
+
+ @dbus.service.method(SERVICE_DBUS_INTERFACE,
+ in_signature="", out_signature="a{sv}")
+ def getProperties(self):
+ """Return service properties."""
+ pary = {}
+ pary['name'] = self._parent.get_name()
+ pary['type'] = self._parent.get_type()
+ pary['domain'] = self._parent.get_domain()
+ pary['activityId'] = self._parent.get_activity_id()
+ port = self._parent.get_port()
+ if port:
+ pary['port'] = self._parent.get_port()
+ addr = self._parent.get_address()
+ if addr:
+ pary['address'] = addr
+ source_addr = self._parent.get_publisher_address()
+ if source_addr:
+ pary['sourceAddress'] = source_addr
+ return pary
+
+ @dbus.service.method(SERVICE_DBUS_INTERFACE,
+ in_signature="s", out_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
+
+class Service(object):
+ """Encapsulates information about a specific ZeroConf/mDNS
+ service as advertised on the network."""
+ def __init__(self, bus_name, object_id, name, stype, domain, address=None, port=-1, properties=None):
+ if not bus_name:
+ raise ValueError("DBus bus name must be valid")
+ if not object_id or type(object_id) != type(1):
+ raise ValueError("object id must be a valid number")
+
+ # Validate immutable options
+ if name and type(name) != type(u""):
+ raise ValueError("name must be unicode.")
+ if not name or not len(name):
+ raise ValueError("must specify a valid service name.")
+
+ if stype and type(stype) != type(u""):
+ raise ValueError("service type must be in unicode.")
+ if not stype or not len(stype):
+ raise ValueError("must specify a valid service type.")
+ if not stype.endswith("._tcp") and not stype.endswith("._udp"):
+ raise ValueError("must specify a TCP or UDP service type.")
+
+ if domain and type(domain) != type(u""):
+ raise ValueError("domain must be in unicode.")
+ if len(domain) and domain != "local":
+ raise ValueError("must use the 'local' domain (for now).")
+
+ (actid, real_name) = _decompose_service_name(name)
+ self._name = real_name
+ self._stype = stype
+ self._domain = domain
+ self._port = -1
+ self.set_port(port)
+ self._properties = {}
+ self.set_properties(properties)
+ # Publisher address is the unicast source IP
+ self._publisher_address = address
+ # Address is the published address, could be multicast or unicast
+ self._address = None
+ if self._properties.has_key('address'):
+ self.set_address(self._properties['address'])
+ else:
+ self.set_address(address)
+
+ # Ensure that an ActivityID tag, if given, matches
+ # what we expect from the service type
+ 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))
+ self._activity_id = actid
+ if actid and not self._properties.has_key(_ACTIVITY_ID_TAG):
+ self._properties[_ACTIVITY_ID_TAG] = actid
+
+ # register ourselves with dbus
+ self._object_id = object_id
+ self._object_path = "/org/laptop/Presence/Services/%d" % self._object_id
+ self._dbus_helper = ServiceDBusHelper(self, bus_name, self._object_path)
+
+ def object_path(self):
+ return self._object_path
+
+ def serialize(self, owner=None):
+ sdict = {}
+ if owner is not None:
+ sdict['name'] = dbus.Variant(owner.get_nick_name())
+ else:
+ sdict['name'] = dbus.Variant(self._name)
+ sdict['stype'] = dbus.Variant(self._stype)
+ if self._activity_id:
+ sdict['activity_id'] = dbus.Variant(self._activity_id)
+ sdict['domain'] = dbus.Variant(self._domain)
+ if self._address:
+ sdict['address'] = dbus.Variant(self._address)
+ sdict['port'] = dbus.Variant(self._port)
+ sdict['properties'] = dbus.Variant(self._properties)
+ return sdict
+
+ def get_name(self):
+ """Return the service's name, usually that of the
+ buddy who provides it."""
+ return self._name
+
+ def is_multicast_service(self):
+ """Return True if the service's address is a multicast address,
+ False if it is not."""
+ return is_multicast_address(self._address)
+
+ def get_one_property(self, key):
+ """Return one property of the service, or None
+ if the property was not found. Cannot distinguish
+ between lack of a property, and a property value that
+ actually is None."""
+ if key in self._properties.keys():
+ return self._properties[key]
+ return None
+
+ def get_properties(self):
+ """Return a python dictionary of all the service's
+ properties."""
+ return self._properties
+
+ def set_properties(self, properties):
+ """Set the service's properties from either an Avahi
+ TXT record (a list of lists of integers), or a
+ python 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,
+ # also convert everything to local encoding (for now)
+ # to ensure consistency
+ for key, value in props.items():
+ tmp_key = key
+ tmp_val = value
+ if type(tmp_key) == type(u""):
+ tmp_key = tmp_key.encode()
+ if type(tmp_val) == type(u""):
+ tmp_val = tmp_val.encode()
+ self._properties[tmp_key] = tmp_val
+
+ def get_type(self):
+ """Return the service's service type."""
+ return self._stype
+
+ def get_activity_id(self):
+ """Return the activity ID this service is associated with, if any."""
+ return self._activity_id
+
+ def get_port(self):
+ return self._port
+
+ def set_port(self, port):
+ if type(port) != type(1) or (port <= 1024 and port > 65536):
+ raise ValueError("must specify a valid port number between 1024 and 65536.")
+ self._port = port
+
+ def get_publisher_address(self):
+ return self._publisher_address
+
+ def get_address(self):
+ return self._address
+
+ def set_address(self, address):
+ if address is not None:
+ if type(address) != type(u""):
+ raise ValueError("address must be unicode")
+ self._address = address
+
+ def get_domain(self):
+ """Return the ZeroConf/mDNS domain the service was found in."""
+ return self._domain
+
+
+#################################################################
+# Tests
+#################################################################
+
+import unittest
+
+class ServiceTestCase(unittest.TestCase):
+ _DEF_NAME = u"foobar"
+ _DEF_STYPE = u"_foo._bar._tcp"
+ _DEF_DOMAIN = u"local"
+ _DEF_ADDRESS = u"1.1.1.1"
+ _DEF_PORT = 1234
+ _DEF_PROPS = {'foobar': 'baz'}
+
+ _STR_TEST_ARGS = [None, 0, [], {}]
+
+ def _test_init_fail(self, name, stype, domain, address, port, properties, fail_msg):
+ """Test something we expect to fail."""
+ try:
+ service = Service(name, stype, domain, address, port, properties)
+ except ValueError, exc:
+ pass
+ else:
+ self.fail("expected a ValueError for %s." % fail_msg)
+
+ def testName(self):
+ for item in self._STR_TEST_ARGS:
+ self._test_init_fail(item, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS, "invalid name")
+
+ def testType(self):
+ for item in self._STR_TEST_ARGS:
+ self._test_init_fail(self._DEF_NAME, item, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS, "invalid service type")
+ self._test_init_fail(self._DEF_NAME, "_bork._foobar", self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS, "invalid service type")
+
+ def testDomain(self):
+ for item in self._STR_TEST_ARGS:
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, item, self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS, "invalid domain")
+ # Only accept local for now
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, "foobar", self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS, "invalid domain")
+ # Make sure "" works
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName('org.laptop.Presence', bus=session_bus)
+ service = Service(bus_name, 1, self._DEF_NAME, self._DEF_STYPE, "", self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS)
+ del bus_name, session_bus
+ assert service, "Empty domain was not accepted!"
+
+ def testAddress(self):
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, [],
+ self._DEF_PORT, self._DEF_PROPS, "invalid address")
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, {},
+ self._DEF_PORT, self._DEF_PROPS, "invalid address")
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, 1234,
+ self._DEF_PORT, self._DEF_PROPS, "invalid address")
+
+ def testPort(self):
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ [], self._DEF_PROPS, "invalid port")
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ {}, self._DEF_PROPS, "invalid port")
+ self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ "adf", self._DEF_PROPS, "invalid port")
+
+ def testGoodInit(self):
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName('org.laptop.Presence', bus=session_bus)
+ service = Service(bus_name, 1, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS)
+ del bus_name, session_bus
+ assert service.get_name() == self._DEF_NAME, "service name wasn't correct after init."
+ assert service.get_type() == self._DEF_STYPE, "service type wasn't correct after init."
+ assert service.get_domain() == "local", "service domain wasn't correct after init."
+ assert service.get_address() == self._DEF_ADDRESS, "service address wasn't correct after init."
+ assert service.get_port() == self._DEF_PORT, "service port wasn't correct after init."
+ value = service.get_one_property('foobar')
+ assert value and value == 'baz', "service property wasn't correct after init."
+
+ def testAvahiProperties(self):
+ props = [[111, 114, 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 65, 118, 97, 104, 105, 46, 99, 111, 111, 107, 105, 101, 61, 50, 54, 48, 49, 53, 52, 51, 57, 53, 50]]
+ key = "org.freedesktop.Avahi.cookie"
+ expected_value = "2601543952"
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName('org.laptop.Presence', bus=session_bus)
+ service = Service(bus_name, 1, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, props)
+ del bus_name, session_bus
+ value = service.get_one_property(key)
+ assert value and value == expected_value, "service properties weren't correct after init."
+ value = service.get_one_property('bork')
+ assert not value, "service properties weren't correct after init."
+
+ def testBoolProperty(self):
+ props = [[111, 114, 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 65, 118, 97, 104, 105, 46, 99, 111, 111, 107, 105, 101]]
+ key = "org.freedesktop.Avahi.cookie"
+ expected_value = True
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName('org.laptop.Presence', bus=session_bus)
+ service = Service(bus_name, 1, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, props)
+ value = service.get_one_property(key)
+ del bus_name, session_bus
+ assert value is not None and value == expected_value, "service properties weren't correct after init."
+
+ def testGroupService(self):
+ # Valid group service type, non-multicast address
+ group_stype = u"_af5e5a7c998e89b9a_group_olpc._udp"
+ self._test_init_fail(self._DEF_NAME, group_stype, self._DEF_DOMAIN, self._DEF_ADDRESS,
+ self._DEF_PORT, self._DEF_PROPS, "group service type, but non-multicast address")
+
+ # Valid group service type, None address
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName('org.laptop.Presence', bus=session_bus)
+ service = Service(bus_name, 1, self._DEF_NAME, group_stype, self._DEF_DOMAIN, None,
+ self._DEF_PORT, self._DEF_PROPS)
+ assert service.get_address() == None, "address was not None as expected!"
+
+ # Valid group service type and multicast address, ensure it works
+ mc_addr = u"224.0.0.34"
+ service = Service(bus_name, 1, self._DEF_NAME, group_stype, self._DEF_DOMAIN, mc_addr,
+ self._DEF_PORT, self._DEF_PROPS)
+ del bus_name, session_bus
+ assert service.get_address() == mc_addr, "address was not expected address!"
+
+ def addToSuite(suite):
+ suite.addTest(ServiceTestCase("testName"))
+ suite.addTest(ServiceTestCase("testType"))
+ suite.addTest(ServiceTestCase("testDomain"))
+ suite.addTest(ServiceTestCase("testAddress"))
+ suite.addTest(ServiceTestCase("testPort"))
+ suite.addTest(ServiceTestCase("testGoodInit"))
+ suite.addTest(ServiceTestCase("testAvahiProperties"))
+ suite.addTest(ServiceTestCase("testBoolProperty"))
+ suite.addTest(ServiceTestCase("testGroupService"))
+ addToSuite = staticmethod(addToSuite)
+
+
+def main():
+ suite = unittest.TestSuite()
+ ServiceTestCase.addToSuite(suite)
+ runner = unittest.TextTestRunner()
+ runner.run(suite)
+
+if __name__ == "__main__":
+ main()