1226b0b6b9
This call would always fail for activity services because the dbus bindings for it did not pass in the activity at all. Fix that.
530 lines
17 KiB
Python
530 lines
17 KiB
Python
# Copyright (C) 2006, Red Hat, Inc.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
import base64
|
|
import logging
|
|
|
|
import gobject
|
|
import dbus, dbus.service
|
|
from sugar import profile
|
|
|
|
|
|
PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp"
|
|
BUDDY_DBUS_OBJECT_PATH = "/org/laptop/Presence/Buddies/"
|
|
BUDDY_DBUS_INTERFACE = "org.laptop.Presence.Buddy"
|
|
|
|
_BUDDY_KEY_COLOR = 'color'
|
|
_BUDDY_KEY_CURACT = 'curact'
|
|
|
|
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,
|
|
signature="o")
|
|
def ServiceAppeared(self, object_path):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="o")
|
|
def ServiceDisappeared(self, object_path):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="")
|
|
def Disappeared(self):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="ao")
|
|
def CurrentActivityChanged(self, activities):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="")
|
|
def IconChanged(self):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="o")
|
|
def JoinedActivity(self, object_path):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="o")
|
|
def LeftActivity(self, object_path):
|
|
pass
|
|
|
|
@dbus.service.signal(BUDDY_DBUS_INTERFACE,
|
|
signature="as")
|
|
def PropertyChanged(self, prop_list):
|
|
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="so", out_signature="o")
|
|
def getServiceOfType(self, stype, activity_op):
|
|
activity = None
|
|
# "/" is the placeholder for None
|
|
if activity_op != "/":
|
|
for act in self._parent.get_joined_activities():
|
|
if act.object_path() == activity_op:
|
|
activity = act
|
|
if not activity:
|
|
raise NotFoundError("Not found")
|
|
|
|
service = self._parent.get_service_of_type(stype, activity)
|
|
if not service:
|
|
raise NotFoundError("Not found")
|
|
return service.object_path()
|
|
|
|
@dbus.service.method(BUDDY_DBUS_INTERFACE,
|
|
in_signature="", out_signature="ao")
|
|
def getJoinedActivities(self):
|
|
acts = []
|
|
for act in self._parent.get_joined_activities():
|
|
acts.append(act.object_path())
|
|
return acts
|
|
|
|
@dbus.service.method(BUDDY_DBUS_INTERFACE,
|
|
in_signature="", out_signature="a{sv}")
|
|
def getProperties(self):
|
|
props = {}
|
|
props['name'] = self._parent.get_name()
|
|
addr = self._parent.get_address()
|
|
if addr:
|
|
props['ip4_address'] = addr
|
|
props['owner'] = self._parent.is_owner()
|
|
color = self._parent.get_color()
|
|
if color:
|
|
props[_BUDDY_KEY_COLOR] = self._parent.get_color()
|
|
return props
|
|
|
|
@dbus.service.method(BUDDY_DBUS_INTERFACE,
|
|
in_signature="", out_signature="o")
|
|
def getCurrentActivity(self):
|
|
activity = self._parent.get_current_activity()
|
|
if not activity:
|
|
raise NotFoundError()
|
|
return activity.object_path()
|
|
|
|
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, icon_cache):
|
|
if not bus_name:
|
|
raise ValueError("DBus bus name must be valid")
|
|
if not object_id or not isinstance(object_id, int):
|
|
raise ValueError("object id must be a valid number")
|
|
# Normal Buddy objects must be created with a valid service,
|
|
# owner objects do not
|
|
if not isinstance(self, Owner):
|
|
if not isinstance(service, Service.Service):
|
|
raise ValueError("service must be a valid service object")
|
|
|
|
self._services = {}
|
|
self._activities = {}
|
|
|
|
self._icon_cache = icon_cache
|
|
|
|
self._nick_name = None
|
|
self._address = None
|
|
if service is not None:
|
|
self._nick_name = service.get_name()
|
|
self._address = service.get_source_address()
|
|
self._color = None
|
|
self._current_activity = None
|
|
self._valid = False
|
|
self._icon = None
|
|
self._icon_tries = 0
|
|
|
|
self._object_id = object_id
|
|
self._object_path = BUDDY_DBUS_OBJECT_PATH + str(self._object_id)
|
|
self._dbus_helper = BuddyDBusHelper(self, bus_name, self._object_path)
|
|
|
|
self._buddy_presence_service = None
|
|
if service is not None:
|
|
self.add_service(service)
|
|
|
|
def object_path(self):
|
|
return dbus.ObjectPath(self._object_path)
|
|
|
|
def _request_buddy_icon_cb(self, result_status, response, user_data):
|
|
"""Callback when icon request has completed."""
|
|
from sugar.p2p import network
|
|
icon = response
|
|
service = user_data
|
|
if result_status == network.RESULT_SUCCESS:
|
|
if icon and len(icon):
|
|
icon = base64.b64decode(icon)
|
|
self._set_icon(icon)
|
|
self._icon_cache.add_icon(icon)
|
|
|
|
if (result_status == network.RESULT_FAILED or not icon) and self._icon_tries < 3:
|
|
self._icon_tries = self._icon_tries + 1
|
|
if self._icon_tries >= 3:
|
|
logging.debug("Failed to retrieve buddy icon for '%s'." % self._nick_name)
|
|
gobject.timeout_add(1000, self._get_buddy_icon, service, True)
|
|
return False
|
|
|
|
def _get_buddy_icon(self, service, retry=False):
|
|
"""Get the buddy's icon. Check the cache first, if its
|
|
not there get the icon from the buddy over the network."""
|
|
if retry != True:
|
|
# Only hit the cache once
|
|
icon_hash = service.get_one_property('icon-hash')
|
|
if icon_hash is not None:
|
|
icon = self._icon_cache.get_icon(icon_hash)
|
|
if icon:
|
|
logging.debug("%s: icon cache hit for %s." % (self._nick_name, icon_hash))
|
|
self._set_icon(icon)
|
|
return False
|
|
logging.debug("%s: icon cache miss, fetching icon from buddy..." % self._nick_name)
|
|
|
|
from sugar.p2p import Stream
|
|
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._get_buddy_icon, service, True)
|
|
return False
|
|
|
|
def _get_service_key(self, service):
|
|
return (service.get_type(), service.get_activity_id())
|
|
|
|
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:
|
|
logging.error("Service and buddy nick names doesn't match: " \
|
|
"%s %s" % (service.get_name(), self._nick_name))
|
|
return False
|
|
|
|
source_addr = service.get_source_address()
|
|
if source_addr != self._address:
|
|
logging.error("Service source and buddy address doesn't " \
|
|
"match: %s %s" % (source_addr, self._address))
|
|
return False
|
|
return self._internal_add_service(service)
|
|
|
|
def _internal_add_service(self, service):
|
|
service_key = self._get_service_key(service)
|
|
if service_key in self._services.keys():
|
|
logging.error("Service already known: %s %s" % (service_key[0],
|
|
service_key[1]))
|
|
return False
|
|
|
|
if service.get_type() == PRESENCE_SERVICE_TYPE and self._buddy_presence_service:
|
|
# already have a presence service for this buddy
|
|
logging.debug("!!! Tried to add a buddy presence service when " \
|
|
"one already existed.")
|
|
return False
|
|
|
|
logging.debug("Buddy %s added service type %s id %s" % (self._nick_name,
|
|
service.get_type(), service.get_activity_id()))
|
|
self._services[service_key] = service
|
|
service.set_owner(self)
|
|
|
|
if service.get_type() == PRESENCE_SERVICE_TYPE:
|
|
self._buddy_presence_service = service
|
|
# A buddy isn't valid until its official presence
|
|
# service has been found and resolved
|
|
self._valid = True
|
|
self._get_buddy_icon(service)
|
|
self._color = service.get_one_property(_BUDDY_KEY_COLOR)
|
|
self._current_activity = service.get_one_property(_BUDDY_KEY_CURACT)
|
|
# Monitor further buddy property changes, like current activity
|
|
# and color
|
|
service.connect('property-changed',
|
|
self.__buddy_presence_service_property_changed_cb)
|
|
|
|
if self._valid:
|
|
self._dbus_helper.ServiceAppeared(service.object_path())
|
|
return True
|
|
|
|
def __buddy_presence_service_property_changed_cb(self, service, keys):
|
|
if _BUDDY_KEY_COLOR in keys:
|
|
new_color = service.get_one_property(_BUDDY_KEY_COLOR)
|
|
if new_color and self._color != new_color:
|
|
self._color = new_color
|
|
self._dbus_helper.PropertyChanged([_BUDDY_KEY_COLOR])
|
|
if _BUDDY_KEY_CURACT in keys:
|
|
# Three cases here:
|
|
# 1) Buddy didn't publish a 'curact' key at all; we do nothing
|
|
# 2) Buddy published a blank/zero-length 'curact' key; we send
|
|
# a current-activity-changed signal for no activity
|
|
# 3) Buddy published a non-zero-length 'curact' key; we send
|
|
# a current-activity-changed signal if we know about the
|
|
# activity already, if not we postpone until the activity
|
|
# is found on the network and added to the buddy
|
|
new_curact = service.get_one_property(_BUDDY_KEY_CURACT)
|
|
if new_curact and self._current_activity != new_curact:
|
|
if not len(new_curact):
|
|
new_curact = None
|
|
self._current_activity = new_curact
|
|
if self._activities.has_key(self._current_activity):
|
|
# Case (3) above, valid activity id
|
|
activity = self._activities[self._current_activity]
|
|
if activity.is_valid():
|
|
self._dbus_helper.CurrentActivityChanged([activity.object_path()])
|
|
elif not self._current_activity:
|
|
# Case (2) above, no current activity
|
|
self._dbus_helper.CurrentActivityChanged([])
|
|
|
|
def __find_service_by_activity_id(self, actid):
|
|
for serv in self._services.values():
|
|
if serv.get_activity_id() == actid:
|
|
return serv
|
|
return None
|
|
|
|
def add_activity(self, activity):
|
|
if activity in self._activities.values():
|
|
return
|
|
actid = activity.get_id()
|
|
if not self.__find_service_by_activity_id(actid):
|
|
raise RuntimeError("Tried to add activity for which we had no service")
|
|
self._activities[actid] = activity
|
|
if activity.is_valid():
|
|
self._dbus_helper.JoinedActivity(activity.object_path())
|
|
|
|
# If when we received a current activity update from the buddy,
|
|
# but didn't know about that activity yet, and now we do know about
|
|
# it, we need to send out the changed activity signal
|
|
if actid == self._current_activity:
|
|
self._dbus_helper.CurrentActivityChanged([activity.object_path()])
|
|
|
|
def remove_service(self, service):
|
|
"""Remove a service from a buddy; ie, the activity was closed
|
|
or the buddy went away."""
|
|
if service.get_source_address() != self._address:
|
|
return
|
|
if service.get_name() != self._nick_name:
|
|
return
|
|
|
|
if service.get_type() == PRESENCE_SERVICE_TYPE \
|
|
and self._buddy_presence_service \
|
|
and service != self._buddy_presence_service:
|
|
logging.debug("!!! Tried to remove a spurious buddy presence service.")
|
|
return
|
|
|
|
service_key = self._get_service_key(service)
|
|
if self._services.has_key(service_key):
|
|
if self._valid:
|
|
self._dbus_helper.ServiceDisappeared(service.object_path())
|
|
del self._services[service_key]
|
|
|
|
if service.get_type() == PRESENCE_SERVICE_TYPE:
|
|
self._valid = False
|
|
self._dbus_helper.Disappeared()
|
|
|
|
def remove_activity(self, activity):
|
|
actid = activity.get_id()
|
|
if not self._activities.has_key(actid):
|
|
return
|
|
del self._activities[actid]
|
|
if activity.is_valid():
|
|
self._dbus_helper.LeftActivity(activity.object_path())
|
|
|
|
# If we just removed the buddy's current activity,
|
|
# send out a signal
|
|
if actid == self._current_activity:
|
|
self._current_activity = None
|
|
self._dbus_helper.CurrentActivityChanged([])
|
|
|
|
def get_joined_activities(self):
|
|
acts = []
|
|
for act in self._activities.values():
|
|
if act.is_valid():
|
|
acts.append(act)
|
|
return acts
|
|
|
|
def get_service_of_type(self, stype, 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 and not activity.is_valid():
|
|
raise RuntimeError("Activity is not yet valid.")
|
|
|
|
if activity:
|
|
key = (stype, activity.get_id())
|
|
else:
|
|
key = (stype, None)
|
|
if self._services.has_key(key):
|
|
return self._services[key]
|
|
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(self):
|
|
"""Return the buddies icon, if any."""
|
|
return self._icon
|
|
|
|
def get_address(self):
|
|
return self._address
|
|
|
|
def get_name(self):
|
|
return self._nick_name
|
|
|
|
def get_color(self):
|
|
return self._color
|
|
|
|
def get_current_activity(self):
|
|
if not self._current_activity:
|
|
return None
|
|
if not self._activities.has_key(self._current_activity):
|
|
return None
|
|
return self._activities[self._current_activity]
|
|
|
|
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 False
|
|
|
|
|
|
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, ps, bus_name, object_id, icon_cache):
|
|
Buddy.__init__(self, bus_name, object_id, None, icon_cache)
|
|
self._nick_name = profile.get_nick_name()
|
|
self._color = profile.get_color()
|
|
self._ps = ps
|
|
|
|
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:
|
|
logging.error("Service and buddy nick names doesn't match: " \
|
|
"%s %s" % (service.get_name(), self._nick_name))
|
|
return False
|
|
|
|
# The Owner initially doesn't have an address, so the first
|
|
# service added to the Owner determines the owner's address
|
|
source_addr = service.get_source_address()
|
|
if self._address is None and service.is_local():
|
|
self._address = source_addr
|
|
self._dbus_helper.PropertyChanged(['ip4_address'])
|
|
|
|
# The owner bypasses address checks and only cares if
|
|
# avahi says the service is a local service
|
|
if not service.is_local():
|
|
logging.error("Cannot add remote service to owner object.")
|
|
return False
|
|
|
|
logging.debug("Adding owner service %s.%s at %s:%d." % (service.get_name(),
|
|
service.get_type(), service.get_source_address(),
|
|
service.get_port()))
|
|
return self._internal_add_service(service)
|
|
|
|
def is_owner(self):
|
|
return 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_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()
|