zeroconf 0.149.3__tar.gz → 0.149.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of zeroconf might be problematic. Click here for more details.
- {zeroconf-0.149.3 → zeroconf-0.149.5}/CHANGELOG.md +62 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/PKG-INFO +1 -1
- {zeroconf-0.149.3 → zeroconf-0.149.5}/pyproject.toml +1 -1
- {zeroconf-0.149.3 → zeroconf-0.149.5}/setup.py +1 -1
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/__init__.py +1 -1
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_engine.py +29 -4
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_protocol/incoming.pxd +1 -1
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_protocol/incoming.py +10 -4
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/__init__.py +31 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/conftest.py +19 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/services/test_browser.py +34 -13
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/services/test_info.py +119 -126
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/services/test_types.py +11 -9
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_asyncio.py +45 -19
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_core.py +16 -11
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_engine.py +51 -3
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_handlers.py +21 -6
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_init.py +3 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_protocol.py +22 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_services.py +5 -3
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_updates.py +1 -1
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/utils/test_asyncio.py +1 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/COPYING +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/README.rst +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/build_ext.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/docs/Makefile +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/docs/_ext/zeroconfautodocfix.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/docs/api.rst +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/docs/conf.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/docs/index.rst +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_cache.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_cache.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_core.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_dns.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_dns.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_exceptions.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/answers.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/answers.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/query_handler.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/query_handler.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/record_manager.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_handlers/record_manager.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_history.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_history.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_listener.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_listener.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_logger.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_protocol/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_protocol/outgoing.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_protocol/outgoing.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_record_update.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_record_update.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/__init__.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/browser.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/browser.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/info.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/info.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/registry.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/registry.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_services/types.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_transport.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_updates.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_updates.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/asyncio.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/ipaddress.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/ipaddress.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/name.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/net.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/time.pxd +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/_utils/time.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/asyncio.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/const.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/src/zeroconf/py.typed +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/helpers.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/test_cache.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/test_incoming.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/test_outgoing.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/test_send.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/benchmarks/test_txt_properties.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/services/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/services/test_registry.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_cache.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_circular_imports.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_dns.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_exceptions.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_history.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_listener.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/test_logger.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/utils/__init__.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/utils/test_ipaddress.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/utils/test_name.py +0 -0
- {zeroconf-0.149.3 → zeroconf-0.149.5}/tests/utils/test_net.py +0 -0
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.149.5 (2026-05-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Bound DNS compression-pointer chain depth in DNSIncoming
|
|
10
|
+
([#1719](https://github.com/python-zeroconf/python-zeroconf/pull/1719),
|
|
11
|
+
[`f9e2359`](https://github.com/python-zeroconf/python-zeroconf/commit/f9e23592137f30fdf7ef710dba065da31c79b1cf))
|
|
12
|
+
|
|
13
|
+
### Testing
|
|
14
|
+
|
|
15
|
+
- Cap helper get_service_info timeout in suppression test (~3.0s → ~1.85s)
|
|
16
|
+
([#1708](https://github.com/python-zeroconf/python-zeroconf/pull/1708),
|
|
17
|
+
[`ee3c7d7`](https://github.com/python-zeroconf/python-zeroconf/commit/ee3c7d74ff45327a3a6d520b86a691e21e2bc219))
|
|
18
|
+
|
|
19
|
+
- Drop ZeroconfServiceTypes.find() timeouts from 500ms to 200ms on loopback
|
|
20
|
+
([#1710](https://github.com/python-zeroconf/python-zeroconf/pull/1710),
|
|
21
|
+
[`64d143d`](https://github.com/python-zeroconf/python-zeroconf/commit/64d143d2ee7874ee1d9cef0dd2799c008b4aa791))
|
|
22
|
+
|
|
23
|
+
- Eliminate test_get_info_single race by injecting from the send mock
|
|
24
|
+
([#1716](https://github.com/python-zeroconf/python-zeroconf/pull/1716),
|
|
25
|
+
[`963d3d7`](https://github.com/python-zeroconf/python-zeroconf/commit/963d3d70e1cde056967eba0d8747ddcd247ae707))
|
|
26
|
+
|
|
27
|
+
- Fix race in test_register_and_lookup_type_by_uppercase_name
|
|
28
|
+
([#1712](https://github.com/python-zeroconf/python-zeroconf/pull/1712),
|
|
29
|
+
[`91aa21d`](https://github.com/python-zeroconf/python-zeroconf/commit/91aa21d52a0873f5fc12d43675b1b521dfe20519))
|
|
30
|
+
|
|
31
|
+
- Speed up service-info request tests with quick_request_timing fixture
|
|
32
|
+
([#1709](https://github.com/python-zeroconf/python-zeroconf/pull/1709),
|
|
33
|
+
[`4bae30a`](https://github.com/python-zeroconf/python-zeroconf/commit/4bae30a2ed0910ee7c4f1d0f92f2c400a7b10f31))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## v0.149.4 (2026-05-17)
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
- **core**: Release sockets when close runs before engine setup completes
|
|
41
|
+
([#1706](https://github.com/python-zeroconf/python-zeroconf/pull/1706),
|
|
42
|
+
[`0deb56b`](https://github.com/python-zeroconf/python-zeroconf/commit/0deb56b78fe6cd701a43ce34dccd4b69d6dd6d36))
|
|
43
|
+
|
|
44
|
+
### Testing
|
|
45
|
+
|
|
46
|
+
- Drop pending multicast responses before TOCTOU assertion
|
|
47
|
+
([#1701](https://github.com/python-zeroconf/python-zeroconf/pull/1701),
|
|
48
|
+
[`d2058d9`](https://github.com/python-zeroconf/python-zeroconf/commit/d2058d95882c70fb2eb786d373a630314e656c15))
|
|
49
|
+
|
|
50
|
+
- Speed up slow loopback tests (closes #1697)
|
|
51
|
+
([#1699](https://github.com/python-zeroconf/python-zeroconf/pull/1699),
|
|
52
|
+
[`dd341a3`](https://github.com/python-zeroconf/python-zeroconf/commit/dd341a378e904cbf9b9a09e5c2ac8ff7c9944cd0))
|
|
53
|
+
|
|
54
|
+
- Speed up slow loopback tests (closes #1700)
|
|
55
|
+
([#1703](https://github.com/python-zeroconf/python-zeroconf/pull/1703),
|
|
56
|
+
[`d03ea36`](https://github.com/python-zeroconf/python-zeroconf/commit/d03ea364dea5866e0301ff11f6b78714ecf991e8))
|
|
57
|
+
|
|
58
|
+
- Speed up test_async_wait_unblocks_on_update
|
|
59
|
+
([#1702](https://github.com/python-zeroconf/python-zeroconf/pull/1702),
|
|
60
|
+
[`653c385`](https://github.com/python-zeroconf/python-zeroconf/commit/653c38559c468672cf907d808d432dec0fb06968))
|
|
61
|
+
|
|
62
|
+
- Widen scheduling buffer in flaky get_info suppression test
|
|
63
|
+
([#1698](https://github.com/python-zeroconf/python-zeroconf/pull/1698),
|
|
64
|
+
[`9b4db62`](https://github.com/python-zeroconf/python-zeroconf/commit/9b4db625e121c2d590ed8fe2f4d187d5e3bb9a73))
|
|
65
|
+
|
|
66
|
+
|
|
5
67
|
## v0.149.3 (2026-05-17)
|
|
6
68
|
|
|
7
69
|
### Bug Fixes
|
|
@@ -19,7 +19,7 @@ install_requires = \
|
|
|
19
19
|
|
|
20
20
|
setup_kwargs = {
|
|
21
21
|
'name': 'zeroconf',
|
|
22
|
-
'version': '0.149.
|
|
22
|
+
'version': '0.149.5',
|
|
23
23
|
'description': 'A pure python implementation of multicast DNS service discovery',
|
|
24
24
|
'long_description': 'python-zeroconf\n===============\n\n.. image:: https://github.com/python-zeroconf/python-zeroconf/workflows/CI/badge.svg\n :target: https://github.com/python-zeroconf/python-zeroconf?query=workflow%3ACI+branch%3Amaster\n\n.. image:: https://img.shields.io/pypi/v/zeroconf.svg\n :target: https://pypi.python.org/pypi/zeroconf\n\n.. image:: https://codecov.io/gh/python-zeroconf/python-zeroconf/branch/master/graph/badge.svg\n :target: https://codecov.io/gh/python-zeroconf/python-zeroconf\n\n.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\n :target: https://codspeed.io/python-zeroconf/python-zeroconf\n :alt: Codspeed.io status for python-zeroconf\n\n.. image:: https://readthedocs.org/projects/python-zeroconf/badge/?version=latest\n :target: https://python-zeroconf.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n\n`Documentation <https://python-zeroconf.readthedocs.io/en/latest/>`_.\n\nThis is fork of pyzeroconf, Multicast DNS Service Discovery for Python,\noriginally by Paul Scott-Murphy (https://github.com/paulsm/pyzeroconf),\nmodified by William McBrine (https://github.com/wmcbrine/pyzeroconf).\n\nThe original William McBrine\'s fork note::\n\n This fork is used in all of my TiVo-related projects: HME for Python\n (and therefore HME/VLC), Network Remote, Remote Proxy, and pyTivo.\n Before this, I was tracking the changes for zeroconf.py in three\n separate repos. I figured I should have an authoritative source.\n\n Although I make changes based on my experience with TiVos, I expect that\n they\'re generally applicable. This version also includes patches found\n on the now-defunct (?) Launchpad repo of pyzeroconf, and elsewhere\n around the net -- not always well-documented, sorry.\n\nCompatible with:\n\n* Bonjour\n* Avahi\n\nCompared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf:\n\n* isn\'t tied to Bonjour or Avahi\n* doesn\'t use D-Bus\n* doesn\'t force you to use particular event loop or Twisted (asyncio is used under the hood but not required)\n* is pip-installable\n* has PyPI distribution\n* has an optional cython extension for performance (pure python is supported as well)\n\nPython compatibility\n--------------------\n\n* CPython 3.10+\n* PyPy 3.10+\n\nVersioning\n----------\n\nThis project uses semantic versioning.\n\nStatus\n------\n\nThis project is actively maintained.\n\nTraffic Reduction\n-----------------\n\nBefore version 0.32, most traffic reduction techniques described in https://datatracker.ietf.org/doc/html/rfc6762#section-7\nwhere not implemented which could lead to excessive network traffic. It is highly recommended that version 0.32 or later\nis used if this is a concern.\n\nIPv6 support\n------------\n\nIPv6 support is relatively new and currently limited, specifically:\n\n* `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` on non-POSIX\n systems.\n* Dual-stack IPv6 sockets are used, which may not be supported everywhere (some\n BSD variants do not have them).\n* Listening on localhost (`::1`) does not work. Help with understanding why is\n appreciated.\n\nHow to get python-zeroconf?\n===========================\n\n* PyPI page https://pypi.org/project/zeroconf/\n* GitHub project https://github.com/python-zeroconf/python-zeroconf\n\nThe easiest way to install python-zeroconf is using pip::\n\n pip install zeroconf\n\n\n\nHow do I use it?\n================\n\nHere\'s an example of browsing for a service:\n\n.. code-block:: python\n\n from zeroconf import ServiceBrowser, ServiceListener, Zeroconf\n\n\n class MyListener(ServiceListener):\n\n def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} updated")\n\n def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} removed")\n\n def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n info = zc.get_service_info(type_, name)\n print(f"Service {name} added, service info: {info}")\n\n\n zeroconf = Zeroconf()\n listener = MyListener()\n browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)\n try:\n input("Press enter to exit...\\n\\n")\n finally:\n zeroconf.close()\n\n.. note::\n\n Discovery and service registration use *all* available network interfaces by default.\n If you want to customize that you need to specify ``interfaces`` argument when\n constructing ``Zeroconf`` object (see the code for details).\n\nIf you don\'t know the name of the service you need to browse for, try:\n\n.. code-block:: python\n\n from zeroconf import ZeroconfServiceTypes\n print(\'\\n\'.join(ZeroconfServiceTypes.find()))\n\nSee examples directory for more.\n\nChangelog\n=========\n\n`Changelog <CHANGELOG.md>`_\n\nLicense\n=======\n\nLGPL, see COPYING file for details.\n',
|
|
25
25
|
'author': 'Paul Scott-Murphy',
|
|
@@ -88,7 +88,7 @@ from ._utils.time import ( # noqa # import needed for backwards compat
|
|
|
88
88
|
|
|
89
89
|
__author__ = "Paul Scott-Murphy, William McBrine"
|
|
90
90
|
__maintainer__ = "Jakub Stasiak <jakub@stasiak.at>"
|
|
91
|
-
__version__ = "0.149.
|
|
91
|
+
__version__ = "0.149.5"
|
|
92
92
|
__license__ = "LGPL"
|
|
93
93
|
|
|
94
94
|
|
|
@@ -114,10 +114,17 @@ class AsyncEngine:
|
|
|
114
114
|
lambda: AsyncListener(self.zc), # type: ignore[arg-type, return-value]
|
|
115
115
|
sock=s,
|
|
116
116
|
)
|
|
117
|
+
# Register the wrapped transport before releasing the engine's
|
|
118
|
+
# handle so a concurrent shutdown always sees ``s`` in exactly
|
|
119
|
+
# one place; do not add an ``await`` between these two steps.
|
|
117
120
|
self.protocols.append(cast(AsyncListener, protocol))
|
|
118
121
|
self.readers.append(make_wrapped_transport(cast(asyncio.DatagramTransport, transport)))
|
|
119
122
|
if s in sender_sockets:
|
|
120
123
|
self.senders.append(make_wrapped_transport(cast(asyncio.DatagramTransport, transport)))
|
|
124
|
+
if s is self._listen_socket:
|
|
125
|
+
self._listen_socket = None
|
|
126
|
+
if s in self._respond_sockets:
|
|
127
|
+
self._respond_sockets.remove(s)
|
|
121
128
|
|
|
122
129
|
def _async_cache_cleanup(self) -> None:
|
|
123
130
|
"""Periodic cache cleanup."""
|
|
@@ -139,19 +146,37 @@ class AsyncEngine:
|
|
|
139
146
|
async def _async_close(self) -> None:
|
|
140
147
|
"""Cancel and wait for the cleanup task to finish."""
|
|
141
148
|
assert self._setup_task is not None
|
|
142
|
-
|
|
149
|
+
# Swallow CancelledError only if the setup task itself was
|
|
150
|
+
# cancelled (close-before-start); outer-task cancellation must
|
|
151
|
+
# propagate.
|
|
152
|
+
try:
|
|
153
|
+
await self._setup_task
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
if not self._setup_task.cancelled():
|
|
156
|
+
raise
|
|
143
157
|
self._async_shutdown()
|
|
144
158
|
await asyncio.sleep(0) # flush out any call soons
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
if self._cleanup_timer is not None:
|
|
160
|
+
self._cleanup_timer.cancel()
|
|
147
161
|
|
|
148
162
|
def _async_shutdown(self) -> None:
|
|
149
|
-
"""Shutdown transports and sockets."""
|
|
163
|
+
"""Shutdown transports and sockets; safe to call repeatedly."""
|
|
150
164
|
assert self.running_future is not None
|
|
151
165
|
assert self.loop is not None
|
|
152
166
|
self.running_future = self.loop.create_future()
|
|
167
|
+
# Cancel pending setup so it can't wrap fresh transports after
|
|
168
|
+
# shutdown has started.
|
|
169
|
+
if self._setup_task is not None and not self._setup_task.done():
|
|
170
|
+
self._setup_task.cancel()
|
|
153
171
|
for wrapped_transport in itertools.chain(self.senders, self.readers):
|
|
154
172
|
wrapped_transport.transport.close()
|
|
173
|
+
# Anything still here was never adopted by a transport.
|
|
174
|
+
if self._listen_socket is not None:
|
|
175
|
+
self._listen_socket.close()
|
|
176
|
+
self._listen_socket = None
|
|
177
|
+
for s in self._respond_sockets:
|
|
178
|
+
s.close()
|
|
179
|
+
self._respond_sockets = []
|
|
155
180
|
|
|
156
181
|
def close(self) -> None:
|
|
157
182
|
"""Close from sync context.
|
|
@@ -83,7 +83,7 @@ cdef class DNSIncoming:
|
|
|
83
83
|
link_py_int=object,
|
|
84
84
|
linked_labels=cython.list
|
|
85
85
|
)
|
|
86
|
-
cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers)
|
|
86
|
+
cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers, unsigned int depth)
|
|
87
87
|
|
|
88
88
|
@cython.locals(offset="unsigned int")
|
|
89
89
|
cdef void _read_header(self)
|
|
@@ -60,7 +60,7 @@ DNS_COMPRESSION_POINTER_LEN = 2
|
|
|
60
60
|
MAX_DNS_LABELS = 128
|
|
61
61
|
MAX_NAME_LENGTH = 253
|
|
62
62
|
|
|
63
|
-
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError)
|
|
63
|
+
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError, RecursionError)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
_seen_logs: dict[str, int | tuple] = {}
|
|
@@ -409,7 +409,7 @@ class DNSIncoming:
|
|
|
409
409
|
labels: list[str] = []
|
|
410
410
|
seen_pointers: set[int] = set()
|
|
411
411
|
original_offset = self.offset
|
|
412
|
-
self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers)
|
|
412
|
+
self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers, 0)
|
|
413
413
|
self._name_cache[original_offset] = labels
|
|
414
414
|
name = ".".join(labels) + "."
|
|
415
415
|
if len(name) > MAX_NAME_LENGTH:
|
|
@@ -418,8 +418,14 @@ class DNSIncoming:
|
|
|
418
418
|
)
|
|
419
419
|
return name
|
|
420
420
|
|
|
421
|
-
def _decode_labels_at_offset(
|
|
421
|
+
def _decode_labels_at_offset(
|
|
422
|
+
self, off: _int, labels: list[str], seen_pointers: set[int], depth: _int
|
|
423
|
+
) -> int:
|
|
422
424
|
# This is a tight loop that is called frequently, small optimizations can make a difference.
|
|
425
|
+
if depth > MAX_DNS_LABELS:
|
|
426
|
+
raise IncomingDecodeError(
|
|
427
|
+
f"DNS compression pointer chain exceeds {MAX_DNS_LABELS} at {off} from {self.source}"
|
|
428
|
+
)
|
|
423
429
|
view = self.view
|
|
424
430
|
while off < self._data_len:
|
|
425
431
|
length = view[off]
|
|
@@ -457,7 +463,7 @@ class DNSIncoming:
|
|
|
457
463
|
if not linked_labels:
|
|
458
464
|
linked_labels = []
|
|
459
465
|
seen_pointers.add(link_py_int)
|
|
460
|
-
self._decode_labels_at_offset(link, linked_labels, seen_pointers)
|
|
466
|
+
self._decode_labels_at_offset(link, linked_labels, seen_pointers, depth + 1)
|
|
461
467
|
self._name_cache[link_py_int] = linked_labels
|
|
462
468
|
labels.extend(linked_labels)
|
|
463
469
|
if len(labels) > MAX_DNS_LABELS:
|
|
@@ -35,6 +35,20 @@ from zeroconf._history import QuestionHistory
|
|
|
35
35
|
|
|
36
36
|
_MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution
|
|
37
37
|
|
|
38
|
+
# get_service_info / async_request timeout for tests using the
|
|
39
|
+
# `quick_request_timing` fixture. The fixture cuts the initial-query
|
|
40
|
+
# delay to ~15ms (10ms _LISTENER_TIME + 1-5ms jitter), so 50ms is
|
|
41
|
+
# ample headroom for tests that only need to observe the first one
|
|
42
|
+
# or two queries.
|
|
43
|
+
QUICK_REQUEST_TIMEOUT_MS = 50
|
|
44
|
+
|
|
45
|
+
# Timeout for ZeroconfServiceTypes.find() / AsyncZeroconfServiceTypes.async_find()
|
|
46
|
+
# in loopback integration tests. `find()` is just `time.sleep(timeout)` —
|
|
47
|
+
# it doesn't short-circuit on the first matching response — so the
|
|
48
|
+
# timeout becomes a lower bound on the test runtime. On loopback the
|
|
49
|
+
# registrar's response lands within a few ms; 200ms is ~50x headroom.
|
|
50
|
+
LOOPBACK_FIND_TIMEOUT = 0.2
|
|
51
|
+
|
|
38
52
|
|
|
39
53
|
class QuestionHistoryWithoutSuppression(QuestionHistory):
|
|
40
54
|
def suppresses(self, question: DNSQuestion, now: float, known_answers: set[DNSRecord]) -> bool:
|
|
@@ -91,6 +105,23 @@ def _clear_cache(zc: Zeroconf) -> None:
|
|
|
91
105
|
zc.question_history.clear()
|
|
92
106
|
|
|
93
107
|
|
|
108
|
+
def _backdate_cache(zc: Zeroconf, ms: int = 1100) -> None:
|
|
109
|
+
"""Backdate every cached record's `created` time by `ms` milliseconds.
|
|
110
|
+
|
|
111
|
+
rfc6762#section-10.2 keys off "received more than one second ago", so
|
|
112
|
+
backdating is equivalent to sleeping `ms` in real time without the
|
|
113
|
+
wall-clock wait.
|
|
114
|
+
|
|
115
|
+
Iterate `store.values()`, not the dict directly — when a record is
|
|
116
|
+
re-added with an equal hash, the key stays the original object while
|
|
117
|
+
the value is replaced with the latest; mutating the key would update
|
|
118
|
+
stale objects no one reads.
|
|
119
|
+
"""
|
|
120
|
+
for store in zc.cache.cache.values():
|
|
121
|
+
for record in store.values():
|
|
122
|
+
record.created -= ms
|
|
123
|
+
|
|
124
|
+
|
|
94
125
|
def time_changed_millis(millis: float | None = None) -> None:
|
|
95
126
|
"""Call all scheduled events for a time."""
|
|
96
127
|
loop = asyncio.get_running_loop()
|
|
@@ -10,6 +10,7 @@ import pytest
|
|
|
10
10
|
|
|
11
11
|
from zeroconf import _core, const
|
|
12
12
|
from zeroconf._handlers import query_handler
|
|
13
|
+
from zeroconf._services import info as service_info
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@pytest.fixture(autouse=True)
|
|
@@ -59,3 +60,21 @@ def quick_timing() -> Generator[None]:
|
|
|
59
60
|
patch.object(_core, "_UNREGISTER_TIME", 10),
|
|
60
61
|
):
|
|
61
62
|
yield
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def quick_request_timing() -> Generator[None]:
|
|
67
|
+
"""Shorten the initial-query delay used by AsyncServiceInfo.async_request.
|
|
68
|
+
|
|
69
|
+
The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762
|
|
70
|
+
§5.2) help spread queries from multiple clients on real networks.
|
|
71
|
+
On loopback they're pure overhead — get_service_info-style tests
|
|
72
|
+
wait ~250ms before the first query even fires. Opt in by adding
|
|
73
|
+
`quick_request_timing` to a test's argument list, then drop the
|
|
74
|
+
test's own timeouts (which had to accommodate that delay).
|
|
75
|
+
"""
|
|
76
|
+
with (
|
|
77
|
+
patch.object(service_info, "_LISTENER_TIME", 10),
|
|
78
|
+
patch.object(service_info, "_AVOID_SYNC_DELAY_RANDOM_INTERVAL", (1, 5)),
|
|
79
|
+
):
|
|
80
|
+
yield
|
|
@@ -331,7 +331,9 @@ class TestServiceBrowser(unittest.TestCase):
|
|
|
331
331
|
service_updated_event.clear()
|
|
332
332
|
service_text = b"path=/~matt2/"
|
|
333
333
|
_inject_response(zeroconf, mock_record_update_incoming_msg(r.ServiceStateChange.Updated))
|
|
334
|
-
|
|
334
|
+
# Negative assertion: a duplicate update must NOT fire the listener. The wait
|
|
335
|
+
# always times out, so keep the budget short rather than reusing wait_time.
|
|
336
|
+
service_updated_event.wait(0.3)
|
|
335
337
|
assert service_added_count == 1
|
|
336
338
|
assert service_updated_count == 2
|
|
337
339
|
assert service_removed_count == 0
|
|
@@ -554,7 +556,7 @@ def test_first_query_delay():
|
|
|
554
556
|
|
|
555
557
|
|
|
556
558
|
@pytest.mark.asyncio
|
|
557
|
-
async def test_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
559
|
+
async def test_asking_default_is_asking_qm_questions_after_the_first_qu(quick_timing: None) -> None:
|
|
558
560
|
"""Verify the service browser's first questions are QU and refresh queries are QM."""
|
|
559
561
|
service_added = asyncio.Event()
|
|
560
562
|
service_removed = asyncio.Event()
|
|
@@ -656,7 +658,7 @@ async def test_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
|
656
658
|
|
|
657
659
|
|
|
658
660
|
@pytest.mark.asyncio
|
|
659
|
-
async def test_ttl_refresh_cancelled_rescue_query():
|
|
661
|
+
async def test_ttl_refresh_cancelled_rescue_query(quick_timing: None) -> None:
|
|
660
662
|
"""Verify seeing a name again cancels the rescue query."""
|
|
661
663
|
service_added = asyncio.Event()
|
|
662
664
|
service_removed = asyncio.Event()
|
|
@@ -844,7 +846,7 @@ async def test_asking_qu_questions():
|
|
|
844
846
|
await aiozc.async_close()
|
|
845
847
|
|
|
846
848
|
|
|
847
|
-
def test_legacy_record_update_listener():
|
|
849
|
+
def test_legacy_record_update_listener(quick_timing: None) -> None:
|
|
848
850
|
"""Test a RecordUpdateListener that does not implement update_records."""
|
|
849
851
|
|
|
850
852
|
# instantiate a zeroconf instance
|
|
@@ -1031,7 +1033,7 @@ def test_service_browser_listeners_update_service():
|
|
|
1031
1033
|
|
|
1032
1034
|
|
|
1033
1035
|
def test_service_browser_listeners_no_update_service():
|
|
1034
|
-
"""
|
|
1036
|
+
"""A listener that ignores update events records only add/remove callbacks."""
|
|
1035
1037
|
|
|
1036
1038
|
# instantiate a zeroconf instance
|
|
1037
1039
|
zc = Zeroconf(interfaces=["127.0.0.1"])
|
|
@@ -1049,6 +1051,9 @@ def test_service_browser_listeners_no_update_service():
|
|
|
1049
1051
|
if name == registration_name:
|
|
1050
1052
|
callbacks.append(("remove", type_, name))
|
|
1051
1053
|
|
|
1054
|
+
def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def]
|
|
1055
|
+
pass
|
|
1056
|
+
|
|
1052
1057
|
listener = MyServiceListener()
|
|
1053
1058
|
|
|
1054
1059
|
browser = r.ServiceBrowser(zc, type_, None, listener)
|
|
@@ -1497,10 +1502,15 @@ def test_service_browser_expire_callbacks():
|
|
|
1497
1502
|
# Force the ttl to be 1 second
|
|
1498
1503
|
now = current_time_millis()
|
|
1499
1504
|
for cache_record in list(zc.cache.cache.values()):
|
|
1500
|
-
for record in cache_record:
|
|
1505
|
+
for record in cache_record.values():
|
|
1501
1506
|
zc.cache._async_set_created_ttl(record, now, 1)
|
|
1502
1507
|
|
|
1503
|
-
|
|
1508
|
+
# Wait for the add callback to fire from the original inject_response.
|
|
1509
|
+
for _ in range(30):
|
|
1510
|
+
time.sleep(0.01)
|
|
1511
|
+
if len(callbacks) == 1:
|
|
1512
|
+
break
|
|
1513
|
+
|
|
1504
1514
|
info.port = 400
|
|
1505
1515
|
info._dns_service_cache = None # we are mutating the record so clear the cache
|
|
1506
1516
|
|
|
@@ -1509,8 +1519,8 @@ def test_service_browser_expire_callbacks():
|
|
|
1509
1519
|
mock_incoming_msg([info.dns_service()]),
|
|
1510
1520
|
)
|
|
1511
1521
|
|
|
1512
|
-
for _ in range(
|
|
1513
|
-
time.sleep(0.
|
|
1522
|
+
for _ in range(30):
|
|
1523
|
+
time.sleep(0.01)
|
|
1514
1524
|
if len(callbacks) == 2:
|
|
1515
1525
|
break
|
|
1516
1526
|
|
|
@@ -1519,8 +1529,19 @@ def test_service_browser_expire_callbacks():
|
|
|
1519
1529
|
("update", type_, registration_name),
|
|
1520
1530
|
]
|
|
1521
1531
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1532
|
+
# Re-add every cached record with `created` in the past so the
|
|
1533
|
+
# next reaper tick (0.01s) expires them and fires the remove
|
|
1534
|
+
# callback, instead of waiting the full TTL in real time.
|
|
1535
|
+
# Going through `_async_set_created_ttl` updates the expiration
|
|
1536
|
+
# heap; mutating `record.created` directly would leave the heap
|
|
1537
|
+
# entry pointing at the original `when` so the reaper never wakes.
|
|
1538
|
+
past = current_time_millis() - 2000
|
|
1539
|
+
for cache_record in list(zc.cache.cache.values()):
|
|
1540
|
+
for record in list(cache_record.values()):
|
|
1541
|
+
zc.cache._async_set_created_ttl(record, past, 1)
|
|
1542
|
+
|
|
1543
|
+
for _ in range(30):
|
|
1544
|
+
time.sleep(0.01)
|
|
1524
1545
|
if len(callbacks) == 3:
|
|
1525
1546
|
break
|
|
1526
1547
|
|
|
@@ -1565,7 +1586,7 @@ def test_scheduled_ptr_query_dunder_methods():
|
|
|
1565
1586
|
|
|
1566
1587
|
|
|
1567
1588
|
@pytest.mark.asyncio
|
|
1568
|
-
async def test_close_zeroconf_without_browser_before_start_up_queries():
|
|
1589
|
+
async def test_close_zeroconf_without_browser_before_start_up_queries(quick_timing: None) -> None:
|
|
1569
1590
|
"""Test that we stop sending startup queries if zeroconf is closed out from under the browser."""
|
|
1570
1591
|
service_added = asyncio.Event()
|
|
1571
1592
|
type_ = "_http._tcp.local."
|
|
@@ -1632,7 +1653,7 @@ async def test_close_zeroconf_without_browser_before_start_up_queries():
|
|
|
1632
1653
|
|
|
1633
1654
|
|
|
1634
1655
|
@pytest.mark.asyncio
|
|
1635
|
-
async def test_close_zeroconf_without_browser_after_start_up_queries():
|
|
1656
|
+
async def test_close_zeroconf_without_browser_after_start_up_queries(quick_timing: None) -> None:
|
|
1636
1657
|
"""Test that we stop sending rescue queries if zeroconf is closed out from under the browser."""
|
|
1637
1658
|
service_added = asyncio.Event()
|
|
1638
1659
|
|