py3: Port Zeroconf/mdns

Requires zeroconf and ifaddr dependencies on python3
This commit is contained in:
Kovid Goyal 2019-04-15 09:43:44 +05:30
parent ef3d0cfe6c
commit 7548a52e3d
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 67 additions and 34 deletions

View File

@ -27,14 +27,8 @@ class BonJour(object): # {{{
ip_address, port = loop.bound_address[:2] ip_address, port = loop.bound_address[:2]
self.zeroconf_ip_address = zipa = verify_ipV4_address(ip_address) or get_external_ip() self.zeroconf_ip_address = zipa = verify_ipV4_address(ip_address) or get_external_ip()
prefix = loop.opts.url_prefix or '' prefix = loop.opts.url_prefix or ''
# The Zeroconf module requires everything to be bytestrings
def enc(x):
if not isinstance(x, bytes):
x = x.encode('ascii')
return x
mdns_services = ( mdns_services = (
(enc(self.service_name), enc(self.service_type), port, {b'path':enc(prefix + self.path)}), (self.service_name, self.service_type, port, {'path':prefix + self.path}),
) )
if self.shutdown.is_set(): if self.shutdown.is_set():
return return

View File

@ -12,6 +12,7 @@ from unittest import skipIf
from glob import glob from glob import glob
from threading import Event from threading import Event
from calibre.constants import ispy3
from calibre.srv.pre_activated import has_preactivated_support from calibre.srv.pre_activated import has_preactivated_support
from calibre.srv.tests.base import BaseTest, TestServer from calibre.srv.tests.base import BaseTest, TestServer
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
@ -114,15 +115,18 @@ class LoopTest(BaseTest):
def test_bonjour(self): def test_bonjour(self):
'Test advertising via BonJour' 'Test advertising via BonJour'
from calibre.srv.bonjour import BonJour from calibre.srv.bonjour import BonJour
from calibre.utils.Zeroconf import Zeroconf if ispy3:
from zeroconf import Zeroconf
else:
from calibre.utils.Zeroconf import Zeroconf
b = BonJour() b = BonJour()
with TestServer(lambda data:(data.path[0] + data.read()), plugins=(b,), shutdown_timeout=5) as server: with TestServer(lambda data:(data.path[0] + data.read()), plugins=(b,), shutdown_timeout=5) as server:
self.assertTrue(b.started.wait(5), 'BonJour not started') self.assertTrue(b.started.wait(5), 'BonJour not started')
self.ae(b.advertised_port, server.address[1]) self.ae(b.advertised_port, server.address[1])
service = b.services[0] service = b.services[0]
self.ae(service.type, b'_calibre._tcp.local.') self.ae(service.type, '_calibre._tcp.local.')
r = Zeroconf() r = Zeroconf()
info = r.getServiceInfo(service.type, service.name) info = r.get_service_info(service.type, service.name)
self.assertIsNotNone(info) self.assertIsNotNone(info)
self.ae(info.text, b'\npath=/opds') self.ae(info.text, b'\npath=/opds')

View File

@ -13,7 +13,7 @@ Test a binary calibre build to ensure that all needed binary images/libraries ha
import os, ctypes, sys, unittest, time import os, ctypes, sys, unittest, time
from calibre.constants import plugins, iswindows, islinux, isosx from calibre.constants import plugins, iswindows, islinux, isosx, ispy3
from polyglot.builtins import iteritems, map, unicode_type from polyglot.builtins import iteritems, map, unicode_type
is_ci = os.environ.get('CI', '').lower() == 'true' is_ci = os.environ.get('CI', '').lower() == 'true'
@ -77,6 +77,15 @@ class BuildTest(unittest.TestCase):
import soupsieve, bs4 import soupsieve, bs4
del soupsieve, bs4 del soupsieve, bs4
def test_zeroconf(self):
if ispy3:
import zeroconf as z, ifaddr
else:
import calibre.utils.Zeroconf as z
ifaddr = None
del z
del ifaddr
def test_plugins(self): def test_plugins(self):
exclusions = set() exclusions = set()
if is_ci: if is_ci:

View File

@ -1384,6 +1384,7 @@ class Zeroconf(object):
if info.request(self, timeout): if info.request(self, timeout):
return info return info
return None return None
get_service_info = getServiceInfo
def addServiceListener(self, type, listener): def addServiceListener(self, type, listener):
"""Adds a listener for a particular service type. This object """Adds a listener for a particular service type. This object
@ -1424,6 +1425,7 @@ class Zeroconf(object):
self.send(out) self.send(out)
i += 1 i += 1
nextTime += _REGISTER_TIME nextTime += _REGISTER_TIME
register_service = registerService
def unregisterService(self, info): def unregisterService(self, info):
"""Unregister a service.""" """Unregister a service."""
@ -1452,6 +1454,7 @@ class Zeroconf(object):
self.send(out) self.send(out)
i += 1 i += 1
nextTime += _UNREGISTER_TIME nextTime += _UNREGISTER_TIME
unregister_service = unregisterService
def unregisterAllServices(self): def unregisterAllServices(self):
"""Unregister all registered services.""" """Unregister all registered services."""

View File

@ -1,4 +1,4 @@
from __future__ import with_statement from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL 3' __license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@ -9,10 +9,12 @@ from threading import Thread
from calibre.utils.filenames import ascii_text from calibre.utils.filenames import ascii_text
from calibre import force_unicode from calibre import force_unicode
from calibre.constants import ispy3
from polyglot.builtins import iteritems, unicode_type
_server = None _server = None
_all_ip_addresses = dict() _all_ip_addresses = {}
class AllIpAddressesGetter(Thread): class AllIpAddressesGetter(Thread):
@ -50,9 +52,9 @@ def get_all_ips(reinitialize=False):
global _all_ip_addresses, _ip_address_getter_thread global _all_ip_addresses, _ip_address_getter_thread
if not _ip_address_getter_thread or (reinitialize and not if not _ip_address_getter_thread or (reinitialize and not
_ip_address_getter_thread.is_alive()): _ip_address_getter_thread.is_alive()):
_all_ip_addresses = dict() _all_ip_addresses = {}
_ip_address_getter_thread = AllIpAddressesGetter() _ip_address_getter_thread = AllIpAddressesGetter()
_ip_address_getter_thread.setDaemon(True) _ip_address_getter_thread.daemon = True
_ip_address_getter_thread.start() _ip_address_getter_thread.start()
return _all_ip_addresses return _all_ip_addresses
@ -61,7 +63,7 @@ def _get_external_ip():
'Get IP address of interface used to connect to the outside world' 'Get IP address of interface used to connect to the outside world'
try: try:
ipaddr = socket.gethostbyname(socket.gethostname()) ipaddr = socket.gethostbyname(socket.gethostname())
except: except Exception:
ipaddr = '127.0.0.1' ipaddr = '127.0.0.1'
if ipaddr.startswith('127.'): if ipaddr.startswith('127.'):
for addr in ('192.0.2.0', '198.51.100.0', 'google.com'): for addr in ('192.0.2.0', '198.51.100.0', 'google.com'):
@ -85,7 +87,7 @@ def verify_ipV4_address(ip_address):
socket.inet_aton(ip_address) socket.inet_aton(ip_address)
if len(ip_address.split('.')) == 4: if len(ip_address.split('.')) == 4:
result = ip_address result = ip_address
except socket.error: except (socket.error, OSError):
# Not legal ip address # Not legal ip address
pass pass
return result return result
@ -108,7 +110,10 @@ def get_external_ip():
def start_server(): def start_server():
global _server global _server
if _server is None: if _server is None:
from calibre.utils.Zeroconf import Zeroconf if ispy3:
from zeroconf import Zeroconf
else:
from calibre.utils.Zeroconf import Zeroconf
try: try:
_server = Zeroconf() _server = Zeroconf()
except Exception: except Exception:
@ -120,7 +125,7 @@ def start_server():
return _server return _server
def create_service(desc, type, port, properties, add_hostname, use_ip_address=None): def create_service(desc, service_type, port, properties, add_hostname, use_ip_address=None):
port = int(port) port = int(port)
try: try:
hostname = ascii_text(force_unicode(socket.gethostname())).partition('.')[0] hostname = ascii_text(force_unicode(socket.gethostname())).partition('.')[0]
@ -142,42 +147,60 @@ def create_service(desc, type, port, properties, add_hostname, use_ip_address=No
local_ip = get_external_ip() local_ip = get_external_ip()
if not local_ip: if not local_ip:
raise ValueError('Failed to determine local IP address to advertise via BonJour') raise ValueError('Failed to determine local IP address to advertise via BonJour')
type = type+'.local.' service_type = service_type+'.local.'
from calibre.utils.Zeroconf import ServiceInfo service_name = desc + '.' + service_type
return ServiceInfo(type, desc+'.'+type, server_name = hostname+'.local.'
address=socket.inet_aton(local_ip), if ispy3:
port=port, from zeroconf import ServiceInfo
properties=properties, else:
server=hostname+'.local.') from calibre.utils.Zeroconf import ServiceInfo
def enc(x):
if isinstance(x, unicode_type):
x = x.encode('ascii')
return x
service_type = enc(service_type)
service_name = enc(service_name)
server_name = enc(server_name)
if properties:
properties = {enc(k): enc(v) for k, v in iteritems(properties)}
return ServiceInfo(
service_type, service_name,
address=socket.inet_aton(local_ip),
port=port,
properties=properties,
server=server_name)
def publish(desc, type, port, properties=None, add_hostname=True, use_ip_address=None): def publish(desc, service_type, port, properties=None, add_hostname=True, use_ip_address=None):
''' '''
Publish a service. Publish a service.
:param desc: Description of service :param desc: Description of service
:param type: Name and type of service. For example _stanza._tcp :param service_type: Name and type of service. For example _stanza._tcp
:param port: Port the service listens on :param port: Port the service listens on
:param properties: An optional dictionary whose keys and values will be put :param properties: An optional dictionary whose keys and values will be put
into the TXT record. into the TXT record.
''' '''
server = start_server() server = start_server()
service = create_service(desc, type, port, properties, add_hostname, service = create_service(desc, service_type, port, properties, add_hostname,
use_ip_address) use_ip_address)
server.registerService(service) server.register_service(service)
return service return service
def unpublish(desc, type, port, properties=None, add_hostname=True): def unpublish(desc, service_type, port, properties=None, add_hostname=True):
''' '''
Unpublish a service. Unpublish a service.
The parameters must be the same as used in the corresponding call to publish The parameters must be the same as used in the corresponding call to publish
''' '''
server = start_server() server = start_server()
service = create_service(desc, type, port, properties, add_hostname) service = create_service(desc, service_type, port, properties, add_hostname)
server.unregisterService(service) server.unregister_service(service)
if server.countRegisteredServices() == 0: if len(server.services) == 0:
stop_server() stop_server()