More presence service rework

This commit is contained in:
Dan Williams 2006-06-12 18:31:26 -04:00
parent d931dca579
commit 17c371119d
10 changed files with 381 additions and 224 deletions

View File

@ -26,4 +26,5 @@ sugar/p2p/Makefile
sugar/p2p/model/Makefile sugar/p2p/model/Makefile
sugar/shell/Makefile sugar/shell/Makefile
sugar/session/Makefile sugar/session/Makefile
sugar/presence/Makefile
]) ])

View File

@ -1,4 +1,4 @@
SUBDIRS = chat browser p2p shell session SUBDIRS = chat browser p2p shell session presence
bin_SCRIPTS = sugar bin_SCRIPTS = sugar

View File

@ -445,26 +445,11 @@ class BuddyChat(Chat):
del self._chats[self._buddy] del self._chats[self._buddy]
class BuddyIconRequestHandler(object):
def __init__(self, group, stream):
self._group = group
self._stream = stream
self._stream.register_handler(self._handle_buddy_icon_request, "get_buddy_icon")
def _handle_buddy_icon_request(self):
"""XMLRPC method, return the owner's icon encoded with base64."""
icon = self._group.get_owner().get_icon()
if icon:
return base64.b64encode(icon)
return ''
class GroupChat(Chat): class GroupChat(Chat):
def __init__(self): def __init__(self):
self._group = Group.get_from_id('local') self._group = Group.get_from_id('local')
self._act_name = "Chat" self._act_name = "Chat"
self._chats = {} self._chats = {}
self._buddy_icon_tries = 0
Chat.__init__(self, self) Chat.__init__(self, self)
@ -482,7 +467,6 @@ class GroupChat(Chat):
# specific buddy chats # specific buddy chats
buddy_service = Service(name, CHAT_SERVICE_TYPE, CHAT_SERVICE_PORT) buddy_service = Service(name, CHAT_SERVICE_TYPE, CHAT_SERVICE_PORT)
self._buddy_stream = Stream.new_from_service(buddy_service, self._group) self._buddy_stream = Stream.new_from_service(buddy_service, self._group)
self._buddy_icon_handler = BuddyIconRequestHandler(self._group, self._buddy_stream)
self._buddy_stream.set_data_listener(getattr(self, "_buddy_recv_message")) self._buddy_stream.set_data_listener(getattr(self, "_buddy_recv_message"))
buddy_service.register(self._group) buddy_service.register(self._group)

View File

@ -1,44 +1,49 @@
import xmlrpclib import xmlrpclib
import socket import socket
import traceback import traceback
import random
import network import network
from MostlyReliablePipe import MostlyReliablePipe from MostlyReliablePipe import MostlyReliablePipe
from sugar.presence import Service
class Stream(object): class Stream(object):
def __init__(self, service, group): def __init__(self, service):
if not service: if not isinstance(service, Service.Service):
raise ValueError("service must be valid") raise ValueError("service must be valid.")
if not service.get_port():
raise ValueError("service must have an address.")
self._service = service self._service = service
self._group = group self._reader_port = self._service.get_port()
self._owner_nick_name = self._group.get_owner().get_nick_name() self._writer_port = self._reader_port
self._port = self._service.get_port()
self._address = self._service.get_address() self._address = self._service.get_address()
self._callback = None self._callback = None
def new_from_service(service, group): def new_from_service(service, start_reader=True):
if service.is_multicast(): if not isinstance(service, Service.Service):
return MulticastStream(service, group) raise ValueError("service must be valid.")
if service.is_multicast_service():
return MulticastStream(service)
else: else:
return UnicastStream(service, group) return UnicastStream(service, start_reader)
new_from_service = staticmethod(new_from_service) new_from_service = staticmethod(new_from_service)
def set_data_listener(self, callback): def set_data_listener(self, callback):
self._callback = callback self._callback = callback
def recv(self, nick_name, data): def _recv(self, address, data):
if nick_name != self._owner_nick_name: if self._callback:
if self._callback: self._callback(data)
self._callback(self._group.get_buddy(nick_name), data)
class UnicastStreamWriter(object): class UnicastStreamWriter(object):
def __init__(self, stream, service, owner_nick_name): def __init__(self, stream, service):
# set up the writer # set up the writer
if not service: if not isinstance(service, Service.Service):
raise ValueError("service must be valid") raise ValueError("service must be valid")
self._service = service self._service = service
self._owner_nick_name = owner_nick_name if not service.get_address():
raise ValueError("service must have a valid address.")
self._address = self._service.get_address() self._address = self._service.get_address()
self._port = self._service.get_port() self._port = self._service.get_port()
self._xmlrpc_addr = "http://%s:%d" % (self._address, self._port) self._xmlrpc_addr = "http://%s:%d" % (self._address, self._port)
@ -47,7 +52,7 @@ class UnicastStreamWriter(object):
def write(self, xmlrpc_data): def write(self, xmlrpc_data):
"""Write some data to the default endpoint of this pipe on the remote server.""" """Write some data to the default endpoint of this pipe on the remote server."""
try: try:
self._writer.message(None, None, self._owner_nick_name, xmlrpc_data) self._writer.message(None, None, xmlrpc_data)
return True return True
except (socket.error, xmlrpclib.Fault, xmlrpclib.ProtocolError): except (socket.error, xmlrpclib.Fault, xmlrpclib.ProtocolError):
traceback.print_exc() traceback.print_exc()
@ -65,58 +70,79 @@ class UnicastStreamWriter(object):
class UnicastStream(Stream): class UnicastStream(Stream):
def __init__(self, service, group): def __init__(self, service, start_reader=True):
Stream.__init__(self, service, group) """Initializes the stream. If the 'start_reader' argument is True,
self._setup() the stream will initialize and start a new stream reader, if it
is False, no reader will be created and the caller must call the
start_reader() method to start the stream reader and be able to
receive any data from the stream."""
Stream.__init__(self, service)
if start_reader:
self.start_reader()
def _setup(self): def start_reader(self, update_service_port=True):
"""Start the stream's reader, which for UnicastStream objects is
and XMLRPC server. If there's a port conflict with some other
service, the reader will try to find another port to use instead.
Returns the port number used for the reader."""
# Set up the reader # Set up the reader
started = False started = False
tries = 10 tries = 10
port = self._service.get_port()
self._reader = None self._reader = None
while not started and tries > 0: while not started and tries > 0:
try: try:
self._reader = network.GlibXMLRPCServer(("", port)) self._reader = network.GlibXMLRPCServer(("", self._reader_port))
self._reader.register_function(self._message, "message") self._reader.register_function(self._message, "message")
if update_service_port:
self._service.set_port(self._reader_port) # Update the service's port
started = True started = True
except(socket.error): except(socket.error):
port = port + 1 self._reader_port = random.randint(self._reader_port + 1, 65500)
tries = tries - 1 tries = tries - 1
if self._reader is None: if self._reader is None:
print 'Could not start xmlrpc server.' print 'Could not start stream reader.'
self._service.set_port(port) return self._reader_port
def _message(self, nick_name, message): def _message(self, message):
"""Called by the XMLRPC server when network data arrives.""" """Called by the XMLRPC server when network data arrives."""
self.recv(nick_name, message) address = network.get_authinfo()
self._recv(address, message)
return True return True
def register_handler(self, handler, name): def register_reader_handler(self, handler, name):
"""Register a custom message handler with the reader. This call
adds a custom XMLRPC method call with the name 'name' to the reader's
XMLRPC server, which then calls the 'handler' argument back when
a method call for it arrives over the network."""
if name == "message": if name == "message":
raise ValueError("Handler name 'message' is a reserved handler.") raise ValueError("Handler name 'message' is a reserved handler.")
self._reader.register_function(handler, name) self._reader.register_function(handler, name)
def new_writer(self, service): def new_writer(self, service):
return UnicastStreamWriter(self, service, self._owner_nick_name) """Return a new stream writer object."""
return UnicastStreamWriter(self, service)
class MulticastStream(Stream): class MulticastStream(Stream):
def __init__(self, service, group): def __init__(self, service):
Stream.__init__(self, service, group) Stream.__init__(self, service)
self._address = self._service.get_group_address() self._internal_start_reader()
self._setup()
def _setup(self): def start_reader(self):
self._pipe = MostlyReliablePipe('', self._address, self._port, self._recv_data_cb) return self._reader_port
def _internal_start_reader(self):
if not service.get_address():
raise ValueError("service must have a valid address.")
self._pipe = MostlyReliablePipe('', self._address, self._reader_port,
self._recv_data_cb)
self._pipe.start() self._pipe.start()
def write(self, data): def write(self, data):
self._pipe.send(self._owner_nick_name + " |**| " + data) self._pipe.send(data)
def _recv_data_cb(self, addr, data, user_data=None): def _recv_data_cb(self, address, data, user_data=None):
[ nick_name, data ] = data.split(" |**| ", 2) self._recv(address, data)
self.recv(nick_name, data)
def new_writer(self, service=None): def new_writer(self, service=None):
return self return self

View File

@ -1,46 +1,108 @@
import pwd import pwd
import os import os
import base64
import pygtk import pygtk
pygtk.require('2.0') pygtk.require('2.0')
import gtk import gtk, gobject
from sugar.p2p import Stream
from sugar.p2p import network
#from sugar import env
PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp" PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp"
class Buddy(object): class Buddy(gobject.GObject):
"""Represents another person on the network and keeps track of the """Represents another person on the network and keeps track of the
activities and resources they make available for sharing.""" activities and resources they make available for sharing."""
__gsignals__ = {
'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([])),
'service-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_PYOBJECT])),
'service-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_PYOBJECT]))
}
def __init__(self, service): def __init__(self, service):
gobject.GObject.__init__(self)
self._services = {} self._services = {}
self._nick_name = service.get_name() self._nick_name = service.get_name()
self._address = service.get_address() self._address = service.get_address()
self._valid = False self._valid = False
self._icon = None self._icon = None
self._icon_tries = 0
self._owner = False
self.add_service(service) self.add_service(service)
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): def add_service(self, service):
"""Adds a new service to this buddy's service list.""" """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
if service.get_address() != self._address:
return False
if service.get_type() in self._services.keys(): if service.get_type() in self._services.keys():
return return False
self._services.keys[service.get_type()] = service self._services[service.get_type()] = service
# FIXME: send out signal for new service found if self._valid:
self.emit("service-added", service)
if service.get_type() == PRESENCE_SERVICE_TYPE: if service.get_type() == PRESENCE_SERVICE_TYPE:
# A buddy isn't valid until its official presence # A buddy isn't valid until its official presence
# service has been found and resolved # service has been found and resolved
self._valid = True self._valid = True
self._request_buddy_icon(service)
return True
def remove_service(self, service): def remove_service(self, service):
"""Remove a service from a buddy; ie, the activity was closed """Remove a service from a buddy; ie, the activity was closed
or the buddy went away.""" or the buddy went away."""
if service.get_type() in self._services.keys(): if service.get_address() != self._address:
return
if service.get_name() != self._nick_name:
return
if self._services.has_key(service.get_type()):
if self._valid:
self.emit("service-removed", service)
del self._services[service.get_type()] del self._services[service.get_type()]
if service.get_type() == PRESENCE_SERVICE_TYPE: if service.get_type() == PRESENCE_SERVICE_TYPE:
self._valid = False self._valid = False
def get_service_of_type(self, stype):
"""Return a service of a certain type, or None if the buddy
doesn't provide that service."""
if self._services.has_key(stype):
return self._services[stype]
return None
def is_valid(self): def is_valid(self):
"""Return whether the buddy is valid or not. A buddy is """Return whether the buddy is valid or not. A buddy is
not valid until its official presence service has been found not valid until its official presence service has been found
@ -63,65 +125,23 @@ class Buddy(object):
def get_address(self): def get_address(self):
return self._address return self._address
def add_service(self, service):
if service.get_name() != self._nick_name:
return False
if service.get_address() != self._address:
return False
if self._services.has_key(service.get_type()):
return False
self._services[service.get_type()] = service
def remove_service(self, stype):
if self._services.has_key(stype):
del self._services[stype]
def get_service(self, stype):
if self._services.has_key(stype):
return self._services[stype]
return None
def get_nick_name(self): def get_nick_name(self):
return self._nick_name return self._nick_name
def set_icon(self, icon): def set_icon(self, icon):
"""Can only set icon for other buddies. The Owner """Can only set icon for other buddies. The Owner
takes care of setting it's own icon.""" takes care of setting it's own icon."""
self._icon = icon if icon != self._icon:
# FIXME: do callbacks for icon-changed self._icon = icon
self.emit("icon-changed")
def is_owner(self):
return self._owner
class Owner(Buddy): class Owner(Buddy):
"""Class representing the owner of this machine/instance.""" """Class representing the owner of the machine. This is the client
def __init__(self): portion of the Owner, paired with the server portion in Owner.py."""
nick = env.get_nick_name() def __init__(self, service):
if not nick: Buddy.__init__(self, service)
nick = pwd.getpwuid(os.getuid())[0] self._owner = True
if not nick or not len(nick):
nick = "n00b"
Buddy.__init__(self)
user_dir = env.get_user_dir()
if not os.path.exists(user_dir):
try:
os.makedirs(user_dir)
except OSError:
print 'Could not create user directory.'
for fname in os.listdir(user_dir):
if not fname.startswith("buddy-icon."):
continue
fd = open(os.path.join(user_dir, fname), "r")
self._icon = fd.read()
fd.close()
break
def set_icon(self, icon):
"""Can only set icon in constructor for now."""
pass
def add_service(self, service):
"""Do nothing here, since all services we need to know about
are registered with us by our group."""
pass

View File

@ -2,16 +2,40 @@ 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
ACTION_SERVICE_APPEARED = 'appeared' def _get_local_ip_address(ifname):
ACTION_SERVICE_DISAPPEARED = 'disappeared' """Call Linux specific bits to retrieve our own IP address."""
import socket
import sys
import fcntl
class PresenceService(object): 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 PresenceService(gobject.GObject):
"""Object providing information about the presence of Buddies """Object providing information about the presence of Buddies
and what activities they make available to others.""" and what activities they make available to others."""
__gsignals__ = {
'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_PYOBJECT])),
'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_PYOBJECT]))
}
__lock = threading.Lock() __lock = threading.Lock()
__instance = None __instance = None
@ -26,12 +50,20 @@ class PresenceService(object):
get_instance = staticmethod(get_instance) get_instance = staticmethod(get_instance)
def __init__(self, debug=False): def __init__(self, debug=False):
gobject.GObject.__init__(self)
self._debug = debug self._debug = debug
self._lock = threading.Lock() self._lock = threading.Lock()
self._started = False self._started = False
# interface -> IP address: interfaces we've gotten events on so far
self._local_addrs = {}
# nick -> Buddy: buddies we've found # nick -> Buddy: buddies we've found
self._buddies = {} self._buddies = {}
# Our owner object
self._owner = None
# group UID -> Group: groups we've found # group UID -> Group: groups we've found
self._groups = {} self._groups = {}
@ -77,6 +109,10 @@ class PresenceService(object):
if self._debug: if self._debug:
print "PresenceService(%d): %s" % (os.getpid(), msg) print "PresenceService(%d): %s" % (os.getpid(), msg)
def get_owner(self):
"""Return the owner of this machine/instance, if we've recognized them yet."""
return self._owner
def _resolve_service_error_handler(self, err): def _resolve_service_error_handler(self, err):
self._log("error resolving service: %s" % err) self._log("error resolving service: %s" % err)
@ -97,6 +133,49 @@ class PresenceService(object):
found.append(service) found.append(service)
return found return found
def _is_special_service_type(self, stype):
"""Return True if the service type is a special, internal service
type, and False if it's not."""
if stype == Buddy.PRESENCE_SERVICE_TYPE:
return True
if Group.is_group_service_type(stype):
return True
return False
def _handle_new_service_for_buddy(self, service):
"""Deal with a new discovered service object."""
# Once a service is resolved, we match it up to an existing buddy,
# or create a new Buddy if this is the first service known about the buddy
added = was_valid = False
name = service.get_name()
buddy = None
try:
buddy = self._buddies[name]
was_valid = buddy.is_valid()
added = buddy.add_service(service)
except KeyError:
# Should this service mark the owner?
if service.get_address() in self._local_addrs.values():
buddy = Buddy.Owner(service)
self._owner = buddy
else:
buddy = Buddy.Buddy(service)
self._buddies[name] = buddy
added = True
if not was_valid and buddy.is_valid():
self.emit("buddy-appeared", buddy)
return buddy
def _handle_new_service_for_group(self, service, buddy):
# If the serivce is a group service, merge it into our groups list
if not buddy:
return
group = None
if not self._groups.has_key(service.get_type()):
group = Group.Group(service)
else:
group = self._groups[service.get_type()]
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
service to assign it to a buddy.""" service to assign it to a buddy."""
@ -108,7 +187,6 @@ class PresenceService(object):
stype=stype, domain=domain) stype=stype, domain=domain)
if not len(found): if not len(found):
return False return False
for service in found: for service in found:
self._unresolved_services.remove(service) self._unresolved_services.remove(service)
@ -118,23 +196,11 @@ class PresenceService(object):
service.set_port(port) service.set_port(port)
service.set_properties(txt) service.set_properties(txt)
# Once a service is resolved, we match it up to an existing buddy, # Merge the service into our buddy and group lists, if needed
# or create a new Buddy if this is the first service known about the buddy buddy = self._handle_new_service_for_buddy(service)
added = was_valid = False if service.is_group_service():
try: self._handle_new_service_for_group(service, buddy)
buddy = self._buddies[name]
was_valid = buddy.is_valid()
added = buddy.add_service(service)
except KeyError:
buddy = Buddy.Buddy(service)
self._buddies[name] = buddy
added = True
if not was_valid and buddy.is_valid():
# FIXME: send out "new buddy" signals
pass
if added:
# FIXME: send out buddy service added signals
pass
return False return False
def _resolve_service_reply_cb_glue(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): def _resolve_service_reply_cb_glue(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags):
@ -160,8 +226,16 @@ class PresenceService(object):
service = Service.Service(name, stype, domain) service = Service.Service(name, stype, domain)
self._unresolved_services.append(service) self._unresolved_services.append(service)
# 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
# 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 stype == Buddy.PRESENCE_SERVICE_TYPE: if stype in self._allowed_service_types or self._is_special_service_type(stype):
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
@ -170,24 +244,19 @@ class PresenceService(object):
def _service_disappeared_cb(self, interface, protocol, name, stype, domain, flags): def _service_disappeared_cb(self, interface, protocol, name, stype, domain, flags):
self._log("service '%s' of type '%s' in domain '%s' on %i.%i disappeared." % (name, stype, domain, interface, protocol)) self._log("service '%s' of type '%s' in domain '%s' on %i.%i disappeared." % (name, stype, domain, interface, protocol))
# Remove the service from our unresolved services list
found = self._find_service(self._unresolved_services, name=name,
stype=stype, domain=domain)
buddy = None
try: try:
# Remove the service from the buddy
buddy = self._buddies[name] buddy = self._buddies[name]
# FIXME: need to be more careful about how we remove services
# from buddies; this could be spoofed
service = buddy.get_service_of_type(stype)
buddy.remove_service(service)
if not buddy.is_valid():
self.emit("buddy-disappeared", buddy)
del self._buddies[name]
except KeyError: except KeyError:
pass pass
# Remove the service from the buddy
if buddy:
buddy.remove_service(found[0])
# FIXME: send buddy service remove signals
if not buddy.is_valid():
del self._buddies[name]
# FIXME: send buddy disappeared message
for service in found: for service in found:
self._unresolved_services.remove(service) self._unresolved_services.remove(service)
return False return False
@ -246,9 +315,11 @@ class PresenceService(object):
def track_service_type(self, stype): def track_service_type(self, stype):
"""Requests that the Presence service look for and recognize """Requests that the Presence service look for and recognize
a certain mDNS service types.""" a certain mDNS service types."""
if not self._started:
raise RuntimeError("presence service must be started first.")
if not type(stype) == type(""): if not type(stype) == type(""):
raise ValueError("service type must be a string.") raise ValueError("service type must be a string.")
if stype == Buddy.PRESENCE_SERVICE_TYPE: if self._is_special_service_type(stype):
return return
if stype in self._allowed_service_types: if stype in self._allowed_service_types:
return return
@ -263,6 +334,8 @@ class PresenceService(object):
def untrack_service_type(self, stype): def untrack_service_type(self, stype):
"""Stop tracking a certain mDNS service.""" """Stop tracking a certain mDNS service."""
if not self._started:
raise RuntimeError("presence service must be started first.")
if not type(stype) == type(""): if not type(stype) == type(""):
raise ValueError("service type must be a string.") raise ValueError("service type must be a string.")
if name in self._allowed_service_types: if name in self._allowed_service_types:
@ -270,19 +343,25 @@ class PresenceService(object):
def register_service(self, service): def register_service(self, service):
"""Register a new service, advertising it to other Buddies on the network.""" """Register a new service, advertising it to other Buddies on the network."""
if not self._started:
raise RuntimeError("presence service must be started first.")
rs_name = service.get_name() rs_name = service.get_name()
rs_stype = service.get_type() rs_stype = service.get_type()
rs_port = service.get_port() rs_port = service.get_port()
if type(rs_port) != type(1) and rs_port <= 1024: if type(rs_port) != type(1) and rs_port <= 1024:
raise ValueError("invalid service port.") raise ValueError("invalid service port.")
rs_props = service.get_properties() rs_props = service.get_properties()
rs_domain = service.get_domain()
if not rs_domain or not len(rs_domain):
rs_domain = ""
self._log("registered service name '%s' type '%s' on port %d with args %s" % (rs_name, rs_stype, rs_port, rs_props)) self._log("registered service name '%s' type '%s' on port %d with args %s" % (rs_name, rs_stype, rs_port, rs_props))
try: try:
group = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, self._server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) group = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, self._server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP)
info = ["%s=%s" % (k, v) for k, v in rs_props.items()] info = ["%s=%s" % (k, v) for k, v in rs_props.items()]
group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, rs_name, rs_stype, group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, rs_name, rs_stype,
"", "", # domain, host (let the system figure it out) rs_domain, "", # let Avahi figure the 'host' out
dbus.UInt16(rs_port), info,) dbus.UInt16(rs_port), info,)
group.Commit() group.Commit()
except dbus.dbus_bindings.DBusException, exc: except dbus.dbus_bindings.DBusException, exc:
@ -295,11 +374,19 @@ class PresenceService(object):
return group return group
def get_buddy_by_nick_name(self, nick_name): def get_buddy_by_nick_name(self, nick_name):
"""Look up and return a buddy by nickname."""
if self._buddies.has_key(nick_name): if self._buddies.has_key(nick_name):
return self._buddies[nick_name] return self._buddies[nick_name]
return None 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_buddies(self): def get_buddies(self):
"""Return the entire buddy list."""
return self._buddies.values() return self._buddies.values()
################################################################# #################################################################
@ -347,6 +434,7 @@ class PresenceServiceTestCase(unittest.TestCase):
buddy = ps.get_buddy_by_nick_name("Paul") buddy = ps.get_buddy_by_nick_name("Paul")
assert buddy, "The registered buddy was not found after 2 seconds!" assert buddy, "The registered buddy was not found after 2 seconds!"
assert buddy.is_valid(), "The buddy was invalid, since no presence was advertised." assert buddy.is_valid(), "The buddy was invalid, since no presence was advertised."
assert buddy.is_owner() == True, "The buddy was not the owner, but it should be!"
def addToSuite(suite): def addToSuite(suite):
suite.addTest(PresenceServiceTestCase("testNoServices")) suite.addTest(PresenceServiceTestCase("testNoServices"))

View File

@ -1,4 +1,5 @@
import avahi import avahi
import Group
def _txt_to_dict(txt): def _txt_to_dict(txt):
"""Convert an avahi-returned TXT record formatted """Convert an avahi-returned TXT record formatted
@ -17,6 +18,18 @@ def _txt_to_dict(txt):
prop_dict[key] = value prop_dict[key] = value
return prop_dict return prop_dict
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
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."""
@ -30,11 +43,15 @@ class Service(object):
if not stype.endswith("._tcp") and not stype.endswith("._udp"): if not stype.endswith("._tcp") and not stype.endswith("._udp"):
raise ValueError("must specify a TCP or UDP service type.") raise ValueError("must specify a TCP or UDP service type.")
if not domain or (type(domain) != type("") and type(domain) != type(u"")): if type(domain) != type("") and type(domain) != type(u""):
raise ValueError("must specify a domain.") raise ValueError("must specify a domain.")
if 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
if Group.is_group_service_type(stype) and address and not _is_multicast_address(address):
raise ValueError("group service type specified, but address was not multicast.")
self._name = name self._name = name
self._stype = stype self._stype = stype
self._domain = domain self._domain = domain
@ -46,17 +63,38 @@ class Service(object):
self.set_properties(properties) self.set_properties(properties)
def get_name(self): def get_name(self):
"""Return the service's name, usually that of the
buddy who provides it."""
return self._name 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 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
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(): if key in self._properties.keys():
return self._properties[key] return self._properties[key]
return None return None
def get_properties(self): def get_properties(self):
"""Return a python dictionary of all the service's
properties."""
return self._properties return self._properties
def set_properties(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 = {} self._properties = {}
if type(properties) == type([]): if type(properties) == type([]):
self._properties = _txt_to_dict(properties) self._properties = _txt_to_dict(properties)
@ -64,6 +102,7 @@ class Service(object):
self._properties = properties self._properties = properties
def get_type(self): def get_type(self):
"""Return the service's service type."""
return self._stype return self._stype
def get_port(self): def get_port(self):
@ -83,16 +122,14 @@ 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 self._domain return self._domain
def is_olpc_service(self):
if self._stype.endswith("._olpc._udp") or self._stype.endswith(".olpc._tcp"):
return True
return False
################################################################# #################################################################
# Tests # Tests
@ -138,6 +175,10 @@ class ServiceTestCase(unittest.TestCase):
# Only accept local for now # 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, "foobar", self._DEF_ADDRESS,
self._DEF_PORT, self._DEF_PROPS, "invalid domain") self._DEF_PORT, self._DEF_PROPS, "invalid domain")
# Make sure "" works
service = Service(self._DEF_NAME, self._DEF_STYPE, "", self._DEF_ADDRESS,
self._DEF_PORT, self._DEF_PROPS)
assert service, "Empty domain was not accepted!"
def testAddress(self): def testAddress(self):
self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, [], self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, [],
@ -186,6 +227,30 @@ class ServiceTestCase(unittest.TestCase):
value = service.get_one_property(key) value = service.get_one_property(key)
assert value is not None and value == expected_value, "service properties weren't correct after init." 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 = "_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
service = Service(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!"
# Set address to invalid multicast address
try:
service.set_address(self._DEF_ADDRESS)
except ValueError, exc:
pass
else:
self.fail("expected a ValueError for invalid address.")
# Valid group service type and multicast address, ensure it works
mc_addr = "224.0.0.34"
service = Service(self._DEF_NAME, group_stype, self._DEF_DOMAIN, mc_addr,
self._DEF_PORT, self._DEF_PROPS)
assert service.get_address() == mc_addr, "address was not expected address!"
def addToSuite(suite): def addToSuite(suite):
suite.addTest(ServiceTestCase("testName")) suite.addTest(ServiceTestCase("testName"))
suite.addTest(ServiceTestCase("testType")) suite.addTest(ServiceTestCase("testType"))
@ -195,6 +260,7 @@ class ServiceTestCase(unittest.TestCase):
suite.addTest(ServiceTestCase("testGoodInit")) suite.addTest(ServiceTestCase("testGoodInit"))
suite.addTest(ServiceTestCase("testAvahiProperties")) suite.addTest(ServiceTestCase("testAvahiProperties"))
suite.addTest(ServiceTestCase("testBoolProperty")) suite.addTest(ServiceTestCase("testBoolProperty"))
suite.addTest(ServiceTestCase("testGroupService"))
addToSuite = staticmethod(addToSuite) addToSuite = staticmethod(addToSuite)

View File

@ -3,4 +3,5 @@ sugar_PYTHON = \
__init__.py \ __init__.py \
activity.py \ activity.py \
shell.py \ shell.py \
PresenceWindow.py PresenceWindow.py \
Owner.py

View File

@ -3,8 +3,8 @@ pygtk.require('2.0')
import gtk import gtk
import gobject import gobject
from sugar.p2p.Group import Group
from sugar.p2p.Stream import Stream from sugar.p2p.Stream import Stream
from sugar.presence.PresenceService import PresenceService
class PresenceWindow(gtk.Window): class PresenceWindow(gtk.Window):
_MODEL_COL_NICK = 0 _MODEL_COL_NICK = 0
@ -14,10 +14,11 @@ class PresenceWindow(gtk.Window):
def __init__(self): def __init__(self):
gtk.Window.__init__(self) gtk.Window.__init__(self)
self._group = Group.get_from_id('local') self._pservice = PresenceService.get_instance()
self._group.add_presence_listener(self._on_group_presence_event) self._pservice.connect("buddy-appeared", self._on_buddy_appeared_cb)
self._group.add_service_listener(self._on_group_service_event) self._pservice.connect("buddy-disappeared", self._on_buddy_disappeared_cb)
self._group.join() self._pservice.set_debug(True)
self._pservice.start()
self._setup_ui() self._setup_ui()
@ -79,62 +80,25 @@ class PresenceWindow(gtk.Window):
self._chats[buddy] = chat self._chats[buddy] = chat
chat.connect_to_shell() chat.connect_to_shell()
def _request_buddy_icon_cb(self, result_status, response, user_data):
icon = response
buddy = 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" % (buddy.get_nick_name(), len(icon))
buddy.set_icon(icon)
if (result_status == network.RESULT_FAILED or not icon) and self._buddy_icon_tries < 3:
self._buddy_icon_tries = self._buddy_icon_tries + 1
print "Failed to retrieve buddy icon for '%s' on try %d of %d" % (buddy.get_nick_name(), \
self._buddy_icon_tries, 3)
gobject.timeout_add(1000, self._request_buddy_icon, buddy)
return False
def _request_buddy_icon(self, buddy):
# FIXME need to use the new presence service when it's done
service = buddy.get_service('_olpc_chat._tcp')
buddy_stream = Stream.new_from_service(service, self._group)
writer = buddy_stream.new_writer(service)
icon = writer.custom_request("get_buddy_icon", self._request_buddy_icon_cb, buddy)
def _on_group_service_event(self, action, service):
if action == Group.SERVICE_ADDED:
# Look for the olpc chat service
# FIXME need to use the new presence service when it's done
if service.get_type() == '_olpc_chat._tcp':
# Find the buddy this service belongs to
buddy = self._group.get_buddy(service.get_name())
if buddy and buddy.get_address() == service.get_address():
# Try to get the buddy's icon
if buddy.get_nick_name() != self._group.get_owner().get_nick_name():
print "Requesting buddy icon from '%s'." % buddy.get_nick_name()
gobject.idle_add(self._request_buddy_icon, buddy)
elif action == Group.SERVICE_REMOVED:
pass
def __buddy_icon_changed_cb(self, buddy): def __buddy_icon_changed_cb(self, buddy):
it = self._get_iter_for_buddy(buddy) it = self._get_iter_for_buddy(buddy)
self._buddy_list_model.set(it, self._MODEL_COL_ICON, buddy.get_icon_pixbuf()) self._buddy_list_model.set(it, self._MODEL_COL_ICON, buddy.get_icon_pixbuf())
def _on_group_presence_event(self, action, buddy): def _on_buddy_appeared_cb(self, pservice, buddy):
if buddy.get_nick_name() == self._group.get_owner().get_nick_name(): if buddy.is_owner():
# Do not show ourself in the buddy list # Do not show ourself in the buddy list
pass return
elif action == Group.BUDDY_JOIN:
aniter = self._buddy_list_model.append(None) aniter = self._buddy_list_model.append(None)
self._buddy_list_model.set(aniter, self._buddy_list_model.set(aniter,
self._MODEL_COL_NICK, buddy.get_nick_name(), self._MODEL_COL_NICK, buddy.get_nick_name(),
self._MODEL_COL_BUDDY, buddy) self._MODEL_COL_BUDDY, buddy)
buddy.connect('icon-changed', self.__buddy_icon_changed_cb) buddy.connect('icon-changed', self.__buddy_icon_changed_cb)
elif action == Group.BUDDY_LEAVE:
aniter = self._get_iter_for_buddy(buddy) def _on_buddy_disappeared_cb(self, pservice, buddy):
if aniter: aniter = self._get_iter_for_buddy(buddy)
self._buddy_list_model.remove(aniter) if aniter:
self._buddy_list_model.remove(aniter)
def _get_iter_for_buddy(self, buddy): def _get_iter_for_buddy(self, buddy):
aniter = self._buddy_list_model.get_iter_first() aniter = self._buddy_list_model.get_iter_first()

View File

@ -8,6 +8,7 @@ import gtk
import pango import pango
from sugar.shell.PresenceWindow import PresenceWindow from sugar.shell.PresenceWindow import PresenceWindow
from sugar.shell.Owner import ShellOwner
activity_counter = 0 activity_counter = 0
@ -240,6 +241,9 @@ class ActivityContainer(dbus.service.Object):
self.current_activity = None self.current_activity = None
# Create our owner service
self._owner = ShellOwner()
def show(self): def show(self):
self.window.show() self.window.show()
@ -359,7 +363,10 @@ def main():
presence_window.show() presence_window.show()
console.set_parent_window(activity_container.window) console.set_parent_window(activity_container.window)
try:
gtk.main()
except KeyboardInterrupt:
pass
if __name__ == "__main__": if __name__ == "__main__":
main() main()
gtk.main()