From ad2bcc43270ddb3bf043fd8b2ecccc2d27ece473 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 6 Sep 2015 16:51:06 +0200 Subject: [PATCH 1/5] Add support for publishing multiple addresses. --- examples/browser.py | 4 +- zeroconf.py | 99 ++++++++++++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/examples/browser.py b/examples/browser.py index c851be676..ca267adfd 100755 --- a/examples/browser.py +++ b/examples/browser.py @@ -19,7 +19,9 @@ def on_service_state_change( if state_change is ServiceStateChange.Added: info = zeroconf.get_service_info(service_type, name) if info: - print(" Address: %s:%d" % (socket.inet_ntoa(cast(bytes, info.address)), cast(int, info.port))) + print( + " Address: %s:%d" % (socket.inet_ntoa(info.addresses[0]), cast(int, info.port)) + ) print(" Weight: %d, priority: %d" % (info.weight, info.priority)) print(" Server: %s" % (info.server,)) if info.properties: diff --git a/zeroconf.py b/zeroconf.py index b8c664730..a6d900c22 100644 --- a/zeroconf.py +++ b/zeroconf.py @@ -1433,7 +1433,7 @@ def __init__( self, type_: str, name: str, - address: Optional[bytes] = None, + address: Optional[Union[bytes, List[bytes]]] = None, port: Optional[int] = None, weight: int = 0, priority: int = 0, @@ -1441,12 +1441,13 @@ def __init__( server: Optional[str] = None, host_ttl: int = _DNS_HOST_TTL, other_ttl: int = _DNS_OTHER_TTL, + addresses: Optional[List[bytes]] = None, ) -> None: """Create a service description. type_: fully qualified service type name name: fully qualified service name - address: IP address as unsigned short, network byte order + address: IP address as unsigned short, network byte order (legacy) port: port that the service runs on weight: weight of the service priority: priority of the service @@ -1454,13 +1455,31 @@ def __init__( bytes for the text field) server: fully qualified name for service host (defaults to name) host_ttl: ttl used for A/SRV records - other_ttl: ttl used for PTR/TXT records""" + other_ttl: ttl used for PTR/TXT records + addresses: List of IP addresses as unsigned short, network byte + order + """ + + # Accept both none, or one, but not both. + assert ( + (address is None and addresses is None) + or (address is None and addresses) + or (address and addresses is None) + ) if not type_.endswith(service_type_name(name, allow_underscores=True)): raise BadTypeInNameException self.type = type_ self.name = name - self.address = address + if addresses is not None: + self.addresses = addresses + elif address is not None: + if isinstance(address, list): + self.addresses = address + else: + self.addresses = [address] + else: + self.addresses = [] self.port = port self.weight = weight self.priority = priority @@ -1553,7 +1572,8 @@ def update_record(self, zc: 'Zeroconf', now: float, record: DNSRecord) -> None: assert isinstance(record, DNSAddress) # if record.name == self.name: if record.name == self.server: - self.address = record.address + if record.address not in self.addresses: + self.addresses.append(record.address) elif record.type == _TYPE_SRV: assert isinstance(record, DNSService) if record.name == self.name: @@ -1585,12 +1605,12 @@ def request(self, zc: 'Zeroconf', timeout: float) -> bool: if cached: self.update_record(zc, now, cached) - if None not in (self.server, self.address, self.text): + if self.server is not None and self.text is not None and self.addresses: return True try: zc.add_listener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)) - while None in (self.server, self.address, self.text): + while self.server is None or self.text is None or not self.addresses: if last <= now: return False if next_ <= now: @@ -1629,7 +1649,16 @@ def __repr__(self) -> str: type(self).__name__, ', '.join( '%s=%r' % (name, getattr(self, name)) - for name in ('type', 'name', 'address', 'port', 'weight', 'priority', 'server', 'properties') + for name in ( + 'type', + 'name', + 'addresses', + 'port', + 'weight', + 'priority', + 'server', + 'properties', + ) ), ) @@ -1916,10 +1945,8 @@ def _broadcast_service(self, info: ServiceInfo) -> None: ) out.add_answer_at_time(DNSText(info.name, _TYPE_TXT, _CLASS_IN, info.other_ttl, info.text), 0) - if info.address: - out.add_answer_at_time( - DNSAddress(info.server, _TYPE_A, _CLASS_IN, info.host_ttl, info.address), 0 - ) + for address in info.addresses: + out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, info.host_ttl, address), 0) self.send(out) i += 1 next_time += _REGISTER_TIME @@ -1952,8 +1979,8 @@ def unregister_service(self, info: ServiceInfo) -> None: ) out.add_answer_at_time(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) - if info.address: - out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0) + for address in info.addresses: + out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, address), 0) self.send(out) i += 1 next_time += _UNREGISTER_TIME @@ -1986,10 +2013,8 @@ def unregister_all_services(self) -> None: 0, ) out.add_answer_at_time(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) - if info.address: - out.add_answer_at_time( - DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0 - ) + for address in info.addresses: + out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, address), 0) self.send(out) i += 1 next_time += _UNREGISTER_TIME @@ -2126,16 +2151,17 @@ def handle_query(self, msg: DNSIncoming, addr: str, port: int) -> None: if question.type in (_TYPE_A, _TYPE_ANY): for service in self.services.values(): if service.server == question.name.lower(): - out.add_answer( - msg, - DNSAddress( - question.name, - _TYPE_A, - _CLASS_IN | _CLASS_UNIQUE, - service.host_ttl, - service.address, - ), - ) + for address in service.addresses: + out.add_answer( + msg, + DNSAddress( + question.name, + _TYPE_A, + _CLASS_IN | _CLASS_UNIQUE, + service.host_ttl, + address, + ), + ) name_to_find = question.name.lower() if name_to_find not in self.services: @@ -2168,15 +2194,16 @@ def handle_query(self, msg: DNSIncoming, addr: str, port: int) -> None: ), ) if question.type == _TYPE_SRV: - out.add_additional_answer( - DNSAddress( - service.server, - _TYPE_A, - _CLASS_IN | _CLASS_UNIQUE, - service.host_ttl, - service.address, + for address in service.addresses: + out.add_additional_answer( + DNSAddress( + service.server, + _TYPE_A, + _CLASS_IN | _CLASS_UNIQUE, + service.host_ttl, + address, + ) ) - ) except Exception: # TODO stop catching all Exceptions self.log_exception_warning() From 92f6ee8cc892f4b4757e416b9c02dc1caaf95bc7 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 6 Sep 2015 16:50:40 +0200 Subject: [PATCH 2/5] Present all addresses that are available. --- examples/browser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/browser.py b/examples/browser.py index ca267adfd..168b3910e 100755 --- a/examples/browser.py +++ b/examples/browser.py @@ -19,9 +19,11 @@ def on_service_state_change( if state_change is ServiceStateChange.Added: info = zeroconf.get_service_info(service_type, name) if info: - print( - " Address: %s:%d" % (socket.inet_ntoa(info.addresses[0]), cast(int, info.port)) - ) + addresses = [ + "%s:%d" % (socket.inet_ntoa(addr), cast(int, info.port)) + for addr in info.addresses + ] + print(" Addresses: %s" % ", ".join(addresses)) print(" Weight: %d, priority: %d" % (info.weight, info.priority)) print(" Server: %s" % (info.server,)) if info.properties: From 59dcf519356845c3d27c7ce7998b918257e1a093 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Sun, 6 Sep 2015 16:51:18 +0200 Subject: [PATCH 3/5] Add test for backwards compatibility. --- test_zeroconf.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test_zeroconf.py b/test_zeroconf.py index 9737d38f4..6e288cda5 100644 --- a/test_zeroconf.py +++ b/test_zeroconf.py @@ -958,3 +958,28 @@ def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT): assert service_removed.is_set() browser.cancel() zeroconf_browser.close() + + +def test_multiple_addresses(): + type_ = "_http._tcp.local." + registration_name = "xxxyyy.%s" % type_ + desc = {'path': '/~paulsm/'} + address = socket.inet_aton("10.0.1.2") + + # Old way + info = ServiceInfo(type_, registration_name, address, 80, 0, 0, desc, "ash-2.local.") + + assert not hasattr(info, "address") + assert info.addresses == [address] + + # Compatibility way + info = ServiceInfo(type_, registration_name, [address, address], 80, 0, 0, desc, "ash-2.local.") + + assert info.addresses == [address, address] + + # New kwarg way + info = ServiceInfo( + type_, registration_name, None, 80, 0, 0, desc, "ash-2.local.", addresses=[address, address] + ) + + assert info.addresses == [address, address] From 2b0d9ae7027a2cb1515e6c7cad3771ca43178b0c Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 20 May 2019 13:43:56 +0200 Subject: [PATCH 4/5] Provide proper deprecation of the "address" argument and field * Raise deprecation warnings when address is used * Add a compatibility property to avoid breaking existing code (based on suggestion by Bas Stottelaar in PR #27) * Make addresses keyword-only, so that address can be eventually removed and replaced with it without breaking consumers * Raise TypeError instead of an assertion on conflicting address and addresses --- examples/browser.py | 5 +---- examples/registration.py | 10 ++++------ examples/self_test.py | 8 +++----- test_zeroconf.py | 14 +++++++++++++- zeroconf.py | 30 +++++++++++++++++++++++------- 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/examples/browser.py b/examples/browser.py index 168b3910e..8b4fd5ee2 100755 --- a/examples/browser.py +++ b/examples/browser.py @@ -19,10 +19,7 @@ def on_service_state_change( if state_change is ServiceStateChange.Added: info = zeroconf.get_service_info(service_type, name) if info: - addresses = [ - "%s:%d" % (socket.inet_ntoa(addr), cast(int, info.port)) - for addr in info.addresses - ] + addresses = ["%s:%d" % (socket.inet_ntoa(addr), cast(int, info.port)) for addr in info.addresses] print(" Addresses: %s" % ", ".join(addresses)) print(" Weight: %d, priority: %d" % (info.weight, info.priority)) print(" Server: %s" % (info.server,)) diff --git a/examples/registration.py b/examples/registration.py index 7829acc92..bda55b831 100755 --- a/examples/registration.py +++ b/examples/registration.py @@ -20,12 +20,10 @@ info = ServiceInfo( "_http._tcp.local.", "Paul's Test Web Site._http._tcp.local.", - socket.inet_aton("127.0.0.1"), - 80, - 0, - 0, - desc, - "ash-2.local.", + addresses=[socket.inet_aton("127.0.0.1")], + port=80, + properties=desc, + server="ash-2.local.", ) zeroconf = Zeroconf() diff --git a/examples/self_test.py b/examples/self_test.py index 6667d13ee..62e325732 100755 --- a/examples/self_test.py +++ b/examples/self_test.py @@ -21,11 +21,9 @@ info = ServiceInfo( "_http._tcp.local.", "My Service Name._http._tcp.local.", - socket.inet_aton("127.0.0.1"), - 1234, - 0, - 0, - desc, + addresses=[socket.inet_aton("127.0.0.1")], + port=1234, + properties=desc, ) print(" Registering service...") r.register_service(info) diff --git a/test_zeroconf.py b/test_zeroconf.py index 6e288cda5..59f03df6c 100644 --- a/test_zeroconf.py +++ b/test_zeroconf.py @@ -969,9 +969,21 @@ def test_multiple_addresses(): # Old way info = ServiceInfo(type_, registration_name, address, 80, 0, 0, desc, "ash-2.local.") - assert not hasattr(info, "address") + assert info.address == address assert info.addresses == [address] + # Updating works + address2 = socket.inet_aton("10.0.1.3") + info.address = address2 + + assert info.address == address2 + assert info.addresses == [address2] + + info.address = None + + assert info.address is None + assert info.addresses == [] + # Compatibility way info = ServiceInfo(type_, registration_name, [address, address], 80, 0, 0, desc, "ash-2.local.") diff --git a/zeroconf.py b/zeroconf.py index a6d900c22..0d321c3a9 100644 --- a/zeroconf.py +++ b/zeroconf.py @@ -30,6 +30,7 @@ import sys import threading import time +import warnings from functools import reduce from typing import AnyStr, Dict, List, Optional, Union, cast from typing import Callable, Set, Tuple # noqa # used in type hints @@ -1441,13 +1442,14 @@ def __init__( server: Optional[str] = None, host_ttl: int = _DNS_HOST_TTL, other_ttl: int = _DNS_OTHER_TTL, - addresses: Optional[List[bytes]] = None, + *, + addresses: Optional[List[bytes]] = None ) -> None: """Create a service description. type_: fully qualified service type name name: fully qualified service name - address: IP address as unsigned short, network byte order (legacy) + address: IP address as unsigned short, network byte order (deprecated, use addresses) port: port that the service runs on weight: weight of the service priority: priority of the service @@ -1461,11 +1463,8 @@ def __init__( """ # Accept both none, or one, but not both. - assert ( - (address is None and addresses is None) - or (address is None and addresses) - or (address and addresses is None) - ) + if address is not None and addresses is not None: + raise TypeError("address and addresses cannot be provided together") if not type_.endswith(service_type_name(name, allow_underscores=True)): raise BadTypeInNameException @@ -1474,6 +1473,7 @@ def __init__( if addresses is not None: self.addresses = addresses elif address is not None: + warnings.warn("address is deprecated, use addresses instead", DeprecationWarning) if isinstance(address, list): self.addresses = address else: @@ -1492,6 +1492,22 @@ def __init__( self.host_ttl = host_ttl self.other_ttl = other_ttl + @property + def address(self): + warnings.warn("ServiceInfo.address is deprecated, use addresses instead", DeprecationWarning) + try: + return self.addresses[0] + except IndexError: + return None + + @address.setter + def address(self, value): + warnings.warn("ServiceInfo.address is deprecated, use addresses instead", DeprecationWarning) + if value is None: + self.addresses = [] + else: + self.addresses = [value] + @property def properties(self) -> ServicePropertiesType: return self._properties From 90f70f0525b950094306a98415b690eb9ecd5ce0 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 20 May 2019 14:43:02 +0200 Subject: [PATCH 5/5] Disable black on ServiceInfo.__init__ until black is fixed Due to https://github.com/python/black/issues/759 black produces code that is invalid Python 3.5 syntax even with --target-version py35. This patch disables reformatting for this call (it doesn't seem to be possible per line) until it's fixed. --- zeroconf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zeroconf.py b/zeroconf.py index 0d321c3a9..c0f16797e 100644 --- a/zeroconf.py +++ b/zeroconf.py @@ -1430,6 +1430,9 @@ class ServiceInfo(RecordUpdateListener): """Service information""" + # FIXME(dtantsur): black 19.3b0 produces code that is not valid syntax on + # Python 3.5: https://github.com/python/black/issues/759 + # fmt: off def __init__( self, type_: str, @@ -1491,6 +1494,7 @@ def __init__( self._set_properties(properties) self.host_ttl = host_ttl self.other_ttl = other_ttl + # fmt: on @property def address(self):