GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat: report whether async_update_interfaces brought every interface up · python-zeroconf/python-zeroconf@c52e18f

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • src/zeroconf
    • _core.py
    • _engine.py
    • asyncio.py
  • tests
    • test_interface_update.py
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/zeroconf/_core.py‎

Copy file name to clipboardExpand all lines: src/zeroconf/_core.py
+30-21Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import threading
1515
from collections.abc import Awaitable
1616
from types import TracebackType
17-
from typing import Self
17+
from typing import Self, cast
1818

1919
from ._cache import DNSCache
2020
from ._dns import DNSQuestion, DNSQuestionType
@@ -417,7 +417,7 @@ async def async_update_interfaces(
417417
interfaces: InterfacesType | None = None,
418418
ip_version: IPVersion | None = None,
419419
apple_p2p: bool | None = None,
420-
) -> None:
420+
) -> bool:
421421
"""Rescan network interfaces and reconcile the sockets in use.
422422
423423
Adds sockets for interfaces that appeared, drops sockets for
@@ -432,8 +432,11 @@ async def async_update_interfaces(
432432
is best-effort: a requested interface that fails to bind, or fails to
433433
re-join after a rebuild, is logged rather than raised, and likewise a
434434
registration that fails to re-announce is logged so one failure cannot
435-
block the others. Raises RuntimeError if apple_p2p is set on a non-Apple
436-
platform (input validation, matching the constructor).
435+
block the others. Returns True if every requested interface is up, and
436+
False if any is not. When no interface is available or the listen socket
437+
cannot be rebuilt for a new address family, nothing is changed and the
438+
previous configuration is kept. Raises RuntimeError if apple_p2p is
439+
set on a non-Apple platform (input validation, matching the constructor).
437440
"""
438441
# Resolve against the retained config but only commit it after the
439442
# engine reconcile succeeds, so a failed reconcile leaves the stored
@@ -449,14 +452,15 @@ async def async_update_interfaces(
449452
# alone; the multi-second re-announce runs unlocked so a bursty
450453
# adapter-change source isn't blocked behind it.
451454
async with self._interface_update_lock:
452-
added = await self.engine.async_update_interfaces(interfaces, ip_version, apple_p2p)
453-
# Copy a mutable list so later caller mutation can't change the
454-
# retained configuration.
455-
self._interfaces = list(interfaces) if isinstance(interfaces, list) else interfaces
456-
self._ip_version = ip_version
457-
self._apple_p2p = apple_p2p
458-
if not added:
459-
return
455+
outcome = await self.engine.async_update_interfaces(interfaces, ip_version, apple_p2p)
456+
if outcome.applied:
457+
# Copy a mutable list so later caller mutation can't change the
458+
# retained configuration.
459+
self._interfaces = list(interfaces) if isinstance(interfaces, list) else interfaces
460+
self._ip_version = ip_version
461+
self._apple_p2p = apple_p2p
462+
if not outcome.added:
463+
return outcome.complete
460464
# Re-announce every registration; one broadcast failing must not mask
461465
# the rest, so collect exceptions and log them individually, naming the
462466
# service so a partial failure is actionable.
@@ -472,6 +476,7 @@ async def async_update_interfaces(
472476
# gather(return_exceptions=True) also captures BaseExceptions
473477
# such as CancelledError; don't swallow a cancellation/interrupt.
474478
raise result
479+
return outcome.complete
475480

476481
async def async_update_service(self, info: ServiceInfo) -> Awaitable:
477482
"""Publish updated records for an already registered service.
@@ -682,23 +687,27 @@ def update_interfaces(
682687
interfaces: InterfacesType | None = None,
683688
ip_version: IPVersion | None = None,
684689
apple_p2p: bool | None = None,
685-
) -> None:
690+
) -> bool:
686691
"""Rescan network interfaces and reconcile the sockets in use.
687692
688-
While it is not expected during normal operation,
689-
this function may raise EventLoopBlocked if the underlying
690-
call to `async_update_interfaces` cannot be completed. Raises
691-
RuntimeError if apple_p2p is set on a non-Apple platform.
693+
Returns whether every requested interface is up, as
694+
`async_update_interfaces` does. While it is not expected during
695+
normal operation, this function may raise EventLoopBlocked if the
696+
underlying call to `async_update_interfaces` cannot be completed.
697+
Raises RuntimeError if apple_p2p is set on a non-Apple platform.
692698
"""
693699
assert self.loop is not None
694700
# Unlike register/update, the re-announce is awaited inline (to log
695701
# per-service failures), so the budget must cover the full announce
696702
# window ((_REGISTER_BROADCASTS - 1) * _REGISTER_TIME) plus the reconcile
697703
# and wait-for-start overhead; double the register budget for headroom.
698-
run_coro_with_timeout(
699-
self.async_update_interfaces(interfaces, ip_version, apple_p2p),
700-
self.loop,
701-
_REGISTER_TIME * _REGISTER_BROADCASTS * 2,
704+
return cast(
705+
bool,
706+
run_coro_with_timeout(
707+
self.async_update_interfaces(interfaces, ip_version, apple_p2p),
708+
self.loop,
709+
_REGISTER_TIME * _REGISTER_BROADCASTS * 2,
710+
),
702711
)
703712

704713
def update_service(self, info: ServiceInfo) -> None:
Collapse file

‎src/zeroconf/_engine.py‎

Copy file name to clipboardExpand all lines: src/zeroconf/_engine.py
+30-10Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import itertools
1212
import socket
1313
import threading
14-
from typing import TYPE_CHECKING, cast
14+
from typing import TYPE_CHECKING, NamedTuple, cast
1515

1616
from ._logger import log
1717
from ._record_update import RecordUpdate
@@ -82,6 +82,17 @@ def _without_transport(
8282
return [wrapped for wrapped in wrappers if wrapped.transport is not transport]
8383

8484

85+
class InterfaceUpdateResult(NamedTuple):
86+
"""Outcome of reconciling the sockets to a requested interface set."""
87+
88+
applied: bool
89+
added: bool
90+
complete: bool
91+
92+
93+
_NOT_APPLIED = InterfaceUpdateResult(applied=False, added=False, complete=False)
94+
95+
8596
class AsyncEngine:
8697
"""An engine wraps sockets in the event loop."""
8798

@@ -122,16 +133,18 @@ async def async_update_interfaces(
122133
interfaces: InterfacesType,
123134
ip_version: IPVersion,
124135
apple_p2p: bool,
125-
) -> bool:
136+
) -> InterfaceUpdateResult:
126137
"""Reconcile sender/reader sockets to the live interface set.
127138
128139
Adds a per-interface responder socket for each interface that
129140
appeared and tears down the socket for each interface that
130141
disappeared, diffing on the bound address. A Default single-family
131142
instance's dual-use listen/responder socket is converted to a pure
132143
listener when moving to an explicit set; otherwise the shared listen
133-
socket is left intact. Returns whether any responder socket was
134-
added, so the caller can skip re-announcing when nothing appeared.
144+
socket is left intact. The result reports whether the reconcile ran
145+
at all, whether any responder socket was added (so the caller can skip
146+
re-announcing when nothing appeared), and whether every desired
147+
interface ended up bound and joined.
135148
"""
136149
assert self.loop is not None
137150
try:
@@ -143,7 +156,7 @@ async def async_update_interfaces(
143156
# a momentary down state doesn't crash a caller's adapter-change
144157
# handler (best-effort contract); the next rescan reconciles.
145158
log.warning("Skipping interface update; no interfaces available: %s", exc)
146-
return False
159+
return _NOT_APPLIED
147160
desired = {_interface_key(interface): interface for interface in normalized}
148161
current = {wrapped.interface_key: wrapped for wrapped in self.senders}
149162
listen_transport = self._listen_transport
@@ -176,16 +189,17 @@ async def async_update_interfaces(
176189
current.pop(listen_key, None)
177190
needs_rebuild = True
178191

192+
complete = True
179193
if needs_rebuild:
180194
try:
181-
await self._async_rebuild_listen_socket(apple_p2p, desired, current)
195+
complete = await self._async_rebuild_listen_socket(apple_p2p, desired, current)
182196
except (OSError, RuntimeError) as exc:
183197
# A fresh wildcard bind / endpoint creation can transiently fail
184198
# during adapter churn. The rebuild raises before tearing down
185199
# the old listen socket, so state is unchanged; log and no-op
186200
# rather than crash the caller's handler (best-effort contract).
187201
log.warning("Skipping interface update; listen socket rebuild failed: %s", exc)
188-
return False
202+
return _NOT_APPLIED
189203
listen_transport = self._listen_transport
190204
listen_socket = listen_transport.sock if listen_transport is not None else None
191205

@@ -207,7 +221,9 @@ async def async_update_interfaces(
207221
continue
208222
if await self._async_add_interface(interface, listen_socket, apple_p2p):
209223
added = True
210-
return added
224+
else:
225+
complete = False
226+
return InterfaceUpdateResult(applied=True, added=added, complete=complete)
211227

212228
def close(self) -> None:
213229
"""Close from sync context.
@@ -341,7 +357,7 @@ async def _async_rebuild_listen_socket(
341357
apple_p2p: bool,
342358
desired: dict[tuple[str, int], str | tuple[tuple[str, int, int], int]],
343359
current: dict[tuple[str, int], _WrappedTransport],
344-
) -> None:
360+
) -> bool:
345361
"""Replace the listen socket with one whose family covers the desired set.
346362
347363
The listen socket's family is otherwise fixed at construction; this
@@ -351,7 +367,8 @@ async def _async_rebuild_listen_socket(
351367
requested ip_version, which an explicit list can contradict) so it
352368
always covers every desired interface and never needs an immediate
353369
re-rebuild. Interfaces that are staying are re-joined on the new socket,
354-
and the old socket is closed (releasing its memberships).
370+
and the old socket is closed (releasing its memberships). Returns
371+
whether every staying interface re-joined.
355372
"""
356373
has_v6 = any(isinstance(interface, tuple) for interface in desired.values())
357374
has_v4 = any(not isinstance(interface, tuple) for interface in desired.values())
@@ -364,12 +381,14 @@ async def _async_rebuild_listen_socket(
364381
new_listen = new_listen_socket(family_version, apple_p2p)
365382
if new_listen is None:
366383
raise RuntimeError("Failed to create a listen socket for the new interface family")
384+
rejoined = True
367385
try:
368386
for bind_address, interface in desired.items():
369387
# A staying interface that can't re-join on the new socket keeps
370388
# its sender but receives only via the shared socket it never
371389
# joined; surface that degraded state like _async_add_interface.
372390
if bind_address in current and not add_multicast_member(new_listen, interface):
391+
rejoined = False
373392
self.zc.log_warning_once(
374393
f"Interface {interface!r} could not re-join the multicast group "
375394
"on the rebuilt listen socket"
@@ -388,6 +407,7 @@ async def _async_rebuild_listen_socket(
388407
old_transport = old_listen_transport.transport
389408
self._async_remove_transport(old_transport)
390409
old_transport.close()
410+
return rejoined
391411

392412
def _async_remove_transport(self, transport: asyncio.DatagramTransport) -> None:
393413
"""Drop a transport's protocol/reader/sender wrappers, cancelling its timers."""
Collapse file

‎src/zeroconf/asyncio.py‎

Copy file name to clipboardExpand all lines: src/zeroconf/asyncio.py
+4-3Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,17 @@ async def async_update_interfaces(
230230
interfaces: InterfacesType | None = None,
231231
ip_version: IPVersion | None = None,
232232
apple_p2p: bool | None = None,
233-
) -> None:
233+
) -> bool:
234234
"""Rescan network interfaces and reconcile the sockets in use.
235235
236236
Adds sockets for interfaces that appeared, drops sockets for
237237
interfaces that disappeared, and re-announces existing
238238
registrations on the resulting senders. ``interfaces``,
239239
``ip_version`` and ``apple_p2p`` each default to the construction-time
240-
value. Raises RuntimeError if apple_p2p is set on a non-Apple platform.
240+
value. Returns whether every requested interface is up. Raises
241+
RuntimeError if apple_p2p is set on a non-Apple platform.
241242
"""
242-
await self.zeroconf.async_update_interfaces(interfaces, ip_version, apple_p2p)
243+
return await self.zeroconf.async_update_interfaces(interfaces, ip_version, apple_p2p)
243244

244245
async def async_update_service(self, info: ServiceInfo) -> Awaitable:
245246
"""Publish updated records for an already registered service.

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.