Skip to content

Commit c690b78

Browse files
authored
docs: add an example for re-announcing on interface changes (#1803)
1 parent 4f3fa73 commit c690b78

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python
2+
3+
"""Re-announce services when the host's network interfaces change.
4+
5+
``AsyncZeroconf.async_update_interfaces()`` reconciles the sockets in use to the
6+
live interface set: it binds responders for interfaces that appeared, tears down
7+
the ones that went away, and re-announces existing registrations on the new
8+
senders. A call where nothing changed is a no-op.
9+
10+
zeroconf does not poll for interface changes itself; detection is
11+
platform-specific and is best driven from whatever signal a host already has (a
12+
netlink subscription on Linux, a framework's adapter-change event, etc.). When
13+
no such signal is available, a small periodic poller like the one below is
14+
enough: snapshot the addresses and reconcile only when they change. The piece
15+
worth copying carefully is the lifecycle, cancel the monitor task before
16+
closing the AsyncZeroconf so it does not outlive the instance.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import argparse
22+
import asyncio
23+
import contextlib
24+
import logging
25+
26+
import ifaddr
27+
28+
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
29+
30+
_LOGGER = logging.getLogger("interface_monitor")
31+
32+
33+
def address_snapshot() -> set[tuple[str, int]]:
34+
"""A snapshot of the host's current addresses; a change triggers a reconcile."""
35+
return {(str(ip.ip), ip.network_prefix) for adapter in ifaddr.get_adapters() for ip in adapter.ips}
36+
37+
38+
async def monitor_interfaces(aiozc: AsyncZeroconf, interval: float) -> None:
39+
"""Reconcile sockets whenever the host's addresses change, until cancelled."""
40+
previous = address_snapshot()
41+
while True:
42+
await asyncio.sleep(interval)
43+
current = address_snapshot()
44+
if current == previous:
45+
continue
46+
try:
47+
print("Interfaces changed, reconciling sockets...")
48+
await aiozc.async_update_interfaces()
49+
except Exception:
50+
# Log and retry on the next tick rather than letting the monitor
51+
# die; leave ``previous`` unchanged so the change is re-attempted.
52+
_LOGGER.exception("Interface reconcile failed; will retry")
53+
else:
54+
previous = current
55+
56+
57+
class AsyncRunner:
58+
def __init__(self) -> None:
59+
self.aiozc: AsyncZeroconf | None = None
60+
self.monitor: asyncio.Task | None = None
61+
62+
async def run(self, info: AsyncServiceInfo, interval: float) -> None:
63+
self.aiozc = AsyncZeroconf()
64+
await self.aiozc.async_register_service(info)
65+
self.monitor = asyncio.create_task(monitor_interfaces(self.aiozc, interval))
66+
print("Registered; monitoring interfaces. Press Ctrl-C to exit...")
67+
await asyncio.Event().wait()
68+
69+
async def close(self, info: AsyncServiceInfo) -> None:
70+
assert self.aiozc is not None
71+
# Stop the monitor before closing so it can't reconcile a closed instance.
72+
if self.monitor is not None:
73+
self.monitor.cancel()
74+
with contextlib.suppress(asyncio.CancelledError):
75+
await self.monitor
76+
# Await the goodbye broadcast so the TTL-0 records are actually sent.
77+
await (await self.aiozc.async_unregister_service(info))
78+
await self.aiozc.async_close()
79+
80+
81+
if __name__ == "__main__":
82+
logging.basicConfig(level=logging.INFO)
83+
84+
parser = argparse.ArgumentParser()
85+
parser.add_argument("--debug", action="store_true")
86+
parser.add_argument("--interval", type=float, default=30.0, help="poll seconds")
87+
args = parser.parse_args()
88+
if args.debug:
89+
logging.getLogger("zeroconf").setLevel(logging.DEBUG)
90+
91+
info = AsyncServiceInfo(
92+
"_http._tcp.local.",
93+
"Interface Monitor Demo._http._tcp.local.",
94+
port=80,
95+
properties={"path": "/"},
96+
server="interface-monitor-demo.local.",
97+
)
98+
99+
loop = asyncio.get_event_loop()
100+
runner = AsyncRunner()
101+
try:
102+
loop.run_until_complete(runner.run(info, args.interval))
103+
except KeyboardInterrupt:
104+
loop.run_until_complete(runner.close(info))

0 commit comments

Comments
 (0)