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]
self.zeroconf_ip_address = zipa = verify_ipV4_address(ip_address) or get_external_ip()
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 = (
(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():
return

View File

@ -12,6 +12,7 @@ from unittest import skipIf
from glob import glob
from threading import Event
from calibre.constants import ispy3
from calibre.srv.pre_activated import has_preactivated_support
from calibre.srv.tests.base import BaseTest, TestServer
from calibre.ptempfile import TemporaryDirectory
@ -114,15 +115,18 @@ class LoopTest(BaseTest):
def test_bonjour(self):
'Test advertising via 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()
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.ae(b.advertised_port, server.address[1])
service = b.services[0]
self.ae(service.type, b'_calibre._tcp.local.')
self.ae(service.type, '_calibre._tcp.local.')
r = Zeroconf()
info = r.getServiceInfo(service.type, service.name)
info = r.get_service_info(service.type, service.name)
self.assertIsNotNone(info)
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
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
is_ci = os.environ.get('CI', '').lower() == 'true'
@ -77,6 +77,15 @@ class BuildTest(unittest.TestCase):
import 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):
exclusions = set()
if is_ci:

View File

@ -1384,6 +1384,7 @@ class Zeroconf(object):
if info.request(self, timeout):
return info
return None
get_service_info = getServiceInfo
def addServiceListener(self, type, listener):
"""Adds a listener for a particular service type. This object
@ -1424,6 +1425,7 @@ class Zeroconf(object):
self.send(out)
i += 1
nextTime += _REGISTER_TIME
register_service = registerService
def unregisterService(self, info):
"""Unregister a service."""
@ -1452,6 +1454,7 @@ class Zeroconf(object):
self.send(out)
i += 1
nextTime += _UNREGISTER_TIME
unregister_service = unregisterService
def unregisterAllServices(self):
"""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'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
@ -9,10 +9,12 @@ from threading import Thread
from calibre.utils.filenames import ascii_text
from calibre import force_unicode
from calibre.constants import ispy3
from polyglot.builtins import iteritems, unicode_type
_server = None
_all_ip_addresses = dict()
_all_ip_addresses = {}
class AllIpAddressesGetter(Thread):
@ -50,9 +52,9 @@ def get_all_ips(reinitialize=False):
global _all_ip_addresses, _ip_address_getter_thread
if not _ip_address_getter_thread or (reinitialize and not
_ip_address_getter_thread.is_alive()):
_all_ip_addresses = dict()
_all_ip_addresses = {}
_ip_address_getter_thread = AllIpAddressesGetter()
_ip_address_getter_thread.setDaemon(True)
_ip_address_getter_thread.daemon = True
_ip_address_getter_thread.start()
return _all_ip_addresses
@ -61,7 +63,7 @@ def _get_external_ip():
'Get IP address of interface used to connect to the outside world'
try:
ipaddr = socket.gethostbyname(socket.gethostname())
except:
except Exception:
ipaddr = '127.0.0.1'
if ipaddr.startswith('127.'):
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)
if len(ip_address.split('.')) == 4:
result = ip_address
except socket.error:
except (socket.error, OSError):
# Not legal ip address
pass
return result
@ -108,7 +110,10 @@ def get_external_ip():
def start_server():
global _server
if _server is None:
from calibre.utils.Zeroconf import Zeroconf
if ispy3:
from zeroconf import Zeroconf
else:
from calibre.utils.Zeroconf import Zeroconf
try:
_server = Zeroconf()
except Exception:
@ -120,7 +125,7 @@ def start_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)
try:
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()
if not local_ip:
raise ValueError('Failed to determine local IP address to advertise via BonJour')
type = type+'.local.'
from calibre.utils.Zeroconf import ServiceInfo
return ServiceInfo(type, desc+'.'+type,
address=socket.inet_aton(local_ip),
port=port,
properties=properties,
server=hostname+'.local.')
service_type = service_type+'.local.'
service_name = desc + '.' + service_type
server_name = hostname+'.local.'
if ispy3:
from zeroconf import ServiceInfo
else:
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.
: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 properties: An optional dictionary whose keys and values will be put
into the TXT record.
'''
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)
server.registerService(service)
server.register_service(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.
The parameters must be the same as used in the corresponding call to publish
'''
server = start_server()
service = create_service(desc, type, port, properties, add_hostname)
server.unregisterService(service)
if server.countRegisteredServices() == 0:
service = create_service(desc, service_type, port, properties, add_hostname)
server.unregister_service(service)
if len(server.services) == 0:
stop_server()