Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/zeroconf/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,11 @@ def generate_unregister_all_services(self) -> DNSOutgoing | None:
return out

async def async_unregister_all_services(self) -> None:
"""Send goodbye packets for every registered service and drop them all.

Runs only at shutdown, so unlike the single service calls it
returns nothing to await separately.
"""
# Send Goodbye packets https://datatracker.ietf.org/doc/html/rfc6762#section-10.1
out = self.generate_unregister_all_services()
if not out:
Expand All @@ -612,6 +617,11 @@ async def async_unregister_all_services(self) -> None:
self.async_send(out)

def unregister_all_services(self) -> None:
"""Send goodbye packets for every registered service and drop them all.

May raise EventLoopBlocked when the event loop cannot finish the
shutdown broadcasts in time.
"""
assert self.loop is not None
run_coro_with_timeout(
self.async_unregister_all_services(),
Expand Down Expand Up @@ -700,6 +710,7 @@ def send(
v6_flow_scope: tuple[()] | tuple[int, int] = (),
transport: _WrappedTransport | None = None,
) -> None:
"""Queue a packet for transmission from any thread."""
assert self.loop is not None
self.loop.call_soon_threadsafe(self.async_send, out, addr, port, v6_flow_scope, transport)

Expand All @@ -711,6 +722,7 @@ def async_send(
v6_flow_scope: tuple[()] | tuple[int, int] = (),
transport: _WrappedTransport | None = None,
) -> None:
"""Transmit a packet from the event loop."""
if self.done:
return

Expand Down
8 changes: 8 additions & 0 deletions src/zeroconf/_dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def _dns_entry_matches(self, other: DNSEntry) -> bool:
return self.key == other.key and self.type == other.type and self.class_ == other.class_

def __eq__(self, other: Any) -> bool:
"""Equal when key, type and class match."""
return isinstance(other, DNSEntry) and self._dns_entry_matches(other)

@staticmethod
Expand Down Expand Up @@ -124,6 +125,7 @@ def __hash__(self) -> int:
return self._hash

def __eq__(self, other: Any) -> bool:
"""Equal when the entry fields match a question."""
return isinstance(other, DNSQuestion) and self._dns_entry_matches(other)

@property
Expand Down Expand Up @@ -268,6 +270,7 @@ def write(self, out: DNSOutgoing) -> None:
out.write_string(self.address)

def __eq__(self, other: Any) -> bool:
"""Equal when address and the entry fields match."""
return isinstance(other, DNSAddress) and self._eq(other)

def _eq(self, other: DNSAddress) -> bool:
Expand Down Expand Up @@ -377,6 +380,7 @@ def write(self, out: DNSOutgoing) -> None:
out.write_name(self.alias)

def __eq__(self, other: Any) -> bool:
"""Equal when alias and the entry fields match."""
return isinstance(other, DNSPointer) and self._eq(other)

def _eq(self, other: DNSPointer) -> bool:
Expand Down Expand Up @@ -421,6 +425,7 @@ def __hash__(self) -> int:
return self._hash

def __eq__(self, other: Any) -> bool:
"""Equal when text and the entry fields match."""
return isinstance(other, DNSText) and self._eq(other)

def _eq(self, other: DNSText) -> bool:
Expand Down Expand Up @@ -502,6 +507,8 @@ def __repr__(self) -> str:


class DNSNsec(DNSRecord):
"""NSEC record asserting which record types exist for a name."""

__slots__ = ("_hash", "next_name", "rdtypes")

def __init__(
Expand Down Expand Up @@ -552,6 +559,7 @@ def write(self, out: DNSOutgoing) -> None:
out.write_string(out_bytes)

def __eq__(self, other: Any) -> bool:
"""Equal when next_name, rdtypes and the entry fields match."""
return isinstance(other, DNSNsec) and self._eq(other)

def _eq(self, other: DNSNsec) -> bool:
Expand Down
1 change: 1 addition & 0 deletions src/zeroconf/_protocol/incoming.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ def _read_record(
return None

def _read_bitmap(self, end: _int) -> list[int]:
"""Decode the NSEC type bitmap."""
rdtypes = []
if end > self._data_len:
raise IncomingDecodeError(
Expand Down
35 changes: 35 additions & 0 deletions src/zeroconf/_protocol/outgoing.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,41 @@ def add_authorative_answer(self, record: DNSPointer) -> None:
self.authorities.append(record)

def add_additional_answer(self, record: DNSRecord) -> None:
"""Append a record to the additional section per DNS-SD guidance.

From: RFC 6763, DNS-Based Service Discovery, February 2013

12. DNS Additional Record Generation

DNS has an efficiency feature whereby a DNS server may place
additional records in the additional section of the DNS message.
These additional records are records that the client did not
explicitly request, but the server has reasonable grounds to expect
that the client might request them shortly, so including them can
save the client from having to issue additional queries.

This section recommends which additional records SHOULD be generated
to improve network efficiency, for both Unicast and Multicast DNS-SD
responses.

12.1. PTR Records

When including a DNS-SD Service Instance Enumeration or Selective
Instance Enumeration (subtype) PTR record in a response packet, the
server/responder SHOULD include the following additional records:

o The SRV record(s) named in the PTR rdata.
o The TXT record(s) named in the PTR rdata.
o All address records (type "A" and "AAAA") named in the SRV rdata.

12.2. SRV Records

When including an SRV record in a response packet, the
server/responder SHOULD include the following additional records:

o All address records (type "A" and "AAAA") named in the SRV rdata.

"""
self.additionals.append(record)

def _write_byte(self, value: int_) -> None:
Expand Down
2 changes: 2 additions & 0 deletions src/zeroconf/_utils/name.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def service_type_name(type_: str, *, strict: bool = True) -> str: # pylint: dis
"""
Validate a fully qualified service name, instance or subtype. [rfc6763]

Returns the name in its validated, fully qualified form.

Domain names used by mDNS-SD take the following forms:

<sn> . <_tcp|_udp> . local.
Expand Down
5 changes: 5 additions & 0 deletions src/zeroconf/_utils/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@


def current_time_millis() -> _float:
"""Monotonic clock reading in milliseconds.

Must stay aligned with asyncio.loop.time(); the backing clock is an
implementation detail and may change.
"""
return time.monotonic() * 1000


Expand Down
5 changes: 5 additions & 0 deletions src/zeroconf/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ async def async_register_service(
)

async def async_unregister_all_services(self) -> None:
"""Send goodbye packets for every registered service and drop them all.

Runs only at shutdown, so unlike the single service calls it
returns nothing to await separately.
"""
await self.zeroconf.async_unregister_all_services()

async def async_unregister_service(self, info: ServiceInfo) -> Awaitable:
Expand Down
Loading