Skip to content

Commit c787610

Browse files
dtantsurjstasiak
authored andcommitted
Add support for multiple addresses when publishing a service (#170)
This is a rebased and fixed version of PR #27, which also adds compatibility shim for ServiceInfo.address and does a proper deprecation for it. * Present all addresses that are available. * Add support for publishing multiple addresses. * Add test for backwards compatibility. * 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 * Disable black on ServiceInfo.__init__ until black is fixed Due to psf/black#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.
1 parent 6b85a33 commit c787610

5 files changed

Lines changed: 129 additions & 48 deletions

File tree

examples/browser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ def on_service_state_change(
1919
if state_change is ServiceStateChange.Added:
2020
info = zeroconf.get_service_info(service_type, name)
2121
if info:
22-
print(" Address: %s:%d" % (socket.inet_ntoa(cast(bytes, info.address)), cast(int, info.port)))
22+
addresses = ["%s:%d" % (socket.inet_ntoa(addr), cast(int, info.port)) for addr in info.addresses]
23+
print(" Addresses: %s" % ", ".join(addresses))
2324
print(" Weight: %d, priority: %d" % (info.weight, info.priority))
2425
print(" Server: %s" % (info.server,))
2526
if info.properties:

examples/registration.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@
2020
info = ServiceInfo(
2121
"_http._tcp.local.",
2222
"Paul's Test Web Site._http._tcp.local.",
23-
socket.inet_aton("127.0.0.1"),
24-
80,
25-
0,
26-
0,
27-
desc,
28-
"ash-2.local.",
23+
addresses=[socket.inet_aton("127.0.0.1")],
24+
port=80,
25+
properties=desc,
26+
server="ash-2.local.",
2927
)
3028

3129
zeroconf = Zeroconf()

examples/self_test.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@
2121
info = ServiceInfo(
2222
"_http._tcp.local.",
2323
"My Service Name._http._tcp.local.",
24-
socket.inet_aton("127.0.0.1"),
25-
1234,
26-
0,
27-
0,
28-
desc,
24+
addresses=[socket.inet_aton("127.0.0.1")],
25+
port=1234,
26+
properties=desc,
2927
)
3028
print(" Registering service...")
3129
r.register_service(info)

test_zeroconf.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,3 +958,40 @@ def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
958958
assert service_removed.is_set()
959959
browser.cancel()
960960
zeroconf_browser.close()
961+
962+
963+
def test_multiple_addresses():
964+
type_ = "_http._tcp.local."
965+
registration_name = "xxxyyy.%s" % type_
966+
desc = {'path': '/~paulsm/'}
967+
address = socket.inet_aton("10.0.1.2")
968+
969+
# Old way
970+
info = ServiceInfo(type_, registration_name, address, 80, 0, 0, desc, "ash-2.local.")
971+
972+
assert info.address == address
973+
assert info.addresses == [address]
974+
975+
# Updating works
976+
address2 = socket.inet_aton("10.0.1.3")
977+
info.address = address2
978+
979+
assert info.address == address2
980+
assert info.addresses == [address2]
981+
982+
info.address = None
983+
984+
assert info.address is None
985+
assert info.addresses == []
986+
987+
# Compatibility way
988+
info = ServiceInfo(type_, registration_name, [address, address], 80, 0, 0, desc, "ash-2.local.")
989+
990+
assert info.addresses == [address, address]
991+
992+
# New kwarg way
993+
info = ServiceInfo(
994+
type_, registration_name, None, 80, 0, 0, desc, "ash-2.local.", addresses=[address, address]
995+
)
996+
997+
assert info.addresses == [address, address]

zeroconf.py

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import sys
3131
import threading
3232
import time
33+
import warnings
3334
from functools import reduce
3435
from typing import AnyStr, Dict, List, Optional, Union, cast
3536
from typing import Callable, Set, Tuple # noqa # used in type hints
@@ -1429,38 +1430,59 @@ class ServiceInfo(RecordUpdateListener):
14291430

14301431
"""Service information"""
14311432

1433+
# FIXME(dtantsur): black 19.3b0 produces code that is not valid syntax on
1434+
# Python 3.5: https://github.com/python/black/issues/759
1435+
# fmt: off
14321436
def __init__(
14331437
self,
14341438
type_: str,
14351439
name: str,
1436-
address: Optional[bytes] = None,
1440+
address: Optional[Union[bytes, List[bytes]]] = None,
14371441
port: Optional[int] = None,
14381442
weight: int = 0,
14391443
priority: int = 0,
14401444
properties=b'',
14411445
server: Optional[str] = None,
14421446
host_ttl: int = _DNS_HOST_TTL,
14431447
other_ttl: int = _DNS_OTHER_TTL,
1448+
*,
1449+
addresses: Optional[List[bytes]] = None
14441450
) -> None:
14451451
"""Create a service description.
14461452
14471453
type_: fully qualified service type name
14481454
name: fully qualified service name
1449-
address: IP address as unsigned short, network byte order
1455+
address: IP address as unsigned short, network byte order (deprecated, use addresses)
14501456
port: port that the service runs on
14511457
weight: weight of the service
14521458
priority: priority of the service
14531459
properties: dictionary of properties (or a string holding the
14541460
bytes for the text field)
14551461
server: fully qualified name for service host (defaults to name)
14561462
host_ttl: ttl used for A/SRV records
1457-
other_ttl: ttl used for PTR/TXT records"""
1463+
other_ttl: ttl used for PTR/TXT records
1464+
addresses: List of IP addresses as unsigned short, network byte
1465+
order
1466+
"""
1467+
1468+
# Accept both none, or one, but not both.
1469+
if address is not None and addresses is not None:
1470+
raise TypeError("address and addresses cannot be provided together")
14581471

14591472
if not type_.endswith(service_type_name(name, allow_underscores=True)):
14601473
raise BadTypeInNameException
14611474
self.type = type_
14621475
self.name = name
1463-
self.address = address
1476+
if addresses is not None:
1477+
self.addresses = addresses
1478+
elif address is not None:
1479+
warnings.warn("address is deprecated, use addresses instead", DeprecationWarning)
1480+
if isinstance(address, list):
1481+
self.addresses = address
1482+
else:
1483+
self.addresses = [address]
1484+
else:
1485+
self.addresses = []
14641486
self.port = port
14651487
self.weight = weight
14661488
self.priority = priority
@@ -1472,6 +1494,23 @@ def __init__(
14721494
self._set_properties(properties)
14731495
self.host_ttl = host_ttl
14741496
self.other_ttl = other_ttl
1497+
# fmt: on
1498+
1499+
@property
1500+
def address(self):
1501+
warnings.warn("ServiceInfo.address is deprecated, use addresses instead", DeprecationWarning)
1502+
try:
1503+
return self.addresses[0]
1504+
except IndexError:
1505+
return None
1506+
1507+
@address.setter
1508+
def address(self, value):
1509+
warnings.warn("ServiceInfo.address is deprecated, use addresses instead", DeprecationWarning)
1510+
if value is None:
1511+
self.addresses = []
1512+
else:
1513+
self.addresses = [value]
14751514

14761515
@property
14771516
def properties(self) -> ServicePropertiesType:
@@ -1553,7 +1592,8 @@ def update_record(self, zc: 'Zeroconf', now: float, record: DNSRecord) -> None:
15531592
assert isinstance(record, DNSAddress)
15541593
# if record.name == self.name:
15551594
if record.name == self.server:
1556-
self.address = record.address
1595+
if record.address not in self.addresses:
1596+
self.addresses.append(record.address)
15571597
elif record.type == _TYPE_SRV:
15581598
assert isinstance(record, DNSService)
15591599
if record.name == self.name:
@@ -1585,12 +1625,12 @@ def request(self, zc: 'Zeroconf', timeout: float) -> bool:
15851625
if cached:
15861626
self.update_record(zc, now, cached)
15871627

1588-
if None not in (self.server, self.address, self.text):
1628+
if self.server is not None and self.text is not None and self.addresses:
15891629
return True
15901630

15911631
try:
15921632
zc.add_listener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
1593-
while None in (self.server, self.address, self.text):
1633+
while self.server is None or self.text is None or not self.addresses:
15941634
if last <= now:
15951635
return False
15961636
if next_ <= now:
@@ -1629,7 +1669,16 @@ def __repr__(self) -> str:
16291669
type(self).__name__,
16301670
', '.join(
16311671
'%s=%r' % (name, getattr(self, name))
1632-
for name in ('type', 'name', 'address', 'port', 'weight', 'priority', 'server', 'properties')
1672+
for name in (
1673+
'type',
1674+
'name',
1675+
'addresses',
1676+
'port',
1677+
'weight',
1678+
'priority',
1679+
'server',
1680+
'properties',
1681+
)
16331682
),
16341683
)
16351684

@@ -1916,10 +1965,8 @@ def _broadcast_service(self, info: ServiceInfo) -> None:
19161965
)
19171966

19181967
out.add_answer_at_time(DNSText(info.name, _TYPE_TXT, _CLASS_IN, info.other_ttl, info.text), 0)
1919-
if info.address:
1920-
out.add_answer_at_time(
1921-
DNSAddress(info.server, _TYPE_A, _CLASS_IN, info.host_ttl, info.address), 0
1922-
)
1968+
for address in info.addresses:
1969+
out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, info.host_ttl, address), 0)
19231970
self.send(out)
19241971
i += 1
19251972
next_time += _REGISTER_TIME
@@ -1952,8 +1999,8 @@ def unregister_service(self, info: ServiceInfo) -> None:
19521999
)
19532000
out.add_answer_at_time(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
19542001

1955-
if info.address:
1956-
out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
2002+
for address in info.addresses:
2003+
out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, address), 0)
19572004
self.send(out)
19582005
i += 1
19592006
next_time += _UNREGISTER_TIME
@@ -1986,10 +2033,8 @@ def unregister_all_services(self) -> None:
19862033
0,
19872034
)
19882035
out.add_answer_at_time(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
1989-
if info.address:
1990-
out.add_answer_at_time(
1991-
DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0
1992-
)
2036+
for address in info.addresses:
2037+
out.add_answer_at_time(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, address), 0)
19932038
self.send(out)
19942039
i += 1
19952040
next_time += _UNREGISTER_TIME
@@ -2126,16 +2171,17 @@ def handle_query(self, msg: DNSIncoming, addr: str, port: int) -> None:
21262171
if question.type in (_TYPE_A, _TYPE_ANY):
21272172
for service in self.services.values():
21282173
if service.server == question.name.lower():
2129-
out.add_answer(
2130-
msg,
2131-
DNSAddress(
2132-
question.name,
2133-
_TYPE_A,
2134-
_CLASS_IN | _CLASS_UNIQUE,
2135-
service.host_ttl,
2136-
service.address,
2137-
),
2138-
)
2174+
for address in service.addresses:
2175+
out.add_answer(
2176+
msg,
2177+
DNSAddress(
2178+
question.name,
2179+
_TYPE_A,
2180+
_CLASS_IN | _CLASS_UNIQUE,
2181+
service.host_ttl,
2182+
address,
2183+
),
2184+
)
21392185

21402186
name_to_find = question.name.lower()
21412187
if name_to_find not in self.services:
@@ -2168,15 +2214,16 @@ def handle_query(self, msg: DNSIncoming, addr: str, port: int) -> None:
21682214
),
21692215
)
21702216
if question.type == _TYPE_SRV:
2171-
out.add_additional_answer(
2172-
DNSAddress(
2173-
service.server,
2174-
_TYPE_A,
2175-
_CLASS_IN | _CLASS_UNIQUE,
2176-
service.host_ttl,
2177-
service.address,
2217+
for address in service.addresses:
2218+
out.add_additional_answer(
2219+
DNSAddress(
2220+
service.server,
2221+
_TYPE_A,
2222+
_CLASS_IN | _CLASS_UNIQUE,
2223+
service.host_ttl,
2224+
address,
2225+
)
21782226
)
2179-
)
21802227
except Exception: # TODO stop catching all Exceptions
21812228
self.log_exception_warning()
21822229

0 commit comments

Comments
 (0)