| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 7816278 commit a41d7b8
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -36,10 +36,10 @@ mypy: | |||
| 36 | 36 | mypy examples/*.py zeroconf/*.py | |
| 37 | 37 | ||
| 38 | 38 | test: | |
| 39 | - pytest -v zeroconf/test.py | ||
| 39 | + pytest -v zeroconf/test.py zeroconf/test_asyncio.py | ||
| 40 | 40 | ||
| 41 | 41 | test_coverage: | |
| 42 | - pytest -v --cov=zeroconf --cov-branch --cov-report html --cov-report term-missing zeroconf/test.py | ||
| 42 | + pytest -v --cov=zeroconf --cov-branch --cov-report html --cov-report term-missing zeroconf/test.py zeroconf/test_asyncio.py | ||
| 43 | 43 | ||
| 44 | 44 | autopep8: | |
| 45 | 45 | autopep8 --max-line-length=$(MAX_LINE_LENGTH) -i setup.py examples zeroconf | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,4 +10,5 @@ mypy;implementation_name=="cpython" | |||
| 10 | 10 | # 0.11.0 breaks things https://github.com/PyCQA/pep8-naming/issues/152 | |
| 11 | 11 | pep8-naming!=0.6.0,!=0.11.0 | |
| 12 | 12 | pytest | |
| 13 | + pytest-asyncio | ||
| 13 | 14 | pytest-cov | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -353,6 +353,16 @@ def service_type_name(type_: str, *, strict: bool = True) -> str: | |||
| 353 | 353 | return service_name + trailer | |
| 354 | 354 | ||
| 355 | 355 | ||
| 356 | + def instance_name_from_service_info(info: "ServiceInfo") -> str: | ||
| 357 | + """Calculate the instance name from the ServiceInfo.""" | ||
| 358 | + # This is kind of funky because of the subtype based tests | ||
| 359 | + # need to make subtypes a first class citizen | ||
| 360 | + service_name = service_type_name(info.name) | ||
| 361 | + if not info.type.endswith(service_name): | ||
| 362 | + raise BadTypeInNameException | ||
| 363 | + return info.name[: -len(service_name) - 1] | ||
| 364 | + | ||
| 365 | + | ||
| 356 | 366 | # Exceptions | |
| 357 | 367 | ||
| 358 | 368 | ||
@@ -2505,8 +2515,6 @@ def __init__( | |||
| 2505 | 2515 | for s in self._respond_sockets: | |
| 2506 | 2516 | self.engine.add_reader(self.listener, s) | |
| 2507 | 2517 | ||
| 2508 | - self.debug = None # type: Optional[DNSOutgoing] | ||
| 2509 | - | ||
| 2510 | 2518 | @property | |
| 2511 | 2519 | def done(self) -> bool: | |
| 2512 | 2520 | return self._GLOBAL_DONE | |
@@ -2580,6 +2588,7 @@ def update_service(self, info: ServiceInfo) -> None: | |||
| 2580 | 2588 | self._broadcast_service(info, _REGISTER_TIME, None) | |
| 2581 | 2589 | ||
| 2582 | 2590 | def _broadcast_service(self, info: ServiceInfo, interval: int, ttl: Optional[int]) -> None: | |
| 2591 | + """Send a broadcasts to announce a service at intervals.""" | ||
| 2583 | 2592 | now = current_time_millis() | |
| 2584 | 2593 | next_time = now | |
| 2585 | 2594 | i = 0 | |
@@ -2589,12 +2598,23 @@ def _broadcast_service(self, info: ServiceInfo, interval: int, ttl: Optional[int | |||
| 2589 | 2598 | now = current_time_millis() | |
| 2590 | 2599 | continue | |
| 2591 | 2600 | ||
| 2592 | - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||
| 2593 | - self._add_broadcast_answer(out, info, ttl) | ||
| 2594 | - self.send(out) | ||
| 2601 | + self.send_service_broadcast(info, ttl) | ||
| 2595 | 2602 | i += 1 | |
| 2596 | 2603 | next_time += interval | |
| 2597 | 2604 | ||
| 2605 | + def send_service_broadcast(self, info: ServiceInfo, ttl: Optional[int]) -> None: | ||
| 2606 | + """Send a broadcast to announce a service.""" | ||
| 2607 | + out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) | ||
| 2608 | + self._add_broadcast_answer(out, info, ttl) | ||
| 2609 | + self.send(out) | ||
| 2610 | + | ||
| 2611 | + def send_service_query(self, info: ServiceInfo) -> None: | ||
| 2612 | + """Send a query to lookup a service.""" | ||
| 2613 | + out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) | ||
| 2614 | + out.add_question(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) | ||
| 2615 | + out.add_authorative_answer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, info.other_ttl, info.name)) | ||
| 2616 | + self.send(out) | ||
| 2617 | + | ||
| 2598 | 2618 | def _add_broadcast_answer(self, out: DNSOutgoing, info: ServiceInfo, override_ttl: Optional[int]) -> None: | |
| 2599 | 2619 | """Add answers to broadcast a service.""" | |
| 2600 | 2620 | other_ttl = info.other_ttl if override_ttl is None else override_ttl | |
@@ -2653,43 +2673,31 @@ def check_service( | |||
| 2653 | 2673 | ) -> None: | |
| 2654 | 2674 | """Checks the network for a unique service name, modifying the | |
| 2655 | 2675 | ServiceInfo passed in if it is not unique.""" | |
| 2656 | - | ||
| 2657 | - # This is kind of funky because of the subtype based tests | ||
| 2658 | - # need to make subtypes a first class citizen | ||
| 2659 | - service_name = service_type_name(info.name) | ||
| 2660 | - if not info.type.endswith(service_name): | ||
| 2661 | - raise BadTypeInNameException | ||
| 2662 | - | ||
| 2663 | - instance_name = info.name[: -len(service_name) - 1] | ||
| 2676 | + instance_name = instance_name_from_service_info(info) | ||
| 2677 | + if cooperating_responders: | ||
| 2678 | + return | ||
| 2664 | 2679 | next_instance_number = 2 | |
| 2665 | - | ||
| 2666 | - now = current_time_millis() | ||
| 2667 | - next_time = now | ||
| 2680 | + next_time = now = current_time_millis() | ||
| 2668 | 2681 | i = 0 | |
| 2669 | 2682 | while i < 3: | |
| 2670 | - if not cooperating_responders: | ||
| 2671 | - # check for a name conflict | ||
| 2672 | - while self.cache.current_entry_with_name_and_alias(info.type, info.name): | ||
| 2673 | - if not allow_name_change: | ||
| 2674 | - raise NonUniqueNameException | ||
| 2675 | - | ||
| 2676 | - # change the name and look for a conflict | ||
| 2677 | - info.name = '%s-%s.%s' % (instance_name, next_instance_number, info.type) | ||
| 2678 | - next_instance_number += 1 | ||
| 2679 | - service_type_name(info.name) | ||
| 2680 | - next_time = now | ||
| 2681 | - i = 0 | ||
| 2683 | + # check for a name conflict | ||
| 2684 | + while self.cache.current_entry_with_name_and_alias(info.type, info.name): | ||
| 2685 | + if not allow_name_change: | ||
| 2686 | + raise NonUniqueNameException | ||
| 2687 | + | ||
| 2688 | + # change the name and look for a conflict | ||
| 2689 | + info.name = '%s-%s.%s' % (instance_name, next_instance_number, info.type) | ||
| 2690 | + next_instance_number += 1 | ||
| 2691 | + service_type_name(info.name) | ||
| 2692 | + next_time = now | ||
| 2693 | + i = 0 | ||
| 2682 | 2694 | ||
| 2683 | 2695 | if now < next_time: | |
| 2684 | 2696 | self.wait(next_time - now) | |
| 2685 | 2697 | now = current_time_millis() | |
| 2686 | 2698 | continue | |
| 2687 | 2699 | ||
| 2688 | - out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA) | ||
| 2689 | - self.debug = out | ||
| 2690 | - out.add_question(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN)) | ||
| 2691 | - out.add_authorative_answer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, info.other_ttl, info.name)) | ||
| 2692 | - self.send(out) | ||
| 2700 | + self.send_service_query(info) | ||
| 2693 | 2701 | i += 1 | |
| 2694 | 2702 | next_time += _CHECK_TIME | |
| 2695 | 2703 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,137 @@ | |||
| 1 | + """ Multicast DNS Service Discovery for Python, v0.14-wmcbrine | ||
| 2 | + Copyright 2003 Paul Scott-Murphy, 2014 William McBrine | ||
| 3 | + | ||
| 4 | + This module provides a framework for the use of DNS Service Discovery | ||
| 5 | + using IP multicast. | ||
| 6 | + | ||
| 7 | + This library is free software; you can redistribute it and/or | ||
| 8 | + modify it under the terms of the GNU Lesser General Public | ||
| 9 | + License as published by the Free Software Foundation; either | ||
| 10 | + version 2.1 of the License, or (at your option) any later version. | ||
| 11 | + | ||
| 12 | + This library is distributed in the hope that it will be useful, | ||
| 13 | + but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| 15 | + Lesser General Public License for more details. | ||
| 16 | + | ||
| 17 | + You should have received a copy of the GNU Lesser General Public | ||
| 18 | + License along with this library; if not, write to the Free Software | ||
| 19 | + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 | ||
| 20 | + USA | ||
| 21 | + """ | ||
| 22 | + import asyncio | ||
| 23 | + from typing import Optional | ||
| 24 | + | ||
| 25 | + from . import ( | ||
| 26 | + IPVersion, | ||
| 27 | + InterfaceChoice, | ||
| 28 | + InterfacesType, | ||
| 29 | + NonUniqueNameException, | ||
| 30 | + ServiceInfo, | ||
| 31 | + Zeroconf, | ||
| 32 | + _CHECK_TIME, | ||
| 33 | + _REGISTER_TIME, | ||
| 34 | + _UNREGISTER_TIME, | ||
| 35 | + instance_name_from_service_info, | ||
| 36 | + ) | ||
| 37 | + | ||
| 38 | + | ||
| 39 | + class AsyncZeroconf: | ||
| 40 | + """Implementation of Zeroconf Multicast DNS Service Discovery | ||
| 41 | + | ||
| 42 | + Supports registration, unregistration, queries and browsing. | ||
| 43 | + | ||
| 44 | + The async version is currently a wrapper around the sync version | ||
| 45 | + with I/O being done in the executor for backwards compatibility. | ||
| 46 | + """ | ||
| 47 | + | ||
| 48 | + def __init__( | ||
| 49 | + self, | ||
| 50 | + interfaces: InterfacesType = InterfaceChoice.All, | ||
| 51 | + unicast: bool = False, | ||
| 52 | + ip_version: Optional[IPVersion] = None, | ||
| 53 | + apple_p2p: bool = False, | ||
| 54 | + ) -> None: | ||
| 55 | + """Creates an instance of the Zeroconf class, establishing | ||
| 56 | + multicast communications, listening and reaping threads. | ||
| 57 | + | ||
| 58 | + :param interfaces: :class:`InterfaceChoice` or a list of IP addresses | ||
| 59 | + (IPv4 and IPv6) and interface indexes (IPv6 only). | ||
| 60 | + | ||
| 61 | + IPv6 notes for non-POSIX systems: | ||
| 62 | + * `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` | ||
| 63 | + on Python versions before 3.8. | ||
| 64 | + | ||
| 65 | + Also listening on loopback (``::1``) doesn't work, use a real address. | ||
| 66 | + :param ip_version: IP versions to support. If `choice` is a list, the default is detected | ||
| 67 | + from it. Otherwise defaults to V4 only for backward compatibility. | ||
| 68 | + :param apple_p2p: use AWDL interface (only macOS) | ||
| 69 | + """ | ||
| 70 | + self.zeroconf = Zeroconf( | ||
| 71 | + interfaces=interfaces, | ||
| 72 | + unicast=unicast, | ||
| 73 | + ip_version=ip_version, | ||
| 74 | + apple_p2p=apple_p2p, | ||
| 75 | + ) | ||
| 76 | + self.loop = asyncio.get_event_loop() | ||
| 77 | + | ||
| 78 | + async def _async_broadcast_service(self, info: ServiceInfo, interval: int, ttl: Optional[int]) -> None: | ||
| 79 | + """Send a broadcasts to announce a service at intervals.""" | ||
| 80 | + for i in range(3): | ||
| 81 | + if i != 0: | ||
| 82 | + await asyncio.sleep(interval / 1000) | ||
| 83 | + await self.loop.run_in_executor(None, self.zeroconf.send_service_broadcast, info, ttl) | ||
| 84 | + | ||
| 85 | + async def async_register_service( | ||
| 86 | + self, | ||
| 87 | + info: ServiceInfo, | ||
| 88 | + cooperating_responders: bool = False, | ||
| 89 | + ) -> None: | ||
| 90 | + """Registers service information to the network with a default TTL. | ||
| 91 | + Zeroconf will then respond to requests for information for that | ||
| 92 | + service. The name of the service may be changed if needed to make | ||
| 93 | + it unique on the network. Additionally multiple cooperating responders | ||
| 94 | + can register the same service on the network for resilience | ||
| 95 | + (if you want this behavior set `cooperating_responders` to `True`). | ||
| 96 | + | ||
| 97 | + The service will be broadcast in a task. | ||
| 98 | + """ | ||
| 99 | + await self.async_check_service(info, cooperating_responders) | ||
| 100 | + await self.loop.run_in_executor(None, self.zeroconf.registry.add, info) | ||
| 101 | + asyncio.ensure_future(self._async_broadcast_service(info, _REGISTER_TIME, None)) | ||
| 102 | + | ||
| 103 | + async def async_check_service(self, info: ServiceInfo, cooperating_responders: bool = False) -> None: | ||
| 104 | + """Checks the network for a unique service name.""" | ||
| 105 | + instance_name_from_service_info(info) | ||
| 106 | + if cooperating_responders: | ||
| 107 | + return | ||
| 108 | + for i in range(3): | ||
| 109 | + # check for a name conflict | ||
| 110 | + if self.zeroconf.cache.current_entry_with_name_and_alias(info.type, info.name): | ||
| 111 | + raise NonUniqueNameException | ||
| 112 | + if i != 0: | ||
| 113 | + await asyncio.sleep(_CHECK_TIME / 1000) | ||
| 114 | + await self.loop.run_in_executor(None, self.zeroconf.send_service_query, info) | ||
| 115 | + | ||
| 116 | + async def async_unregister_service(self, info: ServiceInfo) -> None: | ||
| 117 | + """Unregister a service. | ||
| 118 | + | ||
| 119 | + The service will be broadcast in a task. | ||
| 120 | + """ | ||
| 121 | + await self.loop.run_in_executor(None, self.zeroconf.registry.remove, info) | ||
| 122 | + asyncio.ensure_future(self._async_broadcast_service(info, _UNREGISTER_TIME, 0)) | ||
| 123 | + | ||
| 124 | + async def async_update_service(self, info: ServiceInfo) -> None: | ||
| 125 | + """Registers service information to the network with a default TTL. | ||
| 126 | + Zeroconf will then respond to requests for information for that | ||
| 127 | + service. | ||
| 128 | + | ||
| 129 | + The service will be broadcast in a task. | ||
| 130 | + """ | ||
| 131 | + await self.loop.run_in_executor(None, self.zeroconf.registry.update, info) | ||
| 132 | + asyncio.ensure_future(self._async_broadcast_service(info, _REGISTER_TIME, None)) | ||
| 133 | + | ||
| 134 | + async def async_close(self) -> None: | ||
| 135 | + """Ends the background threads, and prevent this instance from | ||
| 136 | + servicing further queries.""" | ||
| 137 | + await self.loop.run_in_executor(None, self.zeroconf.close) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments