| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff 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() | ||
| Original file line number | Diff line number | Diff 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() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -150,7 +150,8 @@ def async_send_with_transport( | |||
| 150 | 150 | ||
| 151 | 151 | ||
| 152 | 152 | class Zeroconf(QuietLogger): | |
| 153 | - | ||
| 153 | + """High level mDNS service discovery: register services, browse for | ||
| 154 | + them, and resolve their details on the local network.""" | ||
| 154 | 155 | ||
| 155 | 156 | def __init__( | |
| 156 | 157 | self, | |
@@ -160,7 +161,7 @@ def __init__( | |||
| 160 | 161 | apple_p2p: bool = False, | |
| 161 | 162 | use_asyncio: bool | None = None, | |
| 162 | 163 | ) -> None: | |
| 163 | - multicast communications, listening and reaping threads. | ||
| 164 | + """Open the multicast sockets and start serving on the chosen interfaces. | ||
| 164 | 165 | ||
| 165 | 166 | :param interfaces: :class:`InterfaceChoice` or a list of IP addresses | |
| 166 | 167 | (IPv4 and IPv6) and interface indexes (IPv6 only). | |
@@ -317,17 +318,18 @@ def get_service_info( | |||
| 317 | 318 | return None | |
| 318 | 319 | ||
| 319 | 320 | 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.""" | ||
| 322 | 322 | self.remove_service_listener(listener) | |
| 323 | 323 | self.browsers[listener] = ServiceBrowser(self, type_, listener) | |
| 324 | 324 | ||
| 325 | 325 | def remove_service_listener(self, listener: ServiceListener) -> None: | |
| 326 | + """Stop the browser behind the given listener.""" | ||
| 326 | 327 | if listener in self.browsers: | |
| 327 | 328 | self.browsers[listener].cancel() | |
| 328 | 329 | del self.browsers[listener] | |
| 329 | 330 | ||
| 330 | 331 | def remove_all_service_listeners(self) -> None: | |
| 332 | + """Stop the browsers behind every registered listener.""" | ||
| 331 | 333 | for listener in list(self.browsers): | |
| 332 | 334 | self.remove_service_listener(listener) | |
| 333 | 335 | ||
@@ -647,6 +649,7 @@ async def async_check_service( | |||
| 647 | 649 | cooperating_responders: bool = False, | |
| 648 | 650 | strict: bool = True, | |
| 649 | 651 | ) -> None: | |
| 652 | + """Probe the network for name uniqueness, renaming first when allowed.""" | ||
| 650 | 653 | instance_name = instance_name_from_service_info(info, strict=strict) | |
| 651 | 654 | if cooperating_responders: | |
| 652 | 655 | return | |
@@ -685,11 +688,7 @@ def add_listener( | |||
| 685 | 688 | listener: RecordUpdateListener, | |
| 686 | 689 | question: DNSQuestion | list[DNSQuestion] | None, | |
| 687 | 690 | ) -> 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.""" | ||
| 693 | 692 | assert self.loop is not None | |
| 694 | 693 | self.loop.call_soon_threadsafe(self.record_manager.async_add_listener, listener, question) | |
| 695 | 694 | ||
@@ -706,11 +705,7 @@ def async_add_listener( | |||
| 706 | 705 | listener: RecordUpdateListener, | |
| 707 | 706 | question: DNSQuestion | list[DNSQuestion] | None, | |
| 708 | 707 | ) -> 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.""" | ||
| 714 | 709 | self.record_manager.async_add_listener(listener, question) | |
| 715 | 710 | ||
| 716 | 711 | def async_remove_listener(self, listener: RecordUpdateListener) -> None: | |
@@ -797,10 +792,7 @@ def _shutdown_threads(self) -> None: | |||
| 797 | 792 | self.loop.close() | |
| 798 | 793 | ||
| 799 | 794 | 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.""" | ||
| 804 | 796 | assert self.loop is not None | |
| 805 | 797 | if self.loop.is_running(): | |
| 806 | 798 | if self.loop == get_running_loop(): | |
@@ -814,15 +806,7 @@ def close(self) -> None: | |||
| 814 | 806 | self._shutdown_threads() | |
| 815 | 807 | ||
| 816 | 808 | 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.""" | ||
| 826 | 810 | self._close() | |
| 827 | 811 | await self.engine._async_close() # pylint: disable=protected-access | |
| 828 | 812 | self._shutdown_threads() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,6 +26,7 @@ | |||
| 26 | 26 | import socket | |
| 27 | 27 | from typing import TYPE_CHECKING, Any, cast | |
| 28 | 28 | ||
| 29 | + from ._exceptions import AbstractMethodException | ||
| 29 | 30 | from ._utils.net import _is_v6_address | |
| 30 | 31 | from ._utils.time import current_time_millis | |
| 31 | 32 | from .const import _CLASS_MASK, _CLASS_UNIQUE, _CLASSES, _TYPE_ANY, _TYPES | |
@@ -156,7 +157,7 @@ def __repr__(self) -> str: | |||
| 156 | 157 | ) | |
| 157 | 158 | ||
| 158 | 159 | ||
| 159 | - class DNSRecord(DNSEntry): | ||
| 160 | + class DNSRecord(DNSEntry): # noqa: PLW1641 | ||
| 160 | 161 | """A DNS record - like a DNS entry, but has a TTL""" | |
| 161 | 162 | ||
| 162 | 163 | __slots__ = ("created", "ttl") | |
@@ -177,6 +178,9 @@ def _fast_init_record(self, name: str, type_: _int, class_: _int, ttl: _int, cre | |||
| 177 | 178 | self.ttl = ttl | |
| 178 | 179 | self.created = created | |
| 179 | 180 | ||
| 181 | + def __eq__(self, other: Any) -> bool: | ||
| 182 | + raise AbstractMethodException(f"{type(self).__name__} is missing __eq__") | ||
| 183 | + | ||
| 180 | 184 | def __lt__(self, other: DNSRecord) -> bool: | |
| 181 | 185 | return self.ttl < other.ttl | |
| 182 | 186 | ||
@@ -187,6 +191,16 @@ def suppressed_by(self, msg: DNSIncoming) -> bool: | |||
| 187 | 191 | return True | |
| 188 | 192 | return False | |
| 189 | 193 | ||
| 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 | + | ||
| 190 | 204 | # TODO: Switch to just int here | |
| 191 | 205 | def get_remaining_ttl(self, now: _float) -> int | float: | |
| 192 | 206 | """Returns the remaining TTL in seconds.""" | |
@@ -211,6 +225,9 @@ def _set_created_ttl(self, created: _float, ttl: _int) -> None: | |||
| 211 | 225 | self.created = created | |
| 212 | 226 | self.ttl = ttl | |
| 213 | 227 | ||
| 228 | + def write(self, out: DNSOutgoing) -> None: | ||
| 229 | + raise AbstractMethodException(f"{type(self).__name__} is missing write") | ||
| 230 | + | ||
| 214 | 231 | def to_string(self, other: bytes | str) -> str: | |
| 215 | 232 | arg = f"{self.ttl}/{int(self.get_remaining_ttl(current_time_millis()))},{cast(Any, other)}" | |
| 216 | 233 | return DNSEntry.entry_to_string(self, "record", arg) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,32 +53,21 @@ def __init__(self, zeroconf: Zeroconf) -> None: | |||
| 53 | 53 | self.listeners: set[RecordUpdateListener] = set() | |
| 54 | 54 | ||
| 55 | 55 | 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.""" | ||
| 62 | 57 | for listener in self.listeners.copy(): | |
| 63 | 58 | listener.async_update_records(self.zc, now, records) | |
| 64 | 59 | ||
| 65 | 60 | 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.""" | ||
| 72 | 62 | for listener in self.listeners.copy(): | |
| 73 | 63 | listener.async_update_records_complete() | |
| 74 | 64 | if notify: | |
| 75 | 65 | self.zc.async_notify_all() | |
| 76 | 66 | ||
| 77 | 67 | 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. | ||
| 79 | 69 | ||
| 80 | - This function must be run in the event loop as it is not | ||
| 81 | - threadsafe. | ||
| 70 | + Runs in the event loop; not threadsafe. | ||
| 82 | 71 | """ | |
| 83 | 72 | updates: list[RecordUpdate] = [] | |
| 84 | 73 | address_adds: list[DNSRecord] = [] | |
@@ -168,10 +157,8 @@ def async_add_listener( | |||
| 168 | 157 | listener: RecordUpdateListener, | |
| 169 | 158 | question: DNSQuestion | list[DNSQuestion] | None, | |
| 170 | 159 | ) -> 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. | ||
| 175 | 162 | """ | |
| 176 | 163 | if not isinstance(listener, RecordUpdateListener): | |
| 177 | 164 | log.error( # type: ignore[unreachable] | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -55,8 +55,8 @@ | |||
| 55 | 55 | ||
| 56 | 56 | ||
| 57 | 57 | 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.""" | ||
| 60 | 60 | ||
| 61 | 61 | __slots__ = ( | |
| 62 | 62 | "_deferred", | |
@@ -236,6 +236,7 @@ def handle_query_or_defer( | |||
| 236 | 236 | transport: _WrappedTransport, | |
| 237 | 237 | v6_flow_scope: tuple[()] | tuple[int, int], | |
| 238 | 238 | ) -> None: | |
| 239 | + """Answer immediately, or hold a truncated query for reassembly.""" | ||
| 239 | 240 | if not msg.truncated: | |
| 240 | 241 | self._respond_query(msg, addr, port, transport, v6_flow_scope) | |
| 241 | 242 | return | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -311,6 +311,7 @@ def _read_string(self, length: _int) -> bytes: | |||
| 311 | 311 | return info | |
| 312 | 312 | ||
| 313 | 313 | def _read_others(self) -> None: | |
| 314 | + """Read the answer, authority and additional sections.""" | ||
| 314 | 315 | self._did_read_others = True | |
| 315 | 316 | buf = self._buf | |
| 316 | 317 | answers = self._answers | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -65,10 +65,10 @@ cdef class DNSOutgoing: | |||
| 65 | 65 | cdef cython.bint _write_question(self, DNSQuestion question) | |
| 66 | 66 | ||
| 67 | 67 | @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, | ||
| 72 | 72 | ) | |
| 73 | 73 | cdef cython.bint _write_record(self, DNSRecord record, double now) | |
| 74 | 74 | ||
@@ -110,7 +110,7 @@ cdef class DNSOutgoing: | |||
| 110 | 110 | ||
| 111 | 111 | cpdef void write_character_string(self, cython.bytes value) | |
| 112 | 112 | ||
| 113 | - @cython.locals(utfstr=bytes) | ||
| 113 | + @cython.locals(encoded=bytes, byte_length=cython.uint) | ||
| 114 | 114 | cdef void _write_utf(self, cython.str value) | |
| 115 | 115 | ||
| 116 | 116 | @cython.locals( | |
@@ -132,7 +132,6 @@ cdef class DNSOutgoing: | |||
| 132 | 132 | ||
| 133 | 133 | cpdef void add_answer(self, DNSIncoming inp, DNSRecord record) | |
| 134 | 134 | ||
| 135 | - @cython.locals(now_double=double) | ||
| 136 | 135 | cpdef void add_answer_at_time(self, DNSRecord record, double now) | |
| 137 | 136 | ||
| 138 | 137 | cpdef void add_authorative_answer(self, DNSPointer record) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments