| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -199,6 +199,12 @@ def __init__( | |||
| 199 | 199 | ||
| 200 | 200 | self.unicast = unicast | |
| 201 | 201 | self._use_asyncio = use_asyncio | |
| 202 | + # Retained so async_update_interfaces can re-run create_sockets / | ||
| 203 | + # normalize_interface_choice against the live interface set later. | ||
| 204 | + # Copy a mutable list so later caller mutation can't change it. | ||
| 205 | + self._interfaces = list(interfaces) if isinstance(interfaces, list) else interfaces | ||
| 206 | + self._ip_version = ip_version | ||
| 207 | + self._apple_p2p = apple_p2p | ||
| 202 | 208 | listen_socket, respond_sockets = create_sockets(interfaces, unicast, ip_version, apple_p2p=apple_p2p) | |
| 203 | 209 | log.debug("Listen socket %s, respond sockets %s", listen_socket, respond_sockets) | |
| 204 | 210 | ||
@@ -216,6 +222,9 @@ def __init__( | |||
| 216 | 222 | self.record_manager = RecordManager(self) | |
| 217 | 223 | ||
| 218 | 224 | self._notify_futures: set[asyncio.Future] = set() | |
| 225 | + # Serializes async_update_interfaces so overlapping calls (a bursty | ||
| 226 | + # adapter-change source) don't diff against a stale sender snapshot. | ||
| 227 | + self._interface_update_lock = asyncio.Lock() | ||
| 219 | 228 | self.loop: asyncio.AbstractEventLoop | None = None | |
| 220 | 229 | self._loop_thread: threading.Thread | None = None | |
| 221 | 230 | ||
@@ -406,6 +415,91 @@ async def async_update_service(self, info: ServiceInfo) -> Awaitable: | |||
| 406 | 415 | self.registry.async_update(info) | |
| 407 | 416 | return asyncio.ensure_future(self._async_broadcast_service(info, _REGISTER_TIME, None)) | |
| 408 | 417 | ||
| 418 | + def update_interfaces( | ||
| 419 | + self, | ||
| 420 | + interfaces: InterfacesType | None = None, | ||
| 421 | + ip_version: IPVersion | None = None, | ||
| 422 | + apple_p2p: bool | None = None, | ||
| 423 | + ) -> None: | ||
| 424 | + """Rescan network interfaces and reconcile the sockets in use. | ||
| 425 | + | ||
| 426 | + While it is not expected during normal operation, | ||
| 427 | + this function may raise EventLoopBlocked if the underlying | ||
| 428 | + call to `async_update_interfaces` cannot be completed. Raises | ||
| 429 | + RuntimeError if apple_p2p is set on a non-Apple platform. | ||
| 430 | + """ | ||
| 431 | + assert self.loop is not None | ||
| 432 | + # Unlike register/update, the re-announce is awaited inline (to log | ||
| 433 | + # per-service failures), so the budget must cover the full announce | ||
| 434 | + # window ((_REGISTER_BROADCASTS - 1) * _REGISTER_TIME) plus the reconcile | ||
| 435 | + # and wait-for-start overhead; double the register budget for headroom. | ||
| 436 | + run_coro_with_timeout( | ||
| 437 | + self.async_update_interfaces(interfaces, ip_version, apple_p2p), | ||
| 438 | + self.loop, | ||
| 439 | + _REGISTER_TIME * _REGISTER_BROADCASTS * 2, | ||
| 440 | + ) | ||
| 441 | + | ||
| 442 | + async def async_update_interfaces( | ||
| 443 | + self, | ||
| 444 | + interfaces: InterfacesType | None = None, | ||
| 445 | + ip_version: IPVersion | None = None, | ||
| 446 | + apple_p2p: bool | None = None, | ||
| 447 | + ) -> None: | ||
| 448 | + """Rescan network interfaces and reconcile the sockets in use. | ||
| 449 | + | ||
| 450 | + Adds sockets for interfaces that appeared, drops sockets for | ||
| 451 | + interfaces that disappeared, and re-announces existing | ||
| 452 | + registrations when a new sender appeared. ``interfaces``, | ||
| 453 | + ``ip_version`` and ``apple_p2p`` each default to the value passed at | ||
| 454 | + construction; pass a new value to switch it. When the resulting | ||
| 455 | + interface set is unchanged this is a no-op (no sockets touched, | ||
| 456 | + nothing re-announced). The listen socket is rebuilt if the new set | ||
| 457 | + needs a different address family; unicast mode is fixed at | ||
| 458 | + construction. Concurrent calls are serialized. Bringing up interfaces | ||
| 459 | + is best-effort: a requested interface that fails to bind, or fails to | ||
| 460 | + re-join after a rebuild, is logged rather than raised, and likewise a | ||
| 461 | + registration that fails to re-announce is logged so one failure cannot | ||
| 462 | + block the others. Raises RuntimeError if apple_p2p is set on a non-Apple | ||
| 463 | + platform (input validation, matching the constructor). | ||
| 464 | + """ | ||
| 465 | + # Resolve against the retained config but only commit it after the | ||
| 466 | + # engine reconcile succeeds, so a failed reconcile leaves the stored | ||
| 467 | + # config unchanged rather than recording a set that never fully bound | ||
| 468 | + # (a mid-reconcile failure may still have changed some sockets). | ||
| 469 | + interfaces = self._interfaces if interfaces is None else interfaces | ||
| 470 | + ip_version = self._ip_version if ip_version is None else ip_version | ||
| 471 | + apple_p2p = self._apple_p2p if apple_p2p is None else apple_p2p | ||
| 472 | + if apple_p2p and sys.platform != "darwin": | ||
| 473 | + raise RuntimeError("Option `apple_p2p` is not supported on non-Apple platforms.") | ||
| 474 | + await self.async_wait_for_start() | ||
| 475 | + # Only the reconcile mutates the sender set, so hold the lock for that | ||
| 476 | + # alone; the multi-second re-announce runs unlocked so a bursty | ||
| 477 | + # adapter-change source isn't blocked behind it. | ||
| 478 | + async with self._interface_update_lock: | ||
| 479 | + added = await self.engine.async_update_interfaces(interfaces, ip_version, apple_p2p) | ||
| 480 | + # Copy a mutable list so later caller mutation can't change the | ||
| 481 | + # retained configuration. | ||
| 482 | + self._interfaces = list(interfaces) if isinstance(interfaces, list) else interfaces | ||
| 483 | + self._ip_version = ip_version | ||
| 484 | + self._apple_p2p = apple_p2p | ||
| 485 | + if not added: | ||
| 486 | + return | ||
| 487 | + # Re-announce every registration; one broadcast failing must not mask | ||
| 488 | + # the rest, so collect exceptions and log them individually, naming the | ||
| 489 | + # service so a partial failure is actionable. | ||
| 490 | + infos = self.registry.async_get_service_infos() | ||
| 491 | + results = await asyncio.gather( | ||
| 492 | + *[self._async_broadcast_service(info, _REGISTER_TIME, None) for info in infos], | ||
| 493 | + return_exceptions=True, | ||
| 494 | + ) | ||
| 495 | + for info, result in zip(infos, results, strict=True): | ||
| 496 | + if isinstance(result, Exception): | ||
| 497 | + log.warning("Error re-announcing %s after interface update: %s", info.name, result) | ||
| 498 | + elif isinstance(result, BaseException): | ||
| 499 | + # gather(return_exceptions=True) also captures BaseExceptions | ||
| 500 | + # such as CancelledError; don't swallow a cancellation/interrupt. | ||
| 501 | + raise result | ||
| 502 | + | ||
| 409 | 503 | async def async_get_service_info( | |
| 410 | 504 | self, | |
| 411 | 505 | type_: str, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments