diff --git a/services/presence/buddy.py b/services/presence/buddy.py index f302b8ce..200401bd 100644 --- a/services/presence/buddy.py +++ b/services/presence/buddy.py @@ -22,6 +22,7 @@ import dbus import dbus.service from dbus.gobject_service import ExportedGObject from ConfigParser import ConfigParser, NoOptionError +import psutils from sugar import env, profile, util import logging @@ -132,9 +133,19 @@ class Buddy(ExportedGObject): logging.debug("Invalid init property '%s'; ignoring..." % key) del kwargs[key] + # Set icon after superclass init, because it sends DBus and GObject + # signals when set + icon_data = None + if kwargs.has_key(_PROP_ICON): + icon_data = kwargs[_PROP_ICON] + del kwargs[_PROP_ICON] + ExportedGObject.__init__(self, bus_name, self._object_path, gobject_properties=kwargs) + if icon_data: + self.props.icon = icon_data + def do_get_property(self, pspec): """Retrieve current value for the given property specifier @@ -399,176 +410,6 @@ class Buddy(ExportedGObject): except AttributeError: self._valid = False - -NM_SERVICE = 'org.freedesktop.NetworkManager' -NM_IFACE = 'org.freedesktop.NetworkManager' -NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices' -NM_PATH = '/org/freedesktop/NetworkManager' - -class IP4AddressMonitor(gobject.GObject): - """This class, and direct buddy IPv4 address access, will go away quite soon""" - - __gsignals__ = { - 'address-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) - } - - __gproperties__ = { - 'address' : (str, None, None, None, gobject.PARAM_READABLE) - } - - def __init__(self): - gobject.GObject.__init__(self) - self._nm_present = False - self._matches = [] - self._addr = None - self._nm_obj = None - - sys_bus = dbus.SystemBus() - bus_object = sys_bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') - try: - if bus_object.GetNameOwner(NM_SERVICE, dbus_interface='org.freedesktop.DBus'): - self._nm_present = True - except dbus.DBusException: - pass - - if self._nm_present: - self._connect_to_nm() - else: - addr = self._get_address_fallback() - self._update_address(addr) - - def do_get_property(self, pspec): - if pspec.name == "address": - return self._addr - - def _update_address(self, new_addr): - if new_addr == "0.0.0.0": - new_addr = None - if new_addr == self._addr: - return - - self._addr = new_addr - logging.debug("IP4 address now '%s'" % new_addr) - self.emit('address-changed', new_addr) - - def _connect_to_nm(self): - """Connect to NM device state signals to tell when the IPv4 address changes""" - try: - sys_bus = dbus.SystemBus() - proxy = sys_bus.get_object(NM_SERVICE, NM_PATH) - self._nm_obj = dbus.Interface(proxy, NM_IFACE) - except dbus.DBusException, err: - logging.debug("Error finding NetworkManager: %s" % err) - self._nm_present = False - return - - sys_bus = dbus.SystemBus() - match = sys_bus.add_signal_receiver(self._nm_device_active_cb, - signal_name="DeviceNowActive", - dbus_interface=NM_IFACE) - self._matches.append(match) - - match = sys_bus.add_signal_receiver(self._nm_device_no_longer_active_cb, - signal_name="DeviceNoLongerActive", - dbus_interface=NM_IFACE, - named_service=NM_SERVICE) - self._matches.append(match) - - match = sys_bus.add_signal_receiver(self._nm_state_change_cb, - signal_name="StateChange", - dbus_interface=NM_IFACE, - named_service=NM_SERVICE) - self._matches.append(match) - - state = self._nm_obj.state() - if state == 3: # NM_STATE_CONNECTED - self._query_devices() - - def _device_properties_cb(self, *props): - active = props[4] - if not active: - return - act_stage = props[5] - # HACK: OLPC NM has an extra stage, so activated == 8 on OLPC - # but 7 everywhere else - if act_stage != 8 and act_stage != 7: - # not activated - return - self._update_address(props[6]) - - def _device_properties_error_cb(self, err): - logging.debug("Error querying device properties: %s" % err) - - def _query_device_properties(self, device): - sys_bus = dbus.SystemBus() - proxy = sys_bus.get_object(NM_SERVICE, device) - dev = dbus.Interface(proxy, NM_IFACE_DEVICES) - dev.getProperties(reply_handler=self._device_properties_cb, - error_handler=self._device_properties_error_cb) - - def _get_devices_cb(self, ops): - """Query each device's properties""" - for op in ops: - self._query_device_properties(op) - - def _get_devices_error_cb(self, err): - logging.debug("Error getting NetworkManager devices: %s" % err) - - def _query_devices(self): - """Query NM for a list of network devices""" - self._nm_obj.getDevices(reply_handler=self._get_devices_cb, - error_handler=self._get_devices_error_cb) - - def _nm_device_active_cb(self, device, ssid=None): - self._query_device_properties(device) - - def _nm_device_no_longer_active_cb(self, device): - self._update_address(None) - - def _nm_state_change_cb(self, new_state): - if new_state == 4: # NM_STATE_DISCONNECTED - self._update_address(None) - - def handle_name_owner_changed(self, name, old, new): - """Clear state when NM goes away""" - if name != NM_SERVICE: - return - if (old and len(old)) and (not new and not len(new)): - # NM went away - self._nm_present = False - for match in self._matches: - match.remove() - self._matches = [] - self._update_address(None) - elif (not old and not len(old)) and (new and len(new)): - # NM started up - self._nm_present = True - self._connect_to_nm() - - def _get_iface_address(self, iface): - import socket - import fcntl - import struct - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - fd = s.fileno() - SIOCGIFADDR = 0x8915 - addr = fcntl.ioctl(fd, SIOCGIFADDR, struct.pack('256s', iface[:15]))[20:24] - s.close() - return socket.inet_ntoa(addr) - - def _get_address_fallback(self): - import commands - (s, o) = commands.getstatusoutput("/sbin/route -n") - if s != 0: - return - for line in o.split('\n'): - fields = line.split(" ") - if fields[0] == "0.0.0.0": - iface = fields[len(fields) - 1] - return self._get_iface_address(iface) - return None - class GenericOwner(Buddy): """Common functionality for Local User-like objects @@ -617,7 +458,7 @@ class GenericOwner(Buddy): signal_name="NameOwnerChanged", dbus_interface="org.freedesktop.DBus") - self._ip4_addr_monitor = IP4AddressMonitor() + self._ip4_addr_monitor = psutils.IP4AddressMonitor.get_instance() self._ip4_addr_monitor.connect("address-changed", self._ip4_address_changed_cb) def _ip4_address_changed_cb(self, monitor, address): diff --git a/services/presence/psutils.py b/services/presence/psutils.py index 76583d65..b24b1df0 100644 --- a/services/presence/psutils.py +++ b/services/presence/psutils.py @@ -14,6 +14,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import dbus, dbus.glib, gobject +import logging + def bytes_to_string(bytes): """The function converts a D-BUS byte array provided by dbus to string format. @@ -28,3 +31,183 @@ def bytes_to_string(bytes): # Python string ret = ''.join([str(item) for item in bytes]) return ret + + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices' +NM_PATH = '/org/freedesktop/NetworkManager' + +_ip4am = None + +class IP4AddressMonitor(gobject.GObject): + """This class, and direct buddy IPv4 address access, will go away quite soon""" + + __gsignals__ = { + 'address-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + __gproperties__ = { + 'address' : (str, None, None, None, gobject.PARAM_READABLE) + } + + def get_instance(): + """Retrieve (or create) the IP4Address monitor singleton instance""" + global _ip4am + if not _ip4am: + _ip4am = IP4AddressMonitor() + return _ip4am + get_instance = staticmethod(get_instance) + + def __init__(self): + gobject.GObject.__init__(self) + self._nm_present = False + self._matches = [] + self._addr = None + self._nm_obj = None + + sys_bus = dbus.SystemBus() + bus_object = sys_bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + try: + if bus_object.GetNameOwner(NM_SERVICE, dbus_interface='org.freedesktop.DBus'): + self._nm_present = True + except dbus.DBusException: + pass + + if self._nm_present: + self._connect_to_nm() + else: + addr = self._get_address_fallback() + self._update_address(addr) + + def do_get_property(self, pspec): + if pspec.name == "address": + return self._addr + + def _update_address(self, new_addr): + if new_addr == "0.0.0.0": + new_addr = None + if new_addr == self._addr: + return + + self._addr = new_addr + logging.debug("IP4 address now '%s'" % new_addr) + self.emit('address-changed', new_addr) + + def _connect_to_nm(self): + """Connect to NM device state signals to tell when the IPv4 address changes""" + try: + sys_bus = dbus.SystemBus() + proxy = sys_bus.get_object(NM_SERVICE, NM_PATH) + self._nm_obj = dbus.Interface(proxy, NM_IFACE) + except dbus.DBusException, err: + logging.debug("Error finding NetworkManager: %s" % err) + self._nm_present = False + return + + sys_bus = dbus.SystemBus() + match = sys_bus.add_signal_receiver(self._nm_device_active_cb, + signal_name="DeviceNowActive", + dbus_interface=NM_IFACE) + self._matches.append(match) + + match = sys_bus.add_signal_receiver(self._nm_device_no_longer_active_cb, + signal_name="DeviceNoLongerActive", + dbus_interface=NM_IFACE, + named_service=NM_SERVICE) + self._matches.append(match) + + match = sys_bus.add_signal_receiver(self._nm_state_change_cb, + signal_name="StateChange", + dbus_interface=NM_IFACE, + named_service=NM_SERVICE) + self._matches.append(match) + + state = self._nm_obj.state() + if state == 3: # NM_STATE_CONNECTED + self._query_devices() + + def _device_properties_cb(self, *props): + active = props[4] + if not active: + return + act_stage = props[5] + # HACK: OLPC NM has an extra stage, so activated == 8 on OLPC + # but 7 everywhere else + if act_stage != 8 and act_stage != 7: + # not activated + return + self._update_address(props[6]) + + def _device_properties_error_cb(self, err): + logging.debug("Error querying device properties: %s" % err) + + def _query_device_properties(self, device): + sys_bus = dbus.SystemBus() + proxy = sys_bus.get_object(NM_SERVICE, device) + dev = dbus.Interface(proxy, NM_IFACE_DEVICES) + dev.getProperties(reply_handler=self._device_properties_cb, + error_handler=self._device_properties_error_cb) + + def _get_devices_cb(self, ops): + """Query each device's properties""" + for op in ops: + self._query_device_properties(op) + + def _get_devices_error_cb(self, err): + logging.debug("Error getting NetworkManager devices: %s" % err) + + def _query_devices(self): + """Query NM for a list of network devices""" + self._nm_obj.getDevices(reply_handler=self._get_devices_cb, + error_handler=self._get_devices_error_cb) + + def _nm_device_active_cb(self, device, ssid=None): + self._query_device_properties(device) + + def _nm_device_no_longer_active_cb(self, device): + self._update_address(None) + + def _nm_state_change_cb(self, new_state): + if new_state == 4: # NM_STATE_DISCONNECTED + self._update_address(None) + + def handle_name_owner_changed(self, name, old, new): + """Clear state when NM goes away""" + if name != NM_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # NM went away + self._nm_present = False + for match in self._matches: + match.remove() + self._matches = [] + self._update_address(None) + elif (not old and not len(old)) and (new and len(new)): + # NM started up + self._nm_present = True + self._connect_to_nm() + + def _get_iface_address(self, iface): + import socket + import fcntl + import struct + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + fd = s.fileno() + SIOCGIFADDR = 0x8915 + addr = fcntl.ioctl(fd, SIOCGIFADDR, struct.pack('256s', iface[:15]))[20:24] + s.close() + return socket.inet_ntoa(addr) + + def _get_address_fallback(self): + import commands + (s, o) = commands.getstatusoutput("/sbin/route -n") + if s != 0: + return + for line in o.split('\n'): + fields = line.split(" ") + if fields[0] == "0.0.0.0": + iface = fields[len(fields) - 1] + return self._get_iface_address(iface) + return None diff --git a/services/presence/server_plugin.py b/services/presence/server_plugin.py index efb97fc7..63de2797 100644 --- a/services/presence/server_plugin.py +++ b/services/presence/server_plugin.py @@ -136,11 +136,22 @@ class ServerPlugin(gobject.GObject): self._owner.connect("icon-changed", self._owner_icon_changed_cb) self._account = self._get_account_info() - - self._conn = self._init_connection() self._conn_status = CONNECTION_STATUS_DISCONNECTED - self._reconnect_id = 0 + # Monitor IPv4 address as an indicator of the network connection + self._ip4am = psutils.IP4AddressMonitor.get_instance() + self._ip4am.connect('address-changed', self._ip4_address_changed_cb) + + def _ip4_address_changed_cb(self, ip4am, address): + logging.debug("::: IP4 address now %s" % address) + if address: + logging.debug("::: valid IP4 address, conn_status %s" % self._conn_status) + if self._conn_status == CONNECTION_STATUS_DISCONNECTED: + logging.debug("::: will connect") + self.start() + else: + logging.debug("::: invalid IP4 address, will disconnect") + self.cleanup() def _owner_property_changed_cb(self, owner, properties): """Local user's configuration properties have changed @@ -229,6 +240,14 @@ class ServerPlugin(gobject.GObject): """Retrieve our telepathy.client.Connection object""" return self._conn + def _connect_reply_cb(self): + """Handle connection success""" + pass + + def _connect_error_cb(self, exception): + """Handle connection failure""" + logging.debug("Connect error: %s" % exception) + def _init_connection(self): """Set up our connection @@ -262,7 +281,12 @@ class ServerPlugin(gobject.GObject): conn[CONN_INTERFACE_PRESENCE].connect_to_signal('PresenceUpdate', self._presence_update_cb) - return conn + self._conn = conn + status = self._conn[CONN_INTERFACE].GetStatus() + if status == CONNECTION_STATUS_DISCONNECTED: + self._conn[CONN_INTERFACE].Connect(reply_handler=self._connect_reply_cb, + error_handler=self._connect_error_cb) + self._handle_connection_status_change(self._conn, status) def _request_list_channel(self, name): """Request a contact-list channel from Telepathy @@ -297,16 +321,14 @@ class ServerPlugin(gobject.GObject): if local_pending: # accept pending subscriptions - #print 'pending: %r' % local_pending publish[CHANNEL_INTERFACE_GROUP].AddMembers(local_pending, '') - not_subscribed = list(set(publish_handles) - set(subscribe_handles)) self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() self._online_contacts[self_handle] = self._account['account'] - for handle in not_subscribed: - # request subscriptions from people subscribed to us if we're not subscribed to them - subscribe[CHANNEL_INTERFACE_GROUP].AddMembers([self_handle], '') + # request subscriptions from people subscribed to us if we're not subscribed to them + not_subscribed = list(set(publish_handles) - set(subscribe_handles)) + subscribe[CHANNEL_INTERFACE_GROUP].AddMembers(not_subscribed, '') if CONN_INTERFACE_BUDDY_INFO not in self._conn.get_valid_interfaces(): logging.debug('OLPC information not available') @@ -334,8 +356,8 @@ class ServerPlugin(gobject.GObject): self._set_self_avatar() # Request presence for everyone on the channel - self._conn[CONN_INTERFACE_PRESENCE].GetPresence(subscribe_handles, - ignore_reply=True) + subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() + self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) return True def _set_self_avatar_cb(self, token): @@ -504,31 +526,38 @@ class ServerPlugin(gobject.GObject): return handle return None - def _status_changed_cb(self, state, reason): - """Handle notification of connection-status change - - state -- CONNECTION_STATUS_* - reason -- integer code describing the reason... - """ - if state == CONNECTION_STATUS_CONNECTING: - self._conn_status = state - logging.debug("State: connecting...") - elif state == CONNECTION_STATUS_CONNECTED: + def _handle_connection_status_change(self, status, reason): + if status == self._conn_status: + return + + if status == CONNECTION_STATUS_CONNECTING: + self._conn_status = status + logging.debug("status: connecting...") + elif status == CONNECTION_STATUS_CONNECTED: if self._connected_cb(): - logging.debug("State: connected") - self._conn_status = state + logging.debug("status: connected") + self._conn_status = status else: self.cleanup() - logging.debug("State: was connected, but an error occurred") - elif state == CONNECTION_STATUS_DISCONNECTED: + logging.debug("status: was connected, but an error occurred") + elif status == CONNECTION_STATUS_DISCONNECTED: self.cleanup() - logging.debug("State: disconnected (reason %r)" % reason) + logging.debug("status: disconnected (reason %r)" % reason) if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED: # FIXME: handle connection failure; retry later? pass self.emit('status', self._conn_status, int(reason)) + def _status_changed_cb(self, status, reason): + """Handle notification of connection-status change + + status -- CONNECTION_STATUS_* + reason -- integer code describing the reason... + """ + logging.debug("::: connection status changed to %s" % status) + self._handle_connection_status_change(status, reason) + def start(self): """Start up the Telepathy networking connections @@ -541,35 +570,13 @@ class ServerPlugin(gobject.GObject): _connect_reply_cb or _connect_error_cb """ logging.debug("Starting up...") - # If the connection is already connected query initial contacts - conn_status = self._conn[CONN_INTERFACE].GetStatus() - if conn_status == CONNECTION_STATUS_CONNECTED: - self._connected_cb() - subscribe = self._request_list_channel('subscribe') - subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() - self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) - elif conn_status == CONNECTION_STATUS_CONNECTING: - pass + + # Only init connection if we have a valid IP address + if self._ip4am.props.address: + logging.debug("::: Have IP4 address %s, will connect" % self._ip4am.props.address) + self._init_connection() else: - self._conn[CONN_INTERFACE].Connect(reply_handler=self._connect_reply_cb, - error_handler=self._connect_error_cb) - - def _connect_reply_cb(self): - """Handle connection success""" - if self._reconnect_id > 0: - gobject.source_remove(self._reconnect_id) - - def _reconnect(self): - """Reset number-of-attempted connections and re-attempt""" - self._reconnect_id = 0 - self.start() - return False - - def _connect_error_cb(self, exception): - """Handle connection failure""" - logging.debug("Connect error: %s" % exception) - if not self._reconnect_id: - self._reconnect_id = gobject.timeout_add(10000, self._reconnect) + logging.debug("::: No IP4 address, postponing connection") def cleanup(self): """If we still have a connection, disconnect it""" @@ -578,6 +585,12 @@ class ServerPlugin(gobject.GObject): self._conn = None self._conn_status = CONNECTION_STATUS_DISCONNECTED + for handle in self._online_contacts.keys(): + self._contact_offline(handle) + self._online_contacts = {} + self._joined_activites = [] + self._activites = {} + def _contact_offline(self, handle): """Handle contact going offline (send message, update set)""" if not self._online_contacts.has_key(handle): @@ -659,6 +672,9 @@ class ServerPlugin(gobject.GObject): timestamp, statuses = presence[handle] online = handle in self._online_contacts for status, params in statuses.items(): + if not online and status == "offline": + # weren't online in the first place... + continue jid = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] olstr = "ONLINE" if not online: olstr = "OFFLINE"