Philipp Hörist pushed to branch gajim_0.16 at gajim / gajim
Commits: b645ce85 by Philipp Hörist at 2016-12-18T10:09:10+01:00 Resolve IP with bonjour in zeroconf
- - - - - 04f5d65c by Philipp Hörist at 2016-12-18T10:09:13+01:00 Add own logging domain for zeroconf_bonjour
- - - - - 85317bd8 by Philipp Hörist at 2016-12-18T18:59:36+01:00 Make bonjour zeroconf more resilient against errors
Catch bonjour 'ServiceNotRunning' Error Query TXT Records with DNSServiceQueryRecord
- - - - - e7fe1a69 by Philipp Hörist at 2016-12-28T15:59:09+01:00 Merge branch 'gajim_0.16' into 'gajim_0.16'
Fix broken zeroconf on Windows Fixes #8448, #8473
See merge request !6 - - - - -
4 changed files:
- src/common/zeroconf/client_zeroconf.py - src/common/zeroconf/connection_zeroconf.py - src/common/zeroconf/zeroconf_avahi.py - src/common/zeroconf/zeroconf_bonjour.py
Changes:
===================================== src/common/zeroconf/client_zeroconf.py ===================================== --- a/src/common/zeroconf/client_zeroconf.py +++ b/src/common/zeroconf/client_zeroconf.py @@ -669,7 +669,7 @@ class ClientZeroconf:
def resolve_all(self): if self.zeroconf: - self.zeroconf.resolve_all() + return self.zeroconf.resolve_all()
def reannounce(self, txt): self.remove_announce()
===================================== src/common/zeroconf/connection_zeroconf.py ===================================== --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -138,7 +138,9 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
def _on_resolve_timeout(self): if self.connected: - self.connection.resolve_all() + if not self.connection.resolve_all(): + self._on_disconnected() + return False diffs = self.roster.getDiffs() for key in diffs: self.roster.setItem(key) @@ -221,7 +223,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): else: # result is None gajim.nec.push_incoming_event(ConnectionLostEvent(None, conn=self, title=_('Could not start local service'), - msg=_('Please check if avahi-daemon is running.'))) + msg=_('Please check if avahi/bonjour-daemon is running.'))) self.disconnect() return else:
===================================== src/common/zeroconf/zeroconf_avahi.py ===================================== --- a/src/common/zeroconf/zeroconf_avahi.py +++ b/src/common/zeroconf/zeroconf_avahi.py @@ -453,7 +453,7 @@ class Zeroconf: # refresh txt data of all contacts manually (no callback available) def resolve_all(self): if not self.connected: - return + return False for val in self.contacts.values(): # get txt data from last recorded resolved info # TODO: Better try to get it from last IPv6 mDNS, then last IPv4? @@ -463,6 +463,7 @@ class Zeroconf: self.avahi.PROTO_UNSPEC, dbus.UInt32(0), reply_handler=self.service_resolved_all_callback, error_handler=self.error_callback) + return True
def get_contacts(self): return self.contacts
===================================== src/common/zeroconf/zeroconf_bonjour.py ===================================== --- a/src/common/zeroconf/zeroconf_bonjour.py +++ b/src/common/zeroconf/zeroconf_bonjour.py @@ -18,22 +18,27 @@ ##
from common import gajim +import logging import select +import socket import re -from common.zeroconf.zeroconf import C_BARE_NAME, C_DOMAIN, C_TXT +from common.zeroconf.zeroconf import C_BARE_NAME, C_DOMAIN, C_TXT, C_RESOLVED_INFO + +log = logging.getLogger('gajim.c.z.zeroconf_bonjour')
try: import pybonjour except ImportError, e: pass
+kDNSServiceErr_ServiceNotRunning = -65563 +C_RI_FULLNAME = 3
resolve_timeout = 1
class Zeroconf: def __init__(self, new_serviceCB, remove_serviceCB, name_conflictCB, disconnected_CB, error_CB, name, host, port): - self.domain = None # specific domain to browse self.stype = '_presence._tcp' self.port = port # listening port that gets announced self.username = name @@ -52,14 +57,16 @@ class Zeroconf: self.connected = False self.announced = False self.invalid_self_contact = {} + self.resolved_contacts = {} self.resolved = [] - + self.queried = []
def browse_callback(self, sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain): - gajim.log.debug('Found service %s in domain %s on %i(type: %s).' % (serviceName, replyDomain, interfaceIndex, regtype)) + log.debug('Found service %s in domain %s on %i(type: %s).' % (serviceName, replyDomain, interfaceIndex, regtype)) if not self.connected: return if errorCode != pybonjour.kDNSServiceErr_NoError: + log.debug('Error in browse_callback: %s', str(errorCode)) return if not (flags & pybonjour.kDNSServiceFlagsAdd): self.remove_service_callback(serviceName) @@ -72,7 +79,7 @@ class Zeroconf: while not self.resolved: ready = select.select([resolve_sdRef], [], [], resolve_timeout) if resolve_sdRef not in ready[0]: - gajim.log.debug('Resolve timed out') + log.info('Resolve timed out') break pybonjour.DNSServiceProcessResult(resolve_sdRef) else: @@ -81,7 +88,7 @@ class Zeroconf: resolve_sdRef.close()
def remove_service_callback(self, name): - gajim.log.debug('Service %s disappeared.' % name) + log.info('Service %s disappeared.' % name) if not self.connected: return if name != self.name: @@ -91,20 +98,40 @@ class Zeroconf: self.remove_serviceCB(key) return
- def new_domain_callback(self, interface, protocol, domain, flags): - if domain != "local": - self.browse_domain(interface, protocol, domain) - # takes a TXTRecord instance def txt_array_to_dict(self, txt): items = pybonjour.TXTRecord.parse(txt)._items return dict((v[0], v[1]) for v in items.values())
- def service_resolved_callback(self, sdRef, flags, interfaceIndex, errorCode, fullname, - hosttarget, port, txtRecord): + def query_txt_callback(self, sdRef, flags, interfaceIndex, errorCode, hosttarget, + rrtype, rrclass, rdata, ttl): + + if errorCode != pybonjour.kDNSServiceErr_NoError: + log.error('Error in query_record_callback: %s', str(errorCode)) + return + + result = re.split('(?<!\\).', hosttarget) + name = result[0] + + if name != self.name: + # update TXT data only, as intended according to resolve_all comment + old_contact = self.contacts[name] + self.contacts[name] = old_contact[0:C_TXT] + (rdata,) + old_contact[C_TXT+1:] + log.debug(self.contacts[name]) + + self.queried.append(True) + + def query_record_callback(self, sdRef, flags, interfaceIndex, errorCode, hosttarget, + rrtype, rrclass, rdata, ttl): + + if errorCode != pybonjour.kDNSServiceErr_NoError: + log.error('Error in query_record_callback: %s', str(errorCode)) + return + + fullname, port, txtRecord = self.resolved_contacts[hosttarget]
# TODO: do proper decoding... - escaping= { + escaping = { r'.': '.', r'\032': ' ', r'\064': '@', @@ -120,9 +147,12 @@ class Zeroconf: name = name.replace(src, trg)
txt = pybonjour.TXTRecord.parse(txtRecord) + ip = socket.inet_ntoa(rdata)
- gajim.log.debug('Service data for service %s on %i:' % (fullname, interfaceIndex)) - gajim.log.debug('Host %s, port %i, TXT data: %s' % (hosttarget, port, txt._items)) + log.info('Service data for service %s on %i:' + % (fullname, interfaceIndex)) + log.info('Host %s, ip %s, port %i, TXT data: %s' + % (hosttarget, ip, port, txt._items))
if not self.connected: return @@ -133,7 +163,7 @@ class Zeroconf:
# we don't want to see ourselves in the list if name != self.name: - resolved_info = [(interfaceIndex, protocol, hosttarget, -1, port)] + resolved_info = [(interfaceIndex, protocol, hosttarget, fullname, ip, port)] self.contacts[name] = (name, domain, resolved_info, bare_name, txtRecord)
self.new_serviceCB(name) @@ -142,45 +172,55 @@ class Zeroconf: # In case this is not our own record but of another # gajim instance on the same machine, # it will be used when we get a new name. - self.invalid_self_contact[name] = (name, domain, (interfaceIndex, protocol, hosttarget, -1, port), bare_name, txtRecord) - # count services - self.resolved.append(True) + self.invalid_self_contact[name] = \ + (name, domain, + (interfaceIndex, protocol, hosttarget, fullname, ip, port), + bare_name, txtRecord)
- # different handler when resolving all contacts - def service_resolved_all_callback(self, sdRef, flags, interfaceIndex, errorCode, fullname, hosttarget, port, txtRecord): - if not self.connected: - return + self.queried.append(True)
- escaping= { - r'.': '.', - r'\032': ' ', - r'\064': '@', - } + def service_resolved_callback(self, sdRef, flags, interfaceIndex, errorCode, fullname, + hosttarget, port, txtRecord):
- name, stype, protocol, domain, dummy = fullname.split('.') + if errorCode != pybonjour.kDNSServiceErr_NoError: + log.error('Error in service_resolved_callback: %s', str(errorCode)) + return
- # Replace the escaped values - for src, trg in escaping.items(): - name = name.replace(src, trg) + self.resolved_contacts[hosttarget] = (fullname, port, txtRecord)
- bare_name = name - if name.find('@') == -1: - name = name + '@' + name + try: + query_sdRef = \ + pybonjour.DNSServiceQueryRecord(interfaceIndex = interfaceIndex, + fullname = hosttarget, + rrtype = pybonjour.kDNSServiceType_A, + callBack = self.query_record_callback) + + while not self.queried: + ready = select.select([query_sdRef], [], [], resolve_timeout) + if query_sdRef not in ready[0]: + print 'Query record timed out' + break + pybonjour.DNSServiceProcessResult(query_sdRef) + else: + self.queried.pop()
- # we don't want to see ourselves in the list - if name != self.name: - # update TXT data only, as intended according to resolve_all comment - old_contact = self.contacts[name] - self.contacts[name] = old_contact[0:C_TXT] + (self.txt,) + old_contact[C_TXT+1:] + except pybonjour.BonjourError, e: + if e[0][0] == kDNSServiceErr_ServiceNotRunning: + log.info('Service not running') + else: + self.error_CB(_('Error while adding service. %s') % str(e[0][0]))
+ finally: + query_sdRef.close() + + self.resolved.append(True)
def service_added_callback(self, sdRef, flags, errorCode, name, regtype, domain): if errorCode == pybonjour.kDNSServiceErr_NoError: - gajim.log.debug('Service successfully added') + log.info('Service successfully added')
- def service_add_fail_callback(self, err): - if err[0][0] == pybonjour.kDNSServiceErr_NameConflict: - gajim.log.debug('Error while adding service. %s' % str(err)) + elif errorCode == pybonjour.kDNSServiceErr_NameConflict: + log.error('Error while adding service. %s' % str(err)) parts = self.username.split(' ')
#check if last part is a number and if, increment it @@ -190,9 +230,8 @@ class Zeroconf: stripped = 1 alternative_name = self.username + str(stripped+1) self.name_conflictCB(alternative_name) - return - self.error_CB(_('Error while adding service. %s') % str(err)) - self.disconnect() + else: + self.error_CB(_('Error while adding service. %s') % str(err))
# make zeroconf-valid names def replace_show(self, show): @@ -221,20 +260,26 @@ class Zeroconf: txt['status'] = 'avail'
self.txt = pybonjour.TXTRecord(txt, strict=True) - try: - sdRef = pybonjour.DNSServiceRegister(name = self.name, - regtype = self.stype, port = self.port, txtRecord = self.txt, - callBack = self.service_added_callback) - self.service_sdRef = sdRef - except pybonjour.BonjourError, e: - self.service_add_fail_callback(e) - else: - gajim.log.debug('Publishing service %s of type %s' % (self.name, self.stype)) + self.service_sdRef = pybonjour.DNSServiceRegister( + name=self.name, + regtype=self.stype, + port=self.port, + txtRecord=self.txt, + callBack=self.service_added_callback) + + log.info('Publishing service %s of type %s' % (self.name, self.stype)) + + ready = select.select([self.service_sdRef], [], []) + if self.service_sdRef in ready[0]: + pybonjour.DNSServiceProcessResult(self.service_sdRef)
- ready = select.select([sdRef], [], [], resolve_timeout) - if sdRef in ready[0]: - pybonjour.DNSServiceProcessResult(sdRef) + except pybonjour.BonjourError, e: + if e[0][0] == kDNSServiceErr_ServiceNotRunning: + log.info('Service not running') + else: + self.error_CB(_('Error while adding service. %s') % str(e[0][0])) + self.disconnect()
def announce(self): if not self.connected: @@ -252,27 +297,20 @@ class Zeroconf: self.announced = False return True except pybonjour.BonjourError, e: - gajim.log.debug(e) + log.debug(e) return False
- def connect(self): self.name = self.username + '@' + self.host # service name
self.connected = True
# start browsing - if self.domain is None: - # Explicitly browse .local - self.browse_domain() - - # Browse for other browsable domains - #self.domain_sdRef = pybonjour.DNSServiceEnumerateDomains(flags, interfaceIndex=0, callBack=self.new_domain_callback) - + if self.browse_domain(): + return True else: - self.browse_domain(self.domain) - - return True + self.disconnect() + return False
def disconnect(self): if self.connected: @@ -282,39 +320,72 @@ class Zeroconf: self.remove_announce()
def browse_domain(self, domain=None): - gajim.log.debug('starting to browse') try: - self.browse_sdRef = pybonjour.DNSServiceBrowse(regtype=self.stype, domain=domain, callBack=self.browse_callback) + self.browse_sdRef = pybonjour.DNSServiceBrowse( + regtype=self.stype, domain=domain, callBack=self.browse_callback) + log.info('Starting to browse .local') + return True except pybonjour.BonjourError, e: - self.error_CB("Error while browsing: %s" % e) + if e[0][0] == kDNSServiceErr_ServiceNotRunning: + log.info('Service not running') + else: + log.error('Error while browsing for services. %s', str(e[0][0])) + return False
def browse_loop(self): - ready = select.select([self.browse_sdRef], [], [], 0) - if self.browse_sdRef in ready[0]: - pybonjour.DNSServiceProcessResult(self.browse_sdRef) + try: + ready = select.select([self.browse_sdRef], [], [], 0) + if self.browse_sdRef in ready[0]: + pybonjour.DNSServiceProcessResult(self.browse_sdRef) + except pybonjour.BonjourError, e: + if e[0][0] == kDNSServiceErr_ServiceNotRunning: + log.info('Service not running') + return False + else: + log.error('Error while browsing for services. %s', + str(e[0][0])) + return True
- # refresh txt data of all contacts manually (no callback available) + # resolve_all() is called every X seconds and querys for new clients + # and monitors TXT records for changed status def resolve_all(self): if not self.connected: - return - + return False # for now put here as this is synchronous - self.browse_loop() + if not self.browse_loop(): + return False
+ # Monitor TXT Records with DNSServiceQueryRecord because + # its more efficient (see pybonjour documentation) for val in self.contacts.values(): - resolve_sdRef = pybonjour.DNSServiceResolve(0, - pybonjour.kDNSServiceInterfaceIndexAny, val[C_BARE_NAME], - self.stype + '.', val[C_DOMAIN] + '.', - self.service_resolved_all_callback) - try: - ready = select.select([resolve_sdRef], [], [], resolve_timeout) - if resolve_sdRef not in ready[0]: - gajim.log.debug('Resolve timed out (in resolve_all)') - break - pybonjour.DNSServiceProcessResult(resolve_sdRef) + query_sdRef = \ + pybonjour.DNSServiceQueryRecord( + interfaceIndex=pybonjour.kDNSServiceInterfaceIndexAny, + fullname=val[C_RESOLVED_INFO][0][C_RI_FULLNAME], + rrtype=pybonjour.kDNSServiceType_TXT, + callBack=self.query_txt_callback) + + while not self.queried: + ready = select.select([query_sdRef], [], [], resolve_timeout) + if query_sdRef not in ready[0]: + log.info('Query record timed out') + break + pybonjour.DNSServiceProcessResult(query_sdRef) + else: + self.queried.pop() + + except pybonjour.BonjourError, e: + if e[0][0] == kDNSServiceErr_ServiceNotRunning: + log.info('Service not running') + return False + else: + log.error('Error in query for TXT records. %s', + str(e[0][0])) finally: - resolve_sdRef.close() + query_sdRef.close() + + return True
def get_contacts(self): return self.contacts @@ -324,7 +395,7 @@ class Zeroconf: return None return self.contacts[jid]
- def update_txt(self, show = None): + def update_txt(self, show=None): if show: self.txt['status'] = self.replace_show(show)
View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/8a3ddae3eb836ad59ed4cbdc4e6355dd7c...