Skip to content

Commit 55061e7

Browse files
authored
chore: restore behavior with freshly authored replacements (#1844)
1 parent 746ce55 commit 55061e7

13 files changed

Lines changed: 220 additions & 73 deletions

File tree

‎examples/browser.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python
2+
3+
"""Browse for HTTP services with the synchronous API and print each event."""
4+
5+
from __future__ import annotations
6+
7+
import argparse
8+
import logging
9+
10+
from zeroconf import IPVersion, ServiceBrowser, ServiceListener, Zeroconf
11+
12+
13+
class PrintingListener(ServiceListener):
14+
def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
15+
info = zc.get_service_info(type_, name)
16+
print(f"added: {name}")
17+
if info is not None:
18+
print(f" addresses: {', '.join(info.parsed_scoped_addresses())}")
19+
print(f" server: {info.server} port: {info.port}")
20+
print(f" properties: {info.decoded_properties}")
21+
22+
def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
23+
print(f"removed: {name}")
24+
25+
def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
26+
print(f"updated: {name}")
27+
28+
29+
if __name__ == "__main__":
30+
parser = argparse.ArgumentParser(description=__doc__)
31+
parser.add_argument("--debug", action="store_true", help="enable debug logging")
32+
parser.add_argument("--v6-only", action="store_true", help="use IPv6 only")
33+
args = parser.parse_args()
34+
35+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
36+
ip_version = IPVersion.V6Only if args.v6_only else IPVersion.All
37+
38+
zc = Zeroconf(ip_version=ip_version)
39+
browser = ServiceBrowser(zc, "_http._tcp.local.", PrintingListener())
40+
print("browsing for _http._tcp.local., press enter to exit")
41+
try:
42+
input()
43+
finally:
44+
zc.close()

‎examples/registration.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python
2+
3+
"""Register a demo HTTP service and keep it announced until interrupted."""
4+
5+
from __future__ import annotations
6+
7+
import argparse
8+
import logging
9+
import socket
10+
from time import sleep
11+
12+
from zeroconf import IPVersion, ServiceInfo, Zeroconf
13+
14+
if __name__ == "__main__":
15+
parser = argparse.ArgumentParser(description=__doc__)
16+
parser.add_argument("--debug", action="store_true", help="enable debug logging")
17+
parser.add_argument("--v6-only", action="store_true", help="use IPv6 only")
18+
args = parser.parse_args()
19+
20+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
21+
ip_version = IPVersion.V6Only if args.v6_only else IPVersion.All
22+
23+
service = ServiceInfo(
24+
"_http._tcp.local.",
25+
"Demo Web Service._http._tcp.local.",
26+
addresses=[socket.inet_aton("127.0.0.1")],
27+
port=8080,
28+
properties={"path": "/"},
29+
server="demo-host.local.",
30+
)
31+
32+
zc = Zeroconf(ip_version=ip_version)
33+
print("registering Demo Web Service._http._tcp.local., press ctrl-c to exit")
34+
zc.register_service(service)
35+
try:
36+
while True:
37+
sleep(0.5)
38+
except KeyboardInterrupt:
39+
pass
40+
finally:
41+
print("unregistering")
42+
zc.unregister_service(service)
43+
zc.close()

‎src/zeroconf/_core.py‎

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ def async_send_with_transport(
150150

151151

152152
class Zeroconf(QuietLogger):
153-
153+
"""High level mDNS service discovery: register services, browse for
154+
them, and resolve their details on the local network."""
154155

155156
def __init__(
156157
self,
@@ -160,7 +161,7 @@ def __init__(
160161
apple_p2p: bool = False,
161162
use_asyncio: bool | None = None,
162163
) -> None:
163-
multicast communications, listening and reaping threads.
164+
"""Open the multicast sockets and start serving on the chosen interfaces.
164165
165166
:param interfaces: :class:`InterfaceChoice` or a list of IP addresses
166167
(IPv4 and IPv6) and interface indexes (IPv6 only).
@@ -317,17 +318,18 @@ def get_service_info(
317318
return None
318319

319320
def add_service_listener(self, type_: str, listener: ServiceListener) -> None:
320-
will then have its add_service and remove_service methods called when
321-
services of that type become available and unavailable."""
321+
"""Browse for a type and deliver add, remove, and update callbacks to the listener."""
322322
self.remove_service_listener(listener)
323323
self.browsers[listener] = ServiceBrowser(self, type_, listener)
324324

325325
def remove_service_listener(self, listener: ServiceListener) -> None:
326+
"""Stop the browser behind the given listener."""
326327
if listener in self.browsers:
327328
self.browsers[listener].cancel()
328329
del self.browsers[listener]
329330

330331
def remove_all_service_listeners(self) -> None:
332+
"""Stop the browsers behind every registered listener."""
331333
for listener in list(self.browsers):
332334
self.remove_service_listener(listener)
333335

@@ -647,6 +649,7 @@ async def async_check_service(
647649
cooperating_responders: bool = False,
648650
strict: bool = True,
649651
) -> None:
652+
"""Probe the network for name uniqueness, renaming first when allowed."""
650653
instance_name = instance_name_from_service_info(info, strict=strict)
651654
if cooperating_responders:
652655
return
@@ -685,11 +688,7 @@ def add_listener(
685688
listener: RecordUpdateListener,
686689
question: DNSQuestion | list[DNSQuestion] | None,
687690
) -> None:
688-
its update_record method called when information is available to
689-
answer the question(s).
690-
691-
This function is threadsafe
692-
"""
691+
"""Subscribe a record update listener, optionally scoped to questions; threadsafe."""
693692
assert self.loop is not None
694693
self.loop.call_soon_threadsafe(self.record_manager.async_add_listener, listener, question)
695694

@@ -706,11 +705,7 @@ def async_add_listener(
706705
listener: RecordUpdateListener,
707706
question: DNSQuestion | list[DNSQuestion] | None,
708707
) -> None:
709-
its update_record method called when information is available to
710-
answer the question(s).
711-
712-
This function is not threadsafe and must be called in the eventloop.
713-
"""
708+
"""Subscribe a record update listener, optionally scoped to questions; event loop only."""
714709
self.record_manager.async_add_listener(listener, question)
715710

716711
def async_remove_listener(self, listener: RecordUpdateListener) -> None:
@@ -797,10 +792,7 @@ def _shutdown_threads(self) -> None:
797792
self.loop.close()
798793

799794
def close(self) -> None:
800-
servicing further queries.
801-
802-
This method is idempotent and irreversible.
803-
"""
795+
"""Shut down the sockets and engine for good; safe to call repeatedly."""
804796
assert self.loop is not None
805797
if self.loop.is_running():
806798
if self.loop == get_running_loop():
@@ -814,15 +806,7 @@ def close(self) -> None:
814806
self._shutdown_threads()
815807

816808
async def _async_close(self) -> None:
817-
servicing further queries.
818-
819-
This method is idempotent and irreversible.
820-
821-
This call only intended to be used by AsyncZeroconf
822-
823-
Callers are responsible for unregistering all services
824-
before calling this function
825-
"""
809+
"""Shut down for AsyncZeroconf; callers unregister services first."""
826810
self._close()
827811
await self.engine._async_close() # pylint: disable=protected-access
828812
self._shutdown_threads()

‎src/zeroconf/_dns.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import socket
2727
from typing import TYPE_CHECKING, Any, cast
2828

29+
from ._exceptions import AbstractMethodException
2930
from ._utils.net import _is_v6_address
3031
from ._utils.time import current_time_millis
3132
from .const import _CLASS_MASK, _CLASS_UNIQUE, _CLASSES, _TYPE_ANY, _TYPES
@@ -156,7 +157,7 @@ def __repr__(self) -> str:
156157
)
157158

158159

159-
class DNSRecord(DNSEntry):
160+
class DNSRecord(DNSEntry): # noqa: PLW1641
160161
"""A DNS record - like a DNS entry, but has a TTL"""
161162

162163
__slots__ = ("created", "ttl")
@@ -177,6 +178,9 @@ def _fast_init_record(self, name: str, type_: _int, class_: _int, ttl: _int, cre
177178
self.ttl = ttl
178179
self.created = created
179180

181+
def __eq__(self, other: Any) -> bool:
182+
raise AbstractMethodException(f"{type(self).__name__} is missing __eq__")
183+
180184
def __lt__(self, other: DNSRecord) -> bool:
181185
return self.ttl < other.ttl
182186

@@ -187,6 +191,16 @@ def suppressed_by(self, msg: DNSIncoming) -> bool:
187191
return True
188192
return False
189193

194+
def _suppressed_by_answer(self, answer: DNSRecord) -> bool:
195+
"""True when the answer matches this record with at least half its TTL left."""
196+
return self == answer and self.ttl / 2 < answer.ttl
197+
198+
def get_expiration_time(self, percent: _int) -> float:
199+
"""Return the moment when the given percentage of the TTL has elapsed."""
200+
# created is epoch milliseconds and ttl is seconds, so one percent
201+
# of the ttl expressed in milliseconds is ttl * 10
202+
return self.created + percent * self.ttl * 10
203+
190204
# TODO: Switch to just int here
191205
def get_remaining_ttl(self, now: _float) -> int | float:
192206
"""Returns the remaining TTL in seconds."""
@@ -211,6 +225,9 @@ def _set_created_ttl(self, created: _float, ttl: _int) -> None:
211225
self.created = created
212226
self.ttl = ttl
213227

228+
def write(self, out: DNSOutgoing) -> None:
229+
raise AbstractMethodException(f"{type(self).__name__} is missing write")
230+
214231
def to_string(self, other: bytes | str) -> str:
215232
arg = f"{self.ttl}/{int(self.get_remaining_ttl(current_time_millis()))},{cast(Any, other)}"
216233
return DNSEntry.entry_to_string(self, "record", arg)

‎src/zeroconf/_handlers/record_manager.py‎

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,32 +53,21 @@ def __init__(self, zeroconf: Zeroconf) -> None:
5353
self.listeners: set[RecordUpdateListener] = set()
5454

5555
def async_updates(self, now: _float, records: list[_RecordUpdate]) -> None:
56-
a record.
57-
58-
This method must be called before the cache is updated.
59-
60-
This method will be run in the event loop.
61-
"""
56+
"""Notify listeners of updated records, before the cache commits them."""
6257
for listener in self.listeners.copy():
6358
listener.async_update_records(self.zc, now, records)
6459

6560
def async_updates_complete(self, notify: bool) -> None:
66-
a record.
67-
68-
This method must be called after the cache is updated.
69-
70-
This method will be run in the event loop.
71-
"""
61+
"""Notify listeners that a batch landed, after the cache committed it."""
7262
for listener in self.listeners.copy():
7363
listener.async_update_records_complete()
7464
if notify:
7565
self.zc.async_notify_all()
7666

7767
def async_updates_from_response(self, msg: DNSIncoming) -> None:
78-
are held in the cache, and listeners are notified.
68+
"""Ingest a response: cache its answers and notify listeners.
7969
80-
This function must be run in the event loop as it is not
81-
threadsafe.
70+
Runs in the event loop; not threadsafe.
8271
"""
8372
updates: list[RecordUpdate] = []
8473
address_adds: list[DNSRecord] = []
@@ -168,10 +157,8 @@ def async_add_listener(
168157
listener: RecordUpdateListener,
169158
question: DNSQuestion | list[DNSQuestion] | None,
170159
) -> None:
171-
its update_record method called when information is available to
172-
answer the question(s).
173-
174-
This function is not thread-safe and must be called in the eventloop.
160+
"""Subscribe a listener, optionally scoped to questions, and seed it
161+
with matching cached records; event loop only.
175162
"""
176163
if not isinstance(listener, RecordUpdateListener):
177164
log.error( # type: ignore[unreachable]

‎src/zeroconf/_listener.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555

5656

5757
class AsyncListener:
58-
59-
the read() method called when a socket is available for reading."""
58+
"""Asyncio datagram protocol that receives mDNS packets and feeds them
59+
to the record manager and query handler as they arrive."""
6060

6161
__slots__ = (
6262
"_deferred",
@@ -236,6 +236,7 @@ def handle_query_or_defer(
236236
transport: _WrappedTransport,
237237
v6_flow_scope: tuple[()] | tuple[int, int],
238238
) -> None:
239+
"""Answer immediately, or hold a truncated query for reassembly."""
239240
if not msg.truncated:
240241
self._respond_query(msg, addr, port, transport, v6_flow_scope)
241242
return

‎src/zeroconf/_protocol/incoming.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ def _read_string(self, length: _int) -> bytes:
311311
return info
312312

313313
def _read_others(self) -> None:
314+
"""Read the answer, authority and additional sections."""
314315
self._did_read_others = True
315316
buf = self._buf
316317
answers = self._answers

‎src/zeroconf/_protocol/outgoing.pxd‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ cdef class DNSOutgoing:
6565
cdef cython.bint _write_question(self, DNSQuestion question)
6666

6767
@cython.locals(
68-
d=cython.bytes,
69-
data_view=cython.list,
70-
index=cython.uint,
71-
length=cython.uint
68+
data_length_before=cython.uint,
69+
size_before=cython.uint,
70+
length_index=cython.uint,
71+
rdata_size_start=cython.uint,
7272
)
7373
cdef cython.bint _write_record(self, DNSRecord record, double now)
7474

@@ -110,7 +110,7 @@ cdef class DNSOutgoing:
110110

111111
cpdef void write_character_string(self, cython.bytes value)
112112

113-
@cython.locals(utfstr=bytes)
113+
@cython.locals(encoded=bytes, byte_length=cython.uint)
114114
cdef void _write_utf(self, cython.str value)
115115

116116
@cython.locals(
@@ -132,7 +132,6 @@ cdef class DNSOutgoing:
132132

133133
cpdef void add_answer(self, DNSIncoming inp, DNSRecord record)
134134

135-
@cython.locals(now_double=double)
136135
cpdef void add_answer_at_time(self, DNSRecord record, double now)
137136

138137
cpdef void add_authorative_answer(self, DNSPointer record)

0 commit comments

Comments
 (0)