Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-zeroconf for openSUSE:Factory checked in at 2021-11-29 17:28:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old) and /work/SRC/openSUSE:Factory/.python-zeroconf.new.31177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-zeroconf" Mon Nov 29 17:28:36 2021 rev:28 rq:934518 version:0.37.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes 2021-11-07 00:19:30.208553109 +0100 +++ /work/SRC/openSUSE:Factory/.python-zeroconf.new.31177/python-zeroconf.changes 2021-12-02 02:25:12.420617506 +0100 @@ -1,0 +2,13 @@ +Mon Nov 29 11:04:03 UTC 2021 - Dirk M??ller <dmuel...@suse.com> + +- update to 0.37.0: + * Adding a listener that does not inherit from RecordUpdateListener now logs + an error + * The NotRunningException exception is now thrown when Zeroconf is not + running (#1033) @bdraco + * Before this change the consumer would get a timeout or an EventLoopBlocked + exception when calling ServiceInfo.*request when the instance had already been shutdown + or had failed to startup. + * The EventLoopBlocked exception is now thrown when a coroutine times out + +------------------------------------------------------------------- Old: ---- python-zeroconf-0.36.12.obscpio New: ---- python-zeroconf-0.37.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-zeroconf.spec ++++++ --- /var/tmp/diff_new_pack.8cd50M/_old 2021-12-02 02:25:12.880615901 +0100 +++ /var/tmp/diff_new_pack.8cd50M/_new 2021-12-02 02:25:12.884615887 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-zeroconf -Version: 0.36.12 +Version: 0.37.0 Release: 0 Summary: Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi compatible) License: LGPL-2.0-only ++++++ _service ++++++ --- /var/tmp/diff_new_pack.8cd50M/_old 2021-12-02 02:25:12.920615761 +0100 +++ /var/tmp/diff_new_pack.8cd50M/_new 2021-12-02 02:25:12.920615761 +0100 @@ -2,8 +2,8 @@ <service name="obs_scm" mode="disabled"> <param name="url">https://github.com/jstasiak/python-zeroconf</param> <param name="scm">git</param> - <param name="revision">0.36.12</param> - <param name="version">0.36.12</param> + <param name="revision">0.37.0</param> + <param name="version">0.37.0</param> </service> <service name="set_version" mode="disabled"/> <service mode="buildtime" name="tar" /> ++++++ python-zeroconf-0.36.12.obscpio -> python-zeroconf-0.37.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/README.rst new/python-zeroconf-0.37.0/README.rst --- old/python-zeroconf-0.36.12/README.rst 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/README.rst 2021-11-18 21:30:53.000000000 +0100 @@ -138,10 +138,36 @@ Changelog ========= +0.37.0 +====== + +Technically backwards incompatible: + +* Adding a listener that does not inherit from RecordUpdateListener now logs an error (#1034) @bdraco +* The NotRunningException exception is now thrown when Zeroconf is not running (#1033) @bdraco + + Before this change the consumer would get a timeout or an EventLoopBlocked + exception when calling `ServiceInfo.*request` when the instance had already been shutdown + or had failed to startup. + +* The EventLoopBlocked exception is now thrown when a coroutine times out (#1032) @bdraco + + Previously `concurrent.futures.TimeoutError` would have been raised + instead. This is never expected to happen during normal operation. + +0.36.13 +======= + +* Unavailable interfaces are now skipped during socket bind (#1028) @bdraco +* Downgraded incoming corrupt packet logging to debug (#1029) @bdraco + + Warning about network traffic we have no control over is confusing + to users as they think there is something wrong with zeroconf + 0.36.12 ======= -* Prevent service lookups from deadlocking if time abruptly moves backwards (#1006) @bdraco +* Prevented service lookups from deadlocking if time abruptly moves backwards (#1006) @bdraco The typical reason time moves backwards is via an ntp update diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/setup.cfg new/python-zeroconf-0.37.0/setup.cfg --- old/python-zeroconf-0.36.12/setup.cfg 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/setup.cfg 2021-11-18 21:30:53.000000000 +0100 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.36.12 +current_version = 0.37.0 commit = True tag = True tag_name = {new_version} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/tests/test_asyncio.py new/python-zeroconf-0.37.0/tests/test_asyncio.py --- old/python-zeroconf-0.36.12/tests/test_asyncio.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/tests/test_asyncio.py 2021-11-18 21:30:53.000000000 +0100 @@ -23,6 +23,7 @@ DNSService, DNSAddress, DNSText, + NotRunningException, ServiceStateChange, Zeroconf, const, @@ -1091,6 +1092,15 @@ @pytest.mark.asyncio +async def test_async_request_non_running_instance(): + """Test that the async_request throws when zeroconf is not running.""" + aiozc = AsyncZeroconf(interfaces=['127.0.0.1']) + await aiozc.async_close() + with pytest.raises(NotRunningException): + await aiozc.async_get_service_info("_notfound.local.", "notthere._notfound.local.") + + +@pytest.mark.asyncio async def test_legacy_unicast_response(run_isolated): """Verify legacy unicast responses include questions and correct id.""" type_ = "_mservice._tcp.local." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/tests/test_handlers.py new/python-zeroconf-0.37.0/tests/test_handlers.py --- old/python-zeroconf-0.36.12/tests/test_handlers.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/tests/test_handlers.py 2021-11-18 21:30:53.000000000 +0100 @@ -1538,3 +1538,25 @@ # But the one we have not sent yet shoudl still go out later assert info2.dns_pointer() in outgoing_queue.queue[0].answers + + +@pytest.mark.asyncio +async def test_add_listener_warns_when_not_using_record_update_listener(caplog): + """Log when a listener is added that is not using RecordUpdateListener as a base class.""" + + aiozc = AsyncZeroconf(interfaces=['127.0.0.1']) + zc: Zeroconf = aiozc.zeroconf + updated = [] + + class MyListener: + """A RecordUpdateListener that does not implement update_records.""" + + def async_update_records(self, zc: 'Zeroconf', now: float, records: List[r.RecordUpdate]) -> None: + """Update multiple records in one shot.""" + updated.extend(records) + + zc.add_listener(MyListener(), None) + await asyncio.sleep(0) # flush out any call soons + assert "listeners passed to async_add_listener must inherit from RecordUpdateListener" in caplog.text + + await aiozc.async_close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/tests/test_logger.py new/python-zeroconf-0.37.0/tests/test_logger.py --- old/python-zeroconf-0.36.12/tests/test_logger.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/tests/test_logger.py 2021-11-18 21:30:53.000000000 +0100 @@ -4,7 +4,7 @@ """Unit tests for logger.py.""" import logging -from unittest.mock import patch +from unittest.mock import call, patch from zeroconf._logger import QuietLogger, set_logger_level_if_unset @@ -25,6 +25,7 @@ def test_log_warning_once(): """Test we only log with warning level once.""" + QuietLogger._seen_logs = {} quiet_logger = QuietLogger() with patch("zeroconf._logger.log.warning") as mock_log_warning, patch( "zeroconf._logger.log.debug" @@ -45,6 +46,7 @@ def test_log_exception_warning(): """Test we only log with warning level once.""" + QuietLogger._seen_logs = {} quiet_logger = QuietLogger() with patch("zeroconf._logger.log.warning") as mock_log_warning, patch( "zeroconf._logger.log.debug" @@ -63,8 +65,24 @@ assert mock_log_debug.mock_calls +def test_llog_exception_debug(): + """Test we only log with a trace once.""" + QuietLogger._seen_logs = {} + quiet_logger = QuietLogger() + with patch("zeroconf._logger.log.debug") as mock_log_debug: + quiet_logger.log_exception_debug("the exception") + + assert mock_log_debug.mock_calls == [call('the exception', exc_info=True)] + + with patch("zeroconf._logger.log.debug") as mock_log_debug: + quiet_logger.log_exception_debug("the exception") + + assert mock_log_debug.mock_calls == [call('the exception', exc_info=False)] + + def test_log_exception_once(): """Test we only log with warning level once.""" + QuietLogger._seen_logs = {} quiet_logger = QuietLogger() exc = Exception() with patch("zeroconf._logger.log.warning") as mock_log_warning, patch( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/tests/utils/test_asyncio.py new/python-zeroconf-0.37.0/tests/utils/test_asyncio.py --- old/python-zeroconf-0.36.12/tests/utils/test_asyncio.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/tests/utils/test_asyncio.py 2021-11-18 21:30:53.000000000 +0100 @@ -13,6 +13,7 @@ import pytest +from zeroconf import EventLoopBlocked from zeroconf._core import _CLOSE_TIMEOUT from zeroconf._utils import asyncio as aioutils from zeroconf.const import _LOADED_SYSTEM_TIMEOUT @@ -112,3 +113,14 @@ assert ( aioutils._TASK_AWAIT_TIMEOUT + aioutils._GET_ALL_TASKS_TIMEOUT + aioutils._WAIT_FOR_LOOP_TASKS_TIMEOUT ) < 1 + _CLOSE_TIMEOUT + _LOADED_SYSTEM_TIMEOUT + + +async def test_run_coro_with_timeout() -> None: + """Test running a coroutine with a timeout raises EventLoopBlocked.""" + loop = asyncio.get_event_loop() + + def _run_in_loop(): + aioutils.run_coro_with_timeout(asyncio.sleep(0.3), loop, 0.1) + + with pytest.raises(EventLoopBlocked), patch.object(aioutils, "_LOADED_SYSTEM_TIMEOUT", 0.0): + await loop.run_in_executor(None, _run_in_loop) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/tests/utils/test_net.py new/python-zeroconf-0.37.0/tests/utils/test_net.py --- old/python-zeroconf-0.36.12/tests/utils/test_net.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/tests/utils/test_net.py 2021-11-18 21:30:53.000000000 +0100 @@ -3,7 +3,7 @@ """Unit tests for zeroconf._utils.net.""" -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import errno import ifaddr @@ -198,3 +198,26 @@ # No error should return True with patch("socket.socket.setsockopt"): assert netutils.add_multicast_member(sock, interface) is True + + +def test_bind_raises_skips_address(): + """Test bind failing in new_socket returns None on EADDRNOTAVAIL.""" + err = errno.EADDRNOTAVAIL + + def _mock_socket(*args, **kwargs): + sock = MagicMock() + sock.bind = MagicMock(side_effect=OSError(err, "Error: {}".format(err))) + return sock + + with patch("socket.socket", _mock_socket): + assert netutils.new_socket(("0.0.0.0", 0)) is None + + err = errno.EAGAIN + with pytest.raises(OSError), patch("socket.socket", _mock_socket): + netutils.new_socket(("0.0.0.0", 0)) + + +def test_new_respond_socket_new_socket_returns_none(): + """Test new_respond_socket returns None if new_socket returns None.""" + with patch.object(netutils, "new_socket", return_value=None): + assert netutils.new_respond_socket(("0.0.0.0", 0)) is None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/__init__.py new/python-zeroconf-0.37.0/zeroconf/__init__.py --- old/python-zeroconf-0.36.12/zeroconf/__init__.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/__init__.py 2021-11-18 21:30:53.000000000 +0100 @@ -23,7 +23,7 @@ import sys from ._cache import DNSCache # noqa # import needed for backwards compat -from ._core import Zeroconf # noqa # import needed for backwards compat +from ._core import Zeroconf from ._dns import ( # noqa # import needed for backwards compat DNSAddress, DNSEntry, @@ -36,16 +36,18 @@ DNSText, DNSQuestionType, ) -from ._logger import QuietLogger, log # noqa # import needed for backwards compat -from ._exceptions import ( # noqa # import needed for backwards compat +from ._exceptions import ( AbstractMethodException, BadTypeInNameException, Error, + EventLoopBlocked, IncomingDecodeError, NamePartTooLongException, NonUniqueNameException, + NotRunningException, ServiceNameAlreadyRegistered, ) +from ._logger import QuietLogger, log # noqa # import needed for backwards compat from ._protocol.incoming import DNSIncoming # noqa # import needed for backwards compat from ._protocol.outgoing import DNSOutgoing # noqa # import needed for backwards compat from ._services import ( # noqa # import needed for backwards compat @@ -54,16 +56,14 @@ ServiceListener, ServiceStateChange, ) -from ._services.browser import ( # noqa # import needed for backwards compat - ServiceBrowser, -) +from ._services.browser import ServiceBrowser from ._services.info import ( # noqa # import needed for backwards compat instance_name_from_service_info, ServiceInfo, ) from ._services.registry import ServiceRegistry # noqa # import needed for backwards compat from ._services.types import ZeroconfServiceTypes -from ._updates import RecordUpdate, RecordUpdateListener # noqa # import needed for backwards compat +from ._updates import RecordUpdate, RecordUpdateListener from ._utils.name import service_type_name # noqa # import needed for backwards compat from ._utils.net import ( # noqa # import needed for backwards compat add_multicast_member, @@ -79,22 +79,34 @@ __author__ = 'Paul Scott-Murphy, William McBrine' __maintainer__ = 'Jakub Stasiak <ja...@stasiak.at>' -__version__ = '0.36.12' +__version__ = '0.37.0' __license__ = 'LGPL' __all__ = [ "__version__", - "DNSQuestionType", "Zeroconf", "ServiceInfo", "ServiceBrowser", "ServiceListener", - "Error", + "DNSQuestionType", "InterfaceChoice", "ServiceStateChange", "IPVersion", "ZeroconfServiceTypes", + "RecordUpdate", + "RecordUpdateListener", + "current_time_millis", + # Exceptions + "Error", + "AbstractMethodException", + "BadTypeInNameException", + "EventLoopBlocked", + "IncomingDecodeError", + "NamePartTooLongException", + "NonUniqueNameException", + "NotRunningException", + "ServiceNameAlreadyRegistered", ] if sys.version_info <= (3, 6): # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_core.py new/python-zeroconf-0.37.0/zeroconf/_core.py --- old/python-zeroconf-0.36.12/zeroconf/_core.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_core.py 2021-11-18 21:30:53.000000000 +0100 @@ -32,7 +32,7 @@ from ._cache import DNSCache from ._dns import DNSQuestion, DNSQuestionType -from ._exceptions import NonUniqueNameException +from ._exceptions import NonUniqueNameException, NotRunningException from ._handlers import ( MulticastOutgoingQueue, QueryHandler, @@ -80,6 +80,7 @@ _MDNS_PORT, _ONE_SECOND, _REGISTER_TIME, + _STARTUP_TIMEOUT, _TYPE_PTR, _UNREGISTER_TIME, ) @@ -118,15 +119,15 @@ self.protocols: List[AsyncListener] = [] self.readers: List[asyncio.DatagramTransport] = [] self.senders: List[asyncio.DatagramTransport] = [] + self.running_event: Optional[asyncio.Event] = None self._listen_socket = listen_socket self._respond_sockets = respond_sockets self._cleanup_timer: Optional[asyncio.TimerHandle] = None - self._running_event: Optional[asyncio.Event] = None def setup(self, loop: asyncio.AbstractEventLoop, loop_thread_ready: Optional[threading.Event]) -> None: """Set up the instance.""" self.loop = loop - self._running_event = asyncio.Event() + self.running_event = asyncio.Event() self.loop.create_task(self._async_setup(loop_thread_ready)) async def _async_setup(self, loop_thread_ready: Optional[threading.Event]) -> None: @@ -136,16 +137,11 @@ millis_to_seconds(_CACHE_CLEANUP_INTERVAL), self._async_cache_cleanup ) await self._async_create_endpoints() - assert self._running_event is not None - self._running_event.set() + assert self.running_event is not None + self.running_event.set() if loop_thread_ready: loop_thread_ready.set() - async def async_wait_for_start(self) -> None: - """Wait for start up.""" - assert self._running_event is not None - await self._running_event.wait() - async def _async_create_endpoints(self) -> None: """Create endpoints to send and receive.""" assert self.loop is not None @@ -192,7 +188,12 @@ transport.close() def close(self) -> None: - """Close from sync context.""" + """Close from sync context. + + While it is not expected during normal operation, + this function may raise EventLoopBlocked if the underlying + call to `_async_close` cannot be completed. + """ assert self.loop is not None # Guard against Zeroconf.close() being called from the eventloop if get_running_loop() == self.loop: @@ -490,8 +491,17 @@ loop_thread_ready.wait() async def async_wait_for_start(self) -> None: - """Wait for start up.""" - await self.engine.async_wait_for_start() + """Wait for start up for actions that require a running Zeroconf instance. + + Throws NotRunningException if the instance is not running or could + not be started. + """ + if self.done: # If the instance was shutdown from under us, raise immediately + raise NotRunningException + assert self.engine.running_event is not None + await wait_event_or_timeout(self.engine.running_event, timeout=_STARTUP_TIMEOUT) + if not self.engine.running_event.is_set() or self.done: + raise NotRunningException @property def listeners(self) -> List[RecordUpdateListener]: @@ -554,7 +564,12 @@ service. The name of the service may be changed if needed to make it unique on the network. Additionally multiple cooperating responders can register the same service on the network for resilience - (if you want this behavior set `cooperating_responders` to `True`).""" + (if you want this behavior set `cooperating_responders` to `True`). + + While it is not expected during normal operation, + this function may raise EventLoopBlocked if the underlying + call to `register_service` cannot be completed. + """ assert self.loop is not None run_coro_with_timeout( await_awaitable( @@ -591,7 +606,12 @@ def update_service(self, info: ServiceInfo) -> None: """Registers service information to the network with a default TTL. Zeroconf will then respond to requests for information for that - service.""" + service. + + While it is not expected during normal operation, + this function may raise EventLoopBlocked if the underlying + call to `async_update_service` cannot be completed. + """ assert self.loop is not None run_coro_with_timeout( await_awaitable(self.async_update_service(info)), self.loop, _REGISTER_TIME * _REGISTER_BROADCASTS @@ -662,7 +682,12 @@ out.add_answer_at_time(dns_address, 0) def unregister_service(self, info: ServiceInfo) -> None: - """Unregister a service.""" + """Unregister a service. + + While it is not expected during normal operation, + this function may raise EventLoopBlocked if the underlying + call to `async_unregister_service` cannot be completed. + """ assert self.loop is not None run_coro_with_timeout( self.async_unregister_service(info), self.loop, _UNREGISTER_TIME * _REGISTER_BROADCASTS @@ -708,7 +733,12 @@ self.async_send(out) def unregister_all_services(self) -> None: - """Unregister all registered services.""" + """Unregister all registered services. + + While it is not expected during normal operation, + this function may raise EventLoopBlocked if the underlying + call to `async_unregister_all_services` cannot be completed. + """ assert self.loop is not None run_coro_with_timeout( self.async_unregister_all_services(), self.loop, _UNREGISTER_TIME * _REGISTER_BROADCASTS diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_exceptions.py new/python-zeroconf-0.37.0/zeroconf/_exceptions.py --- old/python-zeroconf-0.36.12/zeroconf/_exceptions.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_exceptions.py 2021-11-18 21:30:53.000000000 +0100 @@ -22,28 +22,46 @@ class Error(Exception): - pass + """Base class for all zeroconf exceptions.""" class IncomingDecodeError(Error): - pass + """Exception when there is invalid data in an incoming packet.""" class NonUniqueNameException(Error): - pass + """Exception when the name is already registered.""" class NamePartTooLongException(Error): - pass + """Exception when the name is too long.""" class AbstractMethodException(Error): - pass + """Exception when a required method is not implemented.""" class BadTypeInNameException(Error): - pass + """Exception when the type in a name is invalid.""" class ServiceNameAlreadyRegistered(Error): - pass + """Exception when a service name is already registered.""" + + +class EventLoopBlocked(Error): + """Exception when the event loop is blocked. + + This exception is never expected to be thrown + during normal operation. It should only happen + when the cpu is maxed out or there is something blocking + the event loop. + """ + + +class NotRunningException(Error): + """Exception when an action is called with a zeroconf instance that is not running. + + The instance may not be running because it was already shutdown + or startup has failed in some unexpected way. + """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_handlers.py new/python-zeroconf-0.37.0/zeroconf/_handlers.py --- old/python-zeroconf-0.36.12/zeroconf/_handlers.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_handlers.py 2021-11-18 21:30:53.000000000 +0100 @@ -492,6 +492,12 @@ This function is not threadsafe and must be called in the eventloop. """ + if not isinstance(listener, RecordUpdateListener): + log.error( + "listeners passed to async_add_listener must inherit from RecordUpdateListener;" + " In the future this will fail" + ) + self.listeners.append(listener) if question is None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_logger.py new/python-zeroconf-0.37.0/zeroconf/_logger.py --- old/python-zeroconf-0.36.12/zeroconf/_logger.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_logger.py 2021-11-18 21:30:53.000000000 +0100 @@ -52,6 +52,20 @@ logger(*(logger_data or ['Exception occurred']), exc_info=True) @classmethod + def log_exception_debug(cls, *logger_data: Any) -> None: + log_exc_info = False + exc_info = sys.exc_info() + exc_str = str(exc_info[1]) + import pprint + + pprint.pprint(cls._seen_logs) + if exc_str not in cls._seen_logs: + # log the trace only on the first time + cls._seen_logs[exc_str] = exc_info + log_exc_info = True + log.debug(*(logger_data or ['Exception occurred']), exc_info=log_exc_info) + + @classmethod def log_warning_once(cls, *args: Any) -> None: msg_str = args[0] if msg_str not in cls._seen_logs: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_protocol/incoming.py new/python-zeroconf-0.37.0/zeroconf/_protocol/incoming.py --- old/python-zeroconf-0.36.12/zeroconf/_protocol/incoming.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_protocol/incoming.py 2021-11-18 21:30:53.000000000 +0100 @@ -110,7 +110,7 @@ try: parser_call() except DECODE_EXCEPTIONS: - self.log_exception_warning( + self.log_exception_debug( 'Received invalid packet from %s at offset %d while unpacking %r', self.source, self.offset, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_services/info.py new/python-zeroconf-0.37.0/zeroconf/_services/info.py --- old/python-zeroconf-0.36.12/zeroconf/_services/info.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_services/info.py 2021-11-18 21:30:53.000000000 +0100 @@ -451,6 +451,10 @@ ) -> bool: """Returns true if the service could be discovered on the network, and updates this object with details discovered. + + While it is not expected during normal operation, + this function may raise EventLoopBlocked if the underlying + call to `async_request` cannot be completed. """ assert zc.loop is not None and zc.loop.is_running() if zc.loop == get_running_loop(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_updates.py new/python-zeroconf-0.37.0/zeroconf/_updates.py --- old/python-zeroconf-0.36.12/zeroconf/_updates.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_updates.py 2021-11-18 21:30:53.000000000 +0100 @@ -36,6 +36,12 @@ class RecordUpdateListener: + """Base call for all record listeners. + + All listeners passed to async_add_listener should use RecordUpdateListener + as a base class. In the future it will be required. + """ + def update_record( # pylint: disable=no-self-use self, zc: 'Zeroconf', now: float, record: DNSRecord ) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_utils/asyncio.py new/python-zeroconf-0.37.0/zeroconf/_utils/asyncio.py --- old/python-zeroconf-0.36.12/zeroconf/_utils/asyncio.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_utils/asyncio.py 2021-11-18 21:30:53.000000000 +0100 @@ -21,11 +21,13 @@ """ import asyncio +import concurrent.futures import contextlib import queue from typing import Any, Awaitable, Coroutine, List, Optional, Set, cast from .time import millis_to_seconds +from .._exceptions import EventLoopBlocked from ..const import _LOADED_SYSTEM_TIMEOUT # The combined timeouts should be lower than _CLOSE_TIMEOUT + _WAIT_FOR_LOOP_TASKS_TIMEOUT @@ -91,10 +93,22 @@ def run_coro_with_timeout(aw: Coroutine, loop: asyncio.AbstractEventLoop, timeout: float) -> Any: - """Run a coroutine with a timeout.""" - return asyncio.run_coroutine_threadsafe(aw, loop).result( - millis_to_seconds(timeout) + _LOADED_SYSTEM_TIMEOUT - ) + """Run a coroutine with a timeout. + + The timeout should only be used as a safeguard to prevent + the program from blocking forever. The timeout should + never be expected to be reached during normal operation. + + While not expected during normal operations, the + function raises `EventLoopBlocked` if the coroutine takes + longer to complete than the timeout. + """ + try: + return asyncio.run_coroutine_threadsafe(aw, loop).result( + millis_to_seconds(timeout) + _LOADED_SYSTEM_TIMEOUT + ) + except concurrent.futures.TimeoutError as ex: + raise EventLoopBlocked from ex def shutdown_loop(loop: asyncio.AbstractEventLoop) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_utils/net.py new/python-zeroconf-0.37.0/zeroconf/_utils/net.py --- old/python-zeroconf-0.36.12/zeroconf/_utils/net.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_utils/net.py 2021-11-18 21:30:53.000000000 +0100 @@ -218,7 +218,7 @@ port: int = _MDNS_PORT, ip_version: IPVersion = IPVersion.V4Only, apple_p2p: bool = False, -) -> socket.socket: +) -> Optional[socket.socket]: log.debug( 'Creating new socket with port %s, ip_version %s, apple_p2p %s and bind_addr %r', port, @@ -243,7 +243,17 @@ # https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h s.setsockopt(socket.SOL_SOCKET, 0x1104, 1) - s.bind((bind_addr[0], port, *bind_addr[1:])) + bind_tup = (bind_addr[0], port, *bind_addr[1:]) + try: + s.bind(bind_tup) + except OSError as ex: + if ex.errno == errno.EADDRNOTAVAIL: + log.warning( + 'Address not available when binding to %s, ' 'it is expected to happen on some systems', + bind_tup, + ) + return None + raise log.debug('Created socket %s', s) return s @@ -323,6 +333,8 @@ apple_p2p=apple_p2p, bind_addr=cast(Tuple[Tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),), ) + if not respond_socket: + return None log.debug('Configuring socket %s with multicast interface %s', respond_socket, interface) if is_v6: iface_bin = struct.pack('@I', cast(int, interface[1])) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/_utils/time.py new/python-zeroconf-0.37.0/zeroconf/_utils/time.py --- old/python-zeroconf-0.36.12/zeroconf/_utils/time.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/_utils/time.py 2021-11-18 21:30:53.000000000 +0100 @@ -25,7 +25,11 @@ def current_time_millis() -> float: - """Current system time in milliseconds""" + """Current time in milliseconds. + + The current implemention uses `time.monotonic` + but may change in the future. + """ return time.monotonic() * 1000 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/asyncio.py new/python-zeroconf-0.37.0/zeroconf/asyncio.py --- old/python-zeroconf-0.36.12/zeroconf/asyncio.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/asyncio.py 2021-11-18 21:30:53.000000000 +0100 @@ -218,8 +218,9 @@ async def async_close(self) -> None: """Ends the background threads, and prevent this instance from servicing further queries.""" - with contextlib.suppress(asyncio.TimeoutError): - await asyncio.wait_for(self.zeroconf.async_wait_for_start(), timeout=1) + if not self.zeroconf.done: + with contextlib.suppress(asyncio.TimeoutError): + await asyncio.wait_for(self.zeroconf.async_wait_for_start(), timeout=1) await self.async_remove_all_service_listeners() await self.async_unregister_all_services() await self.zeroconf._async_close() # pylint: disable=protected-access diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-zeroconf-0.36.12/zeroconf/const.py new/python-zeroconf-0.37.0/zeroconf/const.py --- old/python-zeroconf-0.36.12/zeroconf/const.py 2021-11-05 06:11:04.000000000 +0100 +++ new/python-zeroconf-0.37.0/zeroconf/const.py 2021-11-18 21:30:53.000000000 +0100 @@ -34,6 +34,7 @@ _BROWSER_BACKOFF_LIMIT = 3600 # s _CACHE_CLEANUP_INTERVAL = 10000 # ms _LOADED_SYSTEM_TIMEOUT = 10 # s +_STARTUP_TIMEOUT = 9 # s must be lower than _LOADED_SYSTEM_TIMEOUT _ONE_SECOND = 1000 # ms # If the system is loaded or the event ++++++ python-zeroconf.obsinfo ++++++ --- /var/tmp/diff_new_pack.8cd50M/_old 2021-12-02 02:25:13.104615120 +0100 +++ /var/tmp/diff_new_pack.8cd50M/_new 2021-12-02 02:25:13.104615120 +0100 @@ -1,5 +1,5 @@ name: python-zeroconf -version: 0.36.12 -mtime: 1636089064 -commit: 8b0dc48ed42d8edc78750122eb5685a50c3cdc11 +version: 0.37.0 +mtime: 1637267453 +commit: 2996e642f6b1abba1dbb8242ccca4cd4b96696f6