Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
DNSService,
DNSAddress,
DNSText,
NotRunningException,
ServiceStateChange,
Zeroconf,
const,
Expand Down Expand Up @@ -1090,6 +1091,15 @@ async def test_async_request_timeout():
assert (end_time - start_time) < 3000 + 1000


@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."""
Expand Down
2 changes: 2 additions & 0 deletions zeroconf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
IncomingDecodeError,
NamePartTooLongException,
NonUniqueNameException,
NotRunningException,
ServiceNameAlreadyRegistered,
)
from ._logger import QuietLogger, log # noqa # import needed for backwards compat
Expand Down Expand Up @@ -101,6 +102,7 @@
"IncomingDecodeError",
"NamePartTooLongException",
"NonUniqueNameException",
"NotRunningException",
"ServiceNameAlreadyRegistered",
]

Expand Down
29 changes: 17 additions & 12 deletions zeroconf/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -80,6 +80,7 @@
_MDNS_PORT,
_ONE_SECOND,
_REGISTER_TIME,
_STARTUP_TIMEOUT,
_TYPE_PTR,
_UNREGISTER_TIME,
)
Expand Down Expand Up @@ -118,15 +119,15 @@ def __init__(
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:
Expand All @@ -136,16 +137,11 @@ async def _async_setup(self, loop_thread_ready: Optional[threading.Event]) -> No
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
Expand Down Expand Up @@ -495,8 +491,17 @@ def _run_loop() -> None:
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]:
Expand Down
8 changes: 8 additions & 0 deletions zeroconf/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ class EventLoopBlocked(Error):
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.
"""
5 changes: 3 additions & 2 deletions zeroconf/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ async def async_update_service(self, info: ServiceInfo) -> Awaitable:
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
Expand Down
1 change: 1 addition & 0 deletions zeroconf/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down