diff --git a/shell/PresenceService/Buddy.py b/shell/PresenceService/Buddy.py index ca2b566e..af16eb60 100644 --- a/shell/PresenceService/Buddy.py +++ b/shell/PresenceService/Buddy.py @@ -7,6 +7,7 @@ import dbus, dbus.service PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp" +BUDDY_DBUS_OBJECT_PATH = "/org/laptop/Presence/Buddies/" BUDDY_DBUS_INTERFACE = "org.laptop.Presence.Buddy" class NotFoundError(Exception): @@ -85,6 +86,8 @@ class Buddy(object): 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") + if not isinstance(service, Service.Service): + raise ValueError("service must be a valid service object") self._services = {} self._activities = {} @@ -97,7 +100,7 @@ class Buddy(object): self._owner = owner self._object_id = object_id - self._object_path = "/org/laptop/Presence/Buddies/%d" % self._object_id + self._object_path = BUDDY_DBUS_OBJECT_PATH + str(self._object_id) self._dbus_helper = BuddyDBusHelper(self, bus_name, self._object_path) self.add_service(service) @@ -255,3 +258,73 @@ class Owner(Buddy): portion of the Owner, paired with the server portion in Owner.py.""" def __init__(self, bus_name, object_id, service): Buddy.__init__(self, bus_name, object_id, service, owner=True) + + +################################################################# +# Tests +################################################################# + +import unittest +import Service + +__objid_seq = 0 +def _next_objid(): + global __objid_seq + __objid_seq = __objid_seq + 1 + return __objid_seq + + +class BuddyTestCase(unittest.TestCase): + _DEF_NAME = u"Tommy" + _DEF_STYPE = unicode(PRESENCE_SERVICE_TYPE) + _DEF_DOMAIN = u"local" + _DEF_ADDRESS = u"1.1.1.1" + _DEF_PORT = 1234 + + def __init__(self, name): + self._bus = dbus.SessionBus() + self._bus_name = dbus.service.BusName('org.laptop.Presence', bus=self._bus) + unittest.TestCase.__init__(self, name) + + def __del__(self): + del self._bus_name + del self._bus + + def _test_init_fail(self, service, fail_msg): + """Test something we expect to fail.""" + try: + objid = _next_objid() + buddy = Buddy(self._bus_name, objid, service, owner=False) + except ValueError, exc: + pass + else: + self.fail("expected a ValueError for %s." % fail_msg) + + def testService(self): + service = None + self._test_init_fail(service, "invalid service") + + def testGoodInit(self): + objid = _next_objid() + service = Service.Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, + self._DEF_ADDRESS, self._DEF_PORT) + objid = _next_objid() + buddy = Buddy(self._bus_name, objid, service) + assert buddy.get_nick_name() == self._DEF_NAME, "buddy name wasn't correct after init." + assert buddy.get_address() == self._DEF_ADDRESS, "buddy address wasn't correct after init." + assert buddy.object_path() == BUDDY_DBUS_OBJECT_PATH + str(objid) + + def addToSuite(suite): + suite.addTest(BuddyTestCase("testService")) + suite.addTest(BuddyTestCase("testGoodInit")) + addToSuite = staticmethod(addToSuite) + + +def main(): + suite = unittest.TestSuite() + BuddyTestCase.addToSuite(suite) + runner = unittest.TextTestRunner() + runner.run(suite) + +if __name__ == "__main__": + main() diff --git a/shell/PresenceService/PresenceService.py b/shell/PresenceService/PresenceService.py index 657bb92b..92fa8199 100644 --- a/shell/PresenceService/PresenceService.py +++ b/shell/PresenceService/PresenceService.py @@ -68,6 +68,9 @@ _PRESENCE_SERVICE = "org.laptop.Presence" _PRESENCE_DBUS_INTERFACE = "org.laptop.Presence" _PRESENCE_OBJECT_PATH = "/org/laptop/Presence" +class NotFoundError(Exception): + pass + class PresenceServiceDBusHelper(dbus.service.Object): def __init__(self, parent, bus_name): self._parent = parent @@ -104,6 +107,74 @@ class PresenceServiceDBusHelper(dbus.service.Object): def ActivityDisappeared(self, object_path): pass + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="", out_signature="ao") + def getServices(self): + services = self._parent.get_services() + ret = [] + for serv in services: + ret.append(serv.object_path()) + return ret + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="s", out_signature="ao") + def getServicesOfType(self, stype): + services = self._parent.get_services_of_type(stype) + ret = [] + for serv in services: + ret.append(serv.object_path()) + return ret + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="", out_signature="ao") + def getActivities(self): + activities = self._parent.get_activities() + ret = [] + for act in activities: + ret.append(act.object_path()) + return ret + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="s", out_signature="o") + def getActivity(self, actid): + act = self._parent.get_activity(actid) + if not act: + raise NotFoundError("Not found") + return act.object_path() + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="", out_signature="ao") + def getBuddies(self): + buddies = self._parent.get_buddies() + ret = [] + for buddy in buddies: + ret.append(buddy.object_path()) + return ret + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="s", out_signature="o") + def getBuddyByName(self, name): + buddy = self._parent.get_buddy_by_name(name) + if not buddy: + raise NotFoundError("Not found") + return buddy.object_path() + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="s", out_signature="o") + def getBuddyByAddress(self, addr): + buddy = self._parent.get_buddy_by_address(addr) + if not buddy: + raise NotFoundError("Not found") + return buddy.object_path() + + @dbus.service.method(_PRESENCE_DBUS_INTERFACE, + in_signature="", out_signature="o") + def getOwner(self): + owner = self._parent.get_owner() + if not owner: + raise NotFoundError("Not found") + return owner.object_path() + class PresenceService(object): def __init__(self): @@ -154,6 +225,41 @@ class PresenceService(object): self._next_object_id = self._next_object_id + 1 return self._next_object_id + def get_services(self): + return self._services.values() + + def get_services_of_type(self, stype): + ret = [] + for serv in self._services.values(): + if serv.get_type() == stype: + ret.append(serv) + return ret + + def get_activities(self): + return self._activities.values() + + def get_activity(self, actid): + if self._activities.has_key(actid): + return self._activities[actid] + return None + + def get_buddies(self): + return self._buddies.values() + + def get_buddy_by_name(self, name): + if self._buddies.has_key(name): + return self._buddies[name] + return None + + def get_buddy_by_address(self, address): + for buddy in self._buddies.values(): + if buddy.get_address() == address: + return buddy + return None + + def get_owner(self): + return self._owner + 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 = [] @@ -247,11 +353,16 @@ class PresenceService(object): if adv in self._resolve_queue: self._resolve_queue.remove(adv) - # Update the service now that it's been resolved - objid = self._get_next_object_id() - service = Service.Service(self._bus_name, objid, name=full_name, - stype=stype, domain=domain, address=address, port=port, - properties=txt) + # See if we know about this service already + key = (full_name, stype) + if not self._services.has_key(key): + objid = self._get_next_object_id() + service = Service.Service(self._bus_name, objid, name=full_name, + stype=stype, domain=domain, address=address, port=port, + properties=txt) + self._services[key] = service + else: + service = self._services[key] adv.set_service(service) # Merge the service into our buddy and activity lists, if needed @@ -347,7 +458,8 @@ class PresenceService(object): if not buddy.is_valid(): self._dbus_helper.BuddyDisappeared(buddy.object_path()) del self._buddies[buddy_name] - + key = (service.get_full_name(), service.get_type()) + del self._services[key] return False def _service_disappeared_cb_glue(self, interface, protocol, name, stype, domain, flags): @@ -403,6 +515,84 @@ 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 register_service(self, name, stype, properties, address, port, domain): + """Register a new service, advertising it to other Buddies on the network.""" + 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) + self._services[key] = service + + if self.get_owner() and name != self.get_owner().get_nick_name(): + raise RuntimeError("Tried to register a service that didn't have Owner nick as the service name!") + actid = service.get_activity_id() + if actid: + rs_name = Service.compose_service_name(rs_name, actid) + rs_stype = service.get_type() + rs_port = service.get_port() + rs_props = service.get_properties() + rs_domain = service.get_domain() + rs_address = service.get_address() + if not rs_domain or not len(rs_domain): + rs_domain = "" + logging.debug("registered service name '%s' type '%s' on port %d with args %s" % (rs_name, rs_stype, rs_port, rs_props)) + + try: + group = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, self._server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) + + # Add properties; ensure they are converted to ByteArray types + # because python sometimes can't figure that out + info = [] + for k, v in rs_props.items(): + tmp_item = "%s=%s" % (k, v) + info.append(dbus.types.ByteArray(tmp_item)) + + if rs_address and len(rs_address): + info.append("address=%s" % (rs_address)) + logging.debug("PS: about to call AddService for Avahi with rs_name='%s' (%s), rs_stype='%s' (%s)," \ + " rs_domain='%s' (%s), rs_port=%d (%s), info='%s' (%s)" % (rs_name, type(rs_name), rs_stype, + type(rs_stype), rs_domain, type(rs_domain), rs_port, type(rs_port), info, type(info))) + group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, rs_name, rs_stype, + rs_domain, "", # let Avahi figure the 'host' out + dbus.UInt16(rs_port), info,) + group.Commit() + except dbus.dbus_bindings.DBusException, exc: + # FIXME: ignore local name collisions, since that means + # the zeroconf service is already registered. Ideally we + # should un-register it an re-register with the correct info + if str(exc) == "Local name collision": + pass + activity_stype = service.get_type() + self.register_service_type(activity_stype) + + def register_service_type(self, stype): + """Requests that the Presence service look for and recognize + 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) + + # Find unresolved services that match the service type + # we're now interested in, and resolve them + resolv_list = [] + + # Find services of this type + resolv_list = self._find_service_adv(stype=stype) + # Request resolution for them if they aren't in-process already + for adv in resolv_list: + if adv not in self._resolve_queue: + self._resolve_queue.append(adv) + gobject.idle_add(self._resolve_service, adv) + + def unregister_service_type(self, stype): + """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) + def main(): diff --git a/shell/PresenceService/Service.py b/shell/PresenceService/Service.py index 2e51c073..d9a504cc 100644 --- a/shell/PresenceService/Service.py +++ b/shell/PresenceService/Service.py @@ -22,6 +22,8 @@ def _txt_to_dict(txt): return prop_dict def compose_service_name(name, activity_id): + if type(name) == type(""): + name = unicode(name) if not name: raise ValueError("name must be a valid string.") if not activity_id: @@ -29,7 +31,7 @@ def compose_service_name(name, activity_id): if type(name) != type(u""): raise ValueError("name must be in unicode.") composed = "%s [%s]" % (name, activity_id) - return composed.encode() + return composed def _decompose_service_name(name): """Break a service name into the name and activity ID, if we can.""" @@ -58,7 +60,7 @@ def is_multicast_address(address): return False if address[3] != '.': return False - first = int(address[:3]) + first = int(float(address[:3])) if first >= 224 and first <= 239: return True return False @@ -66,6 +68,7 @@ def is_multicast_address(address): _ACTIVITY_ID_TAG = "ActivityID" SERVICE_DBUS_INTERFACE = "org.laptop.Presence.Service" +SERVICE_DBUS_OBJECT_PATH = "/org/laptop/Presence/Services/" class ServiceDBusHelper(dbus.service.Object): """Handle dbus requests and signals for Service objects""" @@ -105,10 +108,11 @@ class ServiceDBusHelper(dbus.service.Object): 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): + def __init__(self, bus_name, object_id, name, stype, domain=u"local", 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): @@ -127,13 +131,14 @@ class Service(object): 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""): + if type(domain) != type(u""): raise ValueError("domain must be in unicode.") - if len(domain) and domain != "local": + if 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._full_name = name self._stype = stype self._domain = domain self._port = -1 @@ -163,7 +168,7 @@ class Service(object): # register ourselves with dbus self._object_id = object_id - self._object_path = "/org/laptop/Presence/Services/%d" % self._object_id + self._object_path = SERVICE_DBUS_OBJECT_PATH + str(self._object_id) self._dbus_helper = ServiceDBusHelper(self, bus_name, self._object_path) def object_path(self): @@ -182,6 +187,9 @@ class Service(object): buddy who provides it.""" return self._name + def get_full_name(self): + return self._full_name + def is_multicast_service(self): """Return True if the service's address is a multicast address, False if it is not.""" @@ -261,6 +269,13 @@ class Service(object): import unittest +__objid_seq = 0 +def _next_objid(): + global __objid_seq + __objid_seq = __objid_seq + 1 + return __objid_seq + + class ServiceTestCase(unittest.TestCase): _DEF_NAME = u"foobar" _DEF_STYPE = u"_foo._bar._tcp" @@ -268,13 +283,23 @@ class ServiceTestCase(unittest.TestCase): _DEF_ADDRESS = u"1.1.1.1" _DEF_PORT = 1234 _DEF_PROPS = {'foobar': 'baz'} - _STR_TEST_ARGS = [None, 0, [], {}] + def __init__(self, name): + self._bus = dbus.SessionBus() + self._bus_name = dbus.service.BusName('org.laptop.Presence', bus=self._bus) + unittest.TestCase.__init__(self, name) + + def __del__(self): + del self._bus_name + del self._bus + 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) + objid = _next_objid() + service = Service(self._bus_name, objid, name, stype, domain, address, + port, properties) except ValueError, exc: pass else: @@ -289,7 +314,7 @@ class ServiceTestCase(unittest.TestCase): 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._test_init_fail(self._DEF_NAME, u"_bork._foobar", self._DEF_DOMAIN, self._DEF_ADDRESS, self._DEF_PORT, self._DEF_PROPS, "invalid service type") def testDomain(self): @@ -297,14 +322,12 @@ class ServiceTestCase(unittest.TestCase): 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._test_init_fail(self._DEF_NAME, self._DEF_STYPE, u"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 + objid = _next_objid() + service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, u"", + self._DEF_ADDRESS, self._DEF_PORT, self._DEF_PROPS) assert service, "Empty domain was not accepted!" def testAddress(self): @@ -324,16 +347,15 @@ class ServiceTestCase(unittest.TestCase): "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 + objid = _next_objid() + service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, + self._DEF_ADDRESS, self._DEF_PORT, self._DEF_PROPS) 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." + assert service.object_path() == SERVICE_DBUS_OBJECT_PATH + str(objid) value = service.get_one_property('foobar') assert value and value == 'baz', "service property wasn't correct after init." @@ -341,11 +363,9 @@ class ServiceTestCase(unittest.TestCase): 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 + objid = _next_objid() + service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, + self._DEF_ADDRESS, self._DEF_PORT, props) 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') @@ -355,32 +375,29 @@ class ServiceTestCase(unittest.TestCase): 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, + objid = _next_objid() + service = Service(self._bus_name, objid, 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): + def testActivityService(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, + actid = "4569a71b80805aa96a847f7ac1c407327b3ec2b4" + name = compose_service_name("Tommy", actid) + + # Valid activity service name, None address + objid = _next_objid() + service = Service(self._bus_name, objid, name, self._DEF_STYPE, self._DEF_DOMAIN, None, self._DEF_PORT, self._DEF_PROPS) assert service.get_address() == None, "address was not None as expected!" + assert service.get_activity_id() == actid, "activity id was different than expected!" - # Valid group service type and multicast address, ensure it works + # Valid activity service name 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, + objid = _next_objid() + service = Service(self._bus_name, objid, name, self._DEF_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): @@ -392,7 +409,7 @@ class ServiceTestCase(unittest.TestCase): suite.addTest(ServiceTestCase("testGoodInit")) suite.addTest(ServiceTestCase("testAvahiProperties")) suite.addTest(ServiceTestCase("testBoolProperty")) - suite.addTest(ServiceTestCase("testGroupService")) + suite.addTest(ServiceTestCase("testActivityService")) addToSuite = staticmethod(addToSuite) diff --git a/sugar/p2p/Stream.py b/sugar/p2p/Stream.py index 4d0705ef..6a0df2d9 100644 --- a/sugar/p2p/Stream.py +++ b/sugar/p2p/Stream.py @@ -10,8 +10,6 @@ from sugar.presence import Service class Stream(object): def __init__(self, service): - if not isinstance(service, Service.Service): - raise ValueError("service must be valid.") if not service.get_port(): raise ValueError("service must have an address.") self._service = service @@ -21,8 +19,6 @@ class Stream(object): self._callback = None def new_from_service(service, start_reader=True): - if not isinstance(service, Service.Service): - raise ValueError("service must be valid.") if service.is_multicast_service(): return MulticastStream(service) else: @@ -40,8 +36,6 @@ class Stream(object): class UnicastStreamWriter(object): def __init__(self, stream, service): # set up the writer - if not isinstance(service, Service.Service): - raise ValueError("service must be valid") self._service = service if not service.get_address(): raise ValueError("service must have a valid address.")