Switch from Groups to grouping services based on activity UID
This commit is contained in:
		
							parent
							
								
									55f538253c
								
							
						
					
					
						commit
						1cdaf97f20
					
				| @ -1,61 +0,0 @@ | |||||||
| import Service |  | ||||||
| 
 |  | ||||||
| import sugar.util |  | ||||||
| 
 |  | ||||||
| def is_group_service_type(stype): |  | ||||||
| 	"""Return True if the service type matches a group |  | ||||||
| 	service type, or False if it does not.""" |  | ||||||
| 	if stype.endswith("_group_olpc._tcp") or stype.endswith("_group_olpc._udp"): |  | ||||||
| 		return True |  | ||||||
| 	return False |  | ||||||
| 
 |  | ||||||
| __GROUP_NAME_TAG = "Name" |  | ||||||
| __GROUP_RESOURCE_TAG = "Resource" |  | ||||||
| 
 |  | ||||||
| def new_group_service(group_name, resource): |  | ||||||
| 	"""Create a new service suitable for defining a new group.""" |  | ||||||
| 	if type(group_name) != type("") or not len(group_name): |  | ||||||
| 		raise ValueError("group name must be a valid string.") |  | ||||||
| 	if type(resource) != type("") or not len(resource): |  | ||||||
| 		raise ValueError("group resource must be a valid string.") |  | ||||||
| 
 |  | ||||||
| 	# Create a randomized service type |  | ||||||
| 	data = "%s%s" % (group_name, resource) |  | ||||||
| 	stype = "_%s_group_olpc._udp" % sugar.util.unique_id(data) |  | ||||||
| 
 |  | ||||||
| 	properties = {__GROUP_NAME_TAG: group_name, __GROUP_RESOURCE_TAG: resource } |  | ||||||
| 	owner_nick = "" |  | ||||||
| 	port = random.randint(5000, 65000) |  | ||||||
| 	# Use random currently unassigned multicast address |  | ||||||
| 	address = "232.%d.%d.%d" % (random.randint(0, 254), random.randint(1, 254), |  | ||||||
| 			random.randint(1, 254)) |  | ||||||
| 	service = Service.Service(owner_nick, stype, "local", address=address, |  | ||||||
| 			port=port, properties=properties) |  | ||||||
| 	return service |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Group(object): |  | ||||||
| 	"""Represents a collection of buddies all interested in the same resource.""" |  | ||||||
| 	def __init__(self, service): |  | ||||||
| 		if not isinstance(service, Service.Service): |  | ||||||
| 			raise ValueError("service argument was not a Service object.") |  | ||||||
| 		if not service.is_group_service(): |  | ||||||
| 			raise ValueError("provided serivce was not a group service.") |  | ||||||
| 		name = service.get_one_property(__GROUP_NAME_TAG) |  | ||||||
| 		if name == None: |  | ||||||
| 			raise ValueError("provided service did not provide a group name.") |  | ||||||
| 		self._name = name |  | ||||||
| 		resource = service.get_one_property(__GROUP_RESOURCE_TAG) |  | ||||||
| 		if resource == None: |  | ||||||
| 			raise ValueError("provided service did not provide a group resource.") |  | ||||||
| 		self._resource = resource |  | ||||||
| 		self._service = service |  | ||||||
| 
 |  | ||||||
| 	def get_name(self): |  | ||||||
| 		return self._name |  | ||||||
| 
 |  | ||||||
| 	def get_service(self): |  | ||||||
| 		return self._service |  | ||||||
| 
 |  | ||||||
| 	def get_resource(self): |  | ||||||
| 		return self._resource |  | ||||||
| @ -2,9 +2,9 @@ import threading | |||||||
| import avahi, dbus, dbus.glib, dbus.dbus_bindings, gobject | import avahi, dbus, dbus.glib, dbus.dbus_bindings, gobject | ||||||
| import Buddy | import Buddy | ||||||
| import Service | import Service | ||||||
| import Group |  | ||||||
| import os | import os | ||||||
| 
 | import string | ||||||
|  | from sugar import util | ||||||
| 
 | 
 | ||||||
| def _get_local_ip_address(ifname): | def _get_local_ip_address(ifname): | ||||||
| 	"""Call Linux specific bits to retrieve our own IP address.""" | 	"""Call Linux specific bits to retrieve our own IP address.""" | ||||||
| @ -97,8 +97,8 @@ class PresenceService(gobject.GObject): | |||||||
| 		# Our owner object | 		# Our owner object | ||||||
| 		self._owner = None | 		self._owner = None | ||||||
| 
 | 
 | ||||||
| 		# group UID -> Group: groups we've found | 		# activity UID -> Service: services grouped by activity UID | ||||||
| 		self._groups = {} | 		self._activity_services = {} | ||||||
| 
 | 
 | ||||||
| 		# All the mdns service types we care about | 		# All the mdns service types we care about | ||||||
| 		self._allowed_service_types = [] | 		self._allowed_service_types = [] | ||||||
| @ -110,7 +110,7 @@ class PresenceService(gobject.GObject): | |||||||
| 		# Resolved service list | 		# Resolved service list | ||||||
| 		self._service_advs = [] | 		self._service_advs = [] | ||||||
| 
 | 
 | ||||||
| 		# Main activity UID to filter on | 		# Main activity UID to filter services on | ||||||
| 		self._activity_uid = None | 		self._activity_uid = None | ||||||
| 
 | 
 | ||||||
| 		self._bus = dbus.SystemBus() | 		self._bus = dbus.SystemBus() | ||||||
| @ -126,8 +126,8 @@ class PresenceService(gobject.GObject): | |||||||
| 		self._started = True | 		self._started = True | ||||||
| 		self._lock.release() | 		self._lock.release() | ||||||
| 
 | 
 | ||||||
| 		if activity_uid and (not type(activity_uid) == type("") or not len(activity_uid)): | 		if activity_uid and not util.validate_activity_uid(activity_uid): | ||||||
| 			raise ValueError("activity uid must be a string.") | 			raise ValueError("activity uid must be a valid UID string.") | ||||||
| 		self._activity_uid = activity_uid | 		self._activity_uid = activity_uid | ||||||
| 
 | 
 | ||||||
| 		# Always browse .local | 		# Always browse .local | ||||||
| @ -177,8 +177,6 @@ class PresenceService(gobject.GObject): | |||||||
| 		type, and False if it's not.""" | 		type, and False if it's not.""" | ||||||
| 		if stype == Buddy.PRESENCE_SERVICE_TYPE: | 		if stype == Buddy.PRESENCE_SERVICE_TYPE: | ||||||
| 			return True | 			return True | ||||||
| 		if Group.is_group_service_type(stype): |  | ||||||
| 			return True |  | ||||||
| 		return False | 		return False | ||||||
| 
 | 
 | ||||||
| 	def _handle_new_service_for_buddy(self, service): | 	def _handle_new_service_for_buddy(self, service): | ||||||
| @ -204,12 +202,24 @@ class PresenceService(gobject.GObject): | |||||||
| 			self.emit("buddy-appeared", buddy) | 			self.emit("buddy-appeared", buddy) | ||||||
| 		return buddy | 		return buddy | ||||||
| 
 | 
 | ||||||
| 	def _handle_new_service_for_group(self, service, buddy): | 	def _handle_new_service_for_activity(self, service, buddy): | ||||||
| 		# If the serivce is a group service, merge it into our groups list | 		# If the serivce is a group service, merge it into our groups list | ||||||
| 		group = None | 		uid = service.get_activity_uid() | ||||||
| 		if not self._groups.has_key(service.get_type()): | 		if not uid: | ||||||
| 			group = Group.Group(service) | 			uid = "*" | ||||||
| 			self._groups[service.get_type()] = group | 		if not self._activity_services.has_key(uid): | ||||||
|  | 			self._activity_services[uid] = [] | ||||||
|  | 		self._activity_services[uid].append((buddy, service)) | ||||||
|  | 
 | ||||||
|  | 	def _handle_remove_service_for_activity(self, service, buddy): | ||||||
|  | 		uid = service.get_activity_uid() | ||||||
|  | 		if not uid: | ||||||
|  | 			uid = "*" | ||||||
|  | 		if self._activity_services.has_key(uid): | ||||||
|  | 			try: | ||||||
|  | 				self._activity_services.remove((buddy, service)) | ||||||
|  | 			except: | ||||||
|  | 				pass | ||||||
| 
 | 
 | ||||||
| 	def _resolve_service_reply_cb(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): | 	def _resolve_service_reply_cb(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): | ||||||
| 		"""When the service discovery finally gets here, we've got enough information about the | 		"""When the service discovery finally gets here, we've got enough information about the | ||||||
| @ -226,13 +236,14 @@ class PresenceService(gobject.GObject): | |||||||
| 		adv.set_resolved(True) | 		adv.set_resolved(True) | ||||||
| 
 | 
 | ||||||
| 		# Update the service now that it's been resolved | 		# Update the service now that it's been resolved | ||||||
| 		service = Service.Service(name, stype, domain, address, port, txt) | 		service = Service.Service(name=name, stype=stype, domain=domain, | ||||||
|  | 				address=address, port=port, properties=txt) | ||||||
| 		adv.set_service(service) | 		adv.set_service(service) | ||||||
| 
 | 
 | ||||||
| 		# Merge the service into our buddy and group lists, if needed | 		# Merge the service into our buddy and group lists, if needed | ||||||
| 		buddy = self._handle_new_service_for_buddy(service) | 		buddy = self._handle_new_service_for_buddy(service) | ||||||
| 		if buddy and service.is_group_service(): | 		if buddy and service.get_activity_uid(): | ||||||
| 			self._handle_new_service_for_group(service, buddy) | 			self._handle_new_service_for_activity(service, buddy) | ||||||
| 
 | 
 | ||||||
| 		return False | 		return False | ||||||
| 
 | 
 | ||||||
| @ -268,8 +279,19 @@ class PresenceService(gobject.GObject): | |||||||
| 				if addr: | 				if addr: | ||||||
| 					self._local_addrs[interface] = addr | 					self._local_addrs[interface] = addr | ||||||
| 
 | 
 | ||||||
|  | 		# Decompose service type if we can | ||||||
|  | 		(uid, stype) = Service._decompose_service_type(stype) | ||||||
|  | 
 | ||||||
| 		# If we care about the service right now, resolve it | 		# If we care about the service right now, resolve it | ||||||
| 		if stype in self._allowed_service_types or self._is_special_service_type(stype): | 		resolve = False | ||||||
|  | 		if self._activity_uid and self._activity_uid == uid: | ||||||
|  | 			if stype in self._allowed_service_types: | ||||||
|  | 				resolve = True | ||||||
|  | 		elif not self._activity_uid: | ||||||
|  | 			resolve = True | ||||||
|  | 		if self._is_special_service_type(stype): | ||||||
|  | 			resolve = True | ||||||
|  | 		if resolve: | ||||||
| 			gobject.idle_add(self._resolve_service, interface, protocol, name, stype, domain, flags) | 			gobject.idle_add(self._resolve_service, interface, protocol, name, stype, domain, flags) | ||||||
| 		return False | 		return False | ||||||
| 
 | 
 | ||||||
| @ -284,20 +306,23 @@ class PresenceService(gobject.GObject): | |||||||
| 		if not adv_list: | 		if not adv_list: | ||||||
| 			return False | 			return False | ||||||
| 
 | 
 | ||||||
| 		# Unresolved services by definition aren't assigned to a buddy | 		# Get the service object; if none, we have nothing left to do | ||||||
| 		try: |  | ||||||
| 			# Remove the service from the buddy |  | ||||||
| 			buddy = self._buddies[name] |  | ||||||
| 			# FIXME: need to be more careful about how we remove services |  | ||||||
| 			# from buddies; this could be spoofed |  | ||||||
| 		adv = adv_list[0] | 		adv = adv_list[0] | ||||||
| 			service = adv.get_service() | 		service = adv.service() | ||||||
|  | 		if not service: | ||||||
|  | 			return False | ||||||
|  | 
 | ||||||
|  | 		# Remove the service from the buddy | ||||||
|  | 		try: | ||||||
|  | 			buddy = self._buddies[name] | ||||||
|  | 		except KeyError: | ||||||
|  | 			pass | ||||||
|  | 		else: | ||||||
| 			buddy.remove_service(service) | 			buddy.remove_service(service) | ||||||
| 			if not buddy.is_valid(): | 			if not buddy.is_valid(): | ||||||
| 				self.emit("buddy-disappeared", buddy) | 				self.emit("buddy-disappeared", buddy) | ||||||
| 				del self._buddies[name] | 				del self._buddies[name] | ||||||
| 		except KeyError: | 			self._handle_remove_service_for_activity(service, buddy) | ||||||
| 			pass |  | ||||||
| 
 | 
 | ||||||
| 		return False | 		return False | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,28 @@ | |||||||
| import avahi | import avahi | ||||||
| import Group | from sugar import util | ||||||
|  | import string | ||||||
|  | 
 | ||||||
|  | def new_group_service(group_name, resource): | ||||||
|  | 	"""Create a new service suitable for defining a new group.""" | ||||||
|  | 	if type(group_name) != type("") or not len(group_name): | ||||||
|  | 		raise ValueError("group name must be a valid string.") | ||||||
|  | 	if type(resource) != type("") or not len(resource): | ||||||
|  | 		raise ValueError("group resource must be a valid string.") | ||||||
|  | 
 | ||||||
|  | 	# Create a randomized service type | ||||||
|  | 	data = "%s%s" % (group_name, resource) | ||||||
|  | 	stype = "_%s_group_olpc._udp" % sugar.util.unique_id(data) | ||||||
|  | 
 | ||||||
|  | 	properties = {__GROUP_NAME_TAG: group_name, __GROUP_RESOURCE_TAG: resource } | ||||||
|  | 	owner_nick = "" | ||||||
|  | 	port = random.randint(5000, 65000) | ||||||
|  | 	# Use random currently unassigned multicast address | ||||||
|  | 	address = "232.%d.%d.%d" % (random.randint(0, 254), random.randint(1, 254), | ||||||
|  | 			random.randint(1, 254)) | ||||||
|  | 	service = Service.Service(owner_nick, stype, "local", address=address, | ||||||
|  | 			port=port, properties=properties) | ||||||
|  | 	return service | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def _txt_to_dict(txt): | def _txt_to_dict(txt): | ||||||
| 	"""Convert an avahi-returned TXT record formatted | 	"""Convert an avahi-returned TXT record formatted | ||||||
| @ -18,6 +41,21 @@ def _txt_to_dict(txt): | |||||||
| 		prop_dict[key] = value | 		prop_dict[key] = value | ||||||
| 	return prop_dict | 	return prop_dict | ||||||
| 
 | 
 | ||||||
|  | def _decompose_service_type(stype): | ||||||
|  | 	"""Break a service type into the UID and real service type, if we can.""" | ||||||
|  | 	if len(stype) < util.ACTIVITY_UID_LEN + 5: | ||||||
|  | 		return (None, stype) | ||||||
|  | 	if stype[0] != "_": | ||||||
|  | 		return (None, stype) | ||||||
|  | 	start = 1 | ||||||
|  | 	end = start + util.ACTIVITY_UID_LEN | ||||||
|  | 	if stype[end] != "_": | ||||||
|  | 		return (None, stype) | ||||||
|  | 	uid = stype[start:end] | ||||||
|  | 	if not util.validate_activity_uid(uid): | ||||||
|  | 		return (None, stype) | ||||||
|  | 	return (uid, stype[end:]) | ||||||
|  | 
 | ||||||
| def is_multicast_address(address): | def is_multicast_address(address): | ||||||
| 	"""Simple numerical check for whether an IP4 address | 	"""Simple numerical check for whether an IP4 address | ||||||
| 	is in the range for multicast addresses or not.""" | 	is in the range for multicast addresses or not.""" | ||||||
| @ -31,12 +69,10 @@ def is_multicast_address(address): | |||||||
| 	return False | 	return False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| __GROUP_UID_TAG = "GroupUID" |  | ||||||
| 
 |  | ||||||
| class Service(object): | class Service(object): | ||||||
| 	"""Encapsulates information about a specific ZeroConf/mDNS | 	"""Encapsulates information about a specific ZeroConf/mDNS | ||||||
| 	service as advertised on the network.""" | 	service as advertised on the network.""" | ||||||
| 	def __init__(self, name, stype, domain, address=None, port=-1, properties=None, group=None): | 	def __init__(self, name, stype, domain, address=None, port=-1, properties=None): | ||||||
| 		# Validate immutable options | 		# Validate immutable options | ||||||
| 		if not name or (type(name) != type("") and type(name) != type(u"")) or not len(name): | 		if not name or (type(name) != type("") and type(name) != type(u"")) or not len(name): | ||||||
| 			raise ValueError("must specify a valid service name.") | 			raise ValueError("must specify a valid service name.") | ||||||
| @ -51,15 +87,13 @@ class Service(object): | |||||||
| 		if len(domain) and domain != "local" and domain != u"local": | 		if len(domain) and domain != "local" and domain != u"local": | ||||||
| 			raise ValueError("must use the 'local' domain (for now).") | 			raise ValueError("must use the 'local' domain (for now).") | ||||||
| 
 | 
 | ||||||
| 		# Group services must have multicast addresses | 		(uid, real_stype) = _decompose_service_type(stype) | ||||||
| 		if Group.is_group_service_type(stype) and address and not is_multicast_address(address): | 		if uid and not util.validate_activity_uid(activity_uid): | ||||||
| 			raise ValueError("group service type specified, but address was not multicast.") | 			raise ValueError("activity_uid not a valid activity UID.") | ||||||
| 
 |  | ||||||
| 		if group and not isinstance(group, Group.Group): |  | ||||||
| 			raise ValueError("group was not a valid group object.") |  | ||||||
| 		 | 		 | ||||||
| 		self._name = name | 		self._name = name | ||||||
| 		self._stype = stype | 		self._stype = stype | ||||||
|  | 		self._real_stype = real_stype | ||||||
| 		self._domain = domain | 		self._domain = domain | ||||||
| 		self._address = None | 		self._address = None | ||||||
| 		self.set_address(address) | 		self.set_address(address) | ||||||
| @ -67,9 +101,7 @@ class Service(object): | |||||||
| 		self.set_port(port) | 		self.set_port(port) | ||||||
| 		self._properties = {} | 		self._properties = {} | ||||||
| 		self.set_properties(properties) | 		self.set_properties(properties) | ||||||
| 		self._group = group | 		self._activity_uid = uid | ||||||
| 		if group: |  | ||||||
| 			self._properties[__GROUP_UID_TAG] = group.get_uid() |  | ||||||
| 
 | 
 | ||||||
| 	def get_name(self): | 	def get_name(self): | ||||||
| 		"""Return the service's name, usually that of the | 		"""Return the service's name, usually that of the | ||||||
| @ -81,11 +113,6 @@ class Service(object): | |||||||
| 		False if it is not.""" | 		False if it is not.""" | ||||||
| 		return is_multicast_address(self._address) | 		return is_multicast_address(self._address) | ||||||
| 
 | 
 | ||||||
| 	def is_group_service(self): |  | ||||||
| 		"""Return True if the service represents a Group, |  | ||||||
| 		False if it does not.""" |  | ||||||
| 		return Group.is_group_service_type(self._stype) |  | ||||||
| 
 |  | ||||||
| 	def get_one_property(self, key): | 	def get_one_property(self, key): | ||||||
| 		"""Return one property of the service, or None | 		"""Return one property of the service, or None | ||||||
| 		if the property was not found.  Cannot distinguish | 		if the property was not found.  Cannot distinguish | ||||||
| @ -131,17 +158,15 @@ class Service(object): | |||||||
| 				raise ValueError("must specify a valid address.") | 				raise ValueError("must specify a valid address.") | ||||||
| 			if not len(address): | 			if not len(address): | ||||||
| 				raise ValueError("must specify a valid address.") | 				raise ValueError("must specify a valid address.") | ||||||
| 			if Group.is_group_service_type(self._stype) and not is_multicast_address(address): |  | ||||||
| 				raise ValueError("group service type specified, but address was not multicast.") |  | ||||||
| 		self._address = address | 		self._address = address | ||||||
| 
 | 
 | ||||||
| 	def get_domain(self): | 	def get_domain(self): | ||||||
| 		"""Return the ZeroConf/mDNS domain the service was found in.""" | 		"""Return the ZeroConf/mDNS domain the service was found in.""" | ||||||
| 		return self._domain | 		return self._domain | ||||||
| 
 | 
 | ||||||
| 	def get_group(self): | 	def get_activity_uid(self): | ||||||
| 		"""Return the group this service is associated with, if any.""" | 		"""Return the activity UID this service is associated with, if any.""" | ||||||
| 		return self._group | 		return self._activity_uid | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ################################################################# | ################################################################# | ||||||
|  | |||||||
| @ -19,3 +19,20 @@ def _sha_data(data): | |||||||
| def unique_id(data = ''): | def unique_id(data = ''): | ||||||
| 	data_string = "%s%s%s" % (time.time(), random.randint(10000, 100000), data) | 	data_string = "%s%s%s" % (time.time(), random.randint(10000, 100000), data) | ||||||
| 	return _stringify_sha(_sha_data(data_string)) | 	return _stringify_sha(_sha_data(data_string)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ACTIVITY_UID_LEN = 40 | ||||||
|  | 
 | ||||||
|  | def is_hex(s): | ||||||
|  | 	return s.strip(string.hexdigits) == ''	 | ||||||
|  | 
 | ||||||
|  | def validate_activity_uid(uid): | ||||||
|  | 	"""Validate an activity UID.""" | ||||||
|  | 	if type(uid) != type(""): | ||||||
|  | 		return False | ||||||
|  | 	if len(uid) != ACTIVITY_UID_LEN: | ||||||
|  | 		return False | ||||||
|  | 	if not is_hex(uid): | ||||||
|  | 		return False | ||||||
|  | 	return True | ||||||
|  | 
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Dan Williams
						Dan Williams