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
8 changes: 4 additions & 4 deletions tests/services/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def test_backoff():
zeroconf_browser = Zeroconf(interfaces=['127.0.0.1'])

# we are going to patch the zeroconf send to check query transmission
old_send = zeroconf_browser.send
old_send = zeroconf_browser.async_send

time_offset = 0.0
start_time = time.time() * 1000
Expand All @@ -366,7 +366,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT):
# patch the zeroconf send
# patch the zeroconf current_time_millis
# patch the backoff limit to prevent test running forever
with unittest.mock.patch.object(zeroconf_browser, "send", send), unittest.mock.patch.object(
with unittest.mock.patch.object(zeroconf_browser, "async_send", send), unittest.mock.patch.object(
_services_browser, "current_time_millis", current_time_millis
), unittest.mock.patch.object(_services_browser, "_BROWSER_BACKOFF_LIMIT", 10):
# dummy service callback
Expand Down Expand Up @@ -432,7 +432,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
zeroconf_browser = Zeroconf(interfaces=['127.0.0.1'])

# we are going to patch the zeroconf send to check packet sizes
old_send = zeroconf_browser.send
old_send = zeroconf_browser.async_send

time_offset = 0.0

Expand All @@ -459,7 +459,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT):
# patch the zeroconf send
# patch the zeroconf current_time_millis
# patch the backoff limit to ensure we always get one query every 1/4 of the DNS TTL
with unittest.mock.patch.object(zeroconf_browser, "send", send), unittest.mock.patch.object(
with unittest.mock.patch.object(zeroconf_browser, "async_send", send), unittest.mock.patch.object(
_services_browser, "current_time_millis", current_time_millis
), unittest.mock.patch.object(_services_browser, "_BROWSER_BACKOFF_LIMIT", int(expected_ttl / 4)):
service_added = Event()
Expand Down
1 change: 1 addition & 0 deletions tests/test_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
calls.append(("update", type, name))

listener = MyListener()

aiozc.zeroconf.add_service_listener(type_, listener)

desc = {'path': '/~paulsm/'}
Expand Down
4 changes: 2 additions & 2 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_lots_of_names(self):
self.verify_name_change(zc, type_, name, server_count)

# we are going to patch the zeroconf send to check packet sizes
old_send = zc.send
old_send = zc.async_send

longest_packet_len = 0
longest_packet = None # type: Optional[r.DNSOutgoing]
Expand All @@ -97,7 +97,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT):
old_send(out, addr=addr, port=port)

# patch the zeroconf send
with unittest.mock.patch.object(zc, "send", send):
with unittest.mock.patch.object(zc, "async_send", send):

# dummy service callback
def on_service_state_change(zeroconf, service_type, state_change, name):
Expand Down
14 changes: 5 additions & 9 deletions zeroconf/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,6 @@ def __init__(
self.query_handler = QueryHandler(self.registry, self.cache)
self.record_manager = RecordManager(self)

self.condition = threading.Condition()
self.async_condition: Optional[asyncio.Condition] = None
self.loop: Optional[asyncio.AbstractEventLoop] = None
self._loop_thread: Optional[threading.Thread] = None
Expand Down Expand Up @@ -338,10 +337,9 @@ def listeners(self) -> List[RecordUpdateListener]:
return self.record_manager.listeners

def wait(self, timeout: float) -> None:
"""Calling thread waits for a given number of milliseconds or
until notified."""
with self.condition:
self.condition.wait(millis_to_seconds(timeout))
"""Calling task waits for a given number of milliseconds or until notified."""
assert self.loop is not None
asyncio.run_coroutine_threadsafe(self.async_wait(timeout), self.loop).result()

async def async_wait(self, timeout: float) -> None:
"""Calling task waits for a given number of milliseconds or until notified."""
Expand All @@ -361,10 +359,8 @@ def async_notify_all(self) -> None:
async def _async_notify_all(self) -> None:
"""Notify all async listeners."""
assert self.async_condition is not None
with self.condition:
self.condition.notify_all()
async with self.async_condition:
self.async_condition.notify_all()
async with self.async_condition:
self.async_condition.notify_all()

def get_service_info(self, type_: str, name: str, timeout: int = 3000) -> Optional[ServiceInfo]:
"""Returns network's service information for a particular
Expand Down
2 changes: 1 addition & 1 deletion zeroconf/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def _async_update_matching_records(
return
listener.async_update_records(self.zc, now, records)
listener.async_update_records_complete()
self.zc.notify_all()
self.zc.async_notify_all()

def remove_listener(self, listener: RecordUpdateListener) -> None:
"""Removes a listener."""
Expand Down
91 changes: 70 additions & 21 deletions zeroconf/_services/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
USA
"""

import asyncio
import contextlib
import queue
import threading
import warnings
from collections import OrderedDict
Expand All @@ -35,6 +38,7 @@
Signal,
SignalRegistrationInterface,
)
from .._utils.aio import get_best_available_queue, get_running_loop, wait_condition_or_timeout
from .._utils.name import service_type_name
from .._utils.time import current_time_millis, millis_to_seconds
from ..const import (
Expand Down Expand Up @@ -180,6 +184,7 @@ def __init__(
for check_type_ in self.types:
# Will generate BadTypeInNameException on a bad name
service_type_name(check_type_, strict=False)
self._browser_task: Optional[asyncio.Task] = None
self.zc = zc
self.addr = addr
self.port = port
Expand All @@ -190,7 +195,7 @@ def __init__(
self._pending_handlers: OrderedDict[Tuple[str, str], ServiceStateChange] = OrderedDict()
self._handlers_to_call: OrderedDict[Tuple[str, str], ServiceStateChange] = OrderedDict()
self._service_state_changed = Signal()

self.queue: Optional[queue.Queue] = None
self.done = False

if hasattr(handlers, 'add_service'):
Expand Down Expand Up @@ -341,6 +346,47 @@ def _seconds_to_wait(self) -> Optional[float]:

return millis_to_seconds(next_time - now)

async def async_browser_task(self) -> None:
"""Run the browser task."""
await self.zc.async_wait_for_start()
assert self.zc.async_condition is not None
while True:
timeout = self._seconds_to_wait()
if timeout:
async with self.zc.async_condition:
# We must check again while holding the condition
# in case the other thread has added to _handlers_to_call
# between when we checked above when we were not
# holding the condition
if not self._handlers_to_call:
await wait_condition_or_timeout(self.zc.async_condition, timeout)

outs = self.generate_ready_queries()
for out in outs:
self.zc.async_send(out, addr=self.addr, port=self.port)

if not self._handlers_to_call:
continue

(name_type, state_change) = self._handlers_to_call.popitem(False)
if self.queue:
self.queue.put((name_type, state_change))
continue

self._service_state_changed.fire(
zeroconf=self.zc,
service_type=name_type[1],
name=name_type[0],
state_change=state_change,
)

async def _async_cancel_browser(self) -> None:
"""Cancel the browser."""
assert self._browser_task is not None
self._browser_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._browser_task


class ServiceBrowser(_ServiceBrowserBase, threading.Thread):
"""Used to browse for a service of a specific type.
Expand All @@ -361,42 +407,45 @@ def __init__(
) -> None:
threading.Thread.__init__(self)
super().__init__(zc, type_, handlers=handlers, listener=listener, addr=addr, port=port, delay=delay)
self.queue = get_best_available_queue()
self.daemon = True
self.start()
self.name = "zeroconf-ServiceBrowser-%s-%s" % (
'-'.join([type_[:-7] for type_ in self.types]),
getattr(self, 'native_id', self.ident),
)
assert self.zc.loop is not None
if get_running_loop() == self.zc.loop:
self._browser_task = cast(asyncio.Task, asyncio.ensure_future(self.async_browser_task()))
return
self._browser_task = cast(
asyncio.Task,
asyncio.run_coroutine_threadsafe(self._async_browser_task(), self.zc.loop).result(),
)

async def _async_browser_task(self) -> asyncio.Task:
return cast(asyncio.Task, asyncio.ensure_future(self.async_browser_task()))

def cancel(self) -> None:
"""Cancel the browser."""
assert self.zc.loop is not None
assert self.queue is not None
self.queue.put(None)
if get_running_loop() == self.zc.loop:
asyncio.ensure_future(self._async_cancel_browser())
else:
asyncio.run_coroutine_threadsafe(self._async_cancel_browser(), self.zc.loop).result()
super().cancel()
self.join()

def run(self) -> None:
"""Run the browser thread."""
assert self.queue is not None
while True:
timeout = self._seconds_to_wait()
if timeout:
with self.zc.condition:
# We must check again while holding the condition
# in case the other thread has added to _handlers_to_call
# between when we checked above when we were not
# holding the condition
if not self._handlers_to_call:
self.zc.condition.wait(timeout)

if self.zc.done or self.done:
event = self.queue.get()
if event is None:
return

outs = self.generate_ready_queries()
for out in outs:
self.zc.send(out, addr=self.addr, port=self.port)

if not self._handlers_to_call:
continue

(name_type, state_change) = self._handlers_to_call.popitem(False)
name_type, state_change = event
self._service_state_changed.fire(
zeroconf=self.zc,
service_type=name_type[1],
Expand Down
8 changes: 8 additions & 0 deletions zeroconf/_utils/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@

import asyncio
import contextlib
import queue
from typing import Optional, Set, cast


def get_best_available_queue() -> queue.Queue:
"""Create the best available queue type."""
if hasattr(queue, "SimpleQueue"):
return queue.SimpleQueue() # type: ignore # pylint: disable=all
return queue.Queue()


# Switch to asyncio.wait_for once https://bugs.python.org/issue39032 is fixed
async def wait_condition_or_timeout(condition: asyncio.Condition, timeout: float) -> None:
"""Wait for a condition or timeout."""
Expand Down
42 changes: 4 additions & 38 deletions zeroconf/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@
import asyncio
import contextlib
from types import TracebackType # noqa # used in type hints
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union, cast

from ._core import Zeroconf
from ._exceptions import NonUniqueNameException
from ._services.browser import _ServiceBrowserBase
from ._services.info import ServiceInfo, instance_name_from_service_info
from ._services.types import ZeroconfServiceTypes
from ._utils.aio import wait_condition_or_timeout
from ._utils.net import IPVersion, InterfaceChoice, InterfacesType
from ._utils.time import millis_to_seconds
from .const import (
Expand Down Expand Up @@ -83,46 +82,13 @@ def __init__(
port: int = _MDNS_PORT,
delay: int = _BROWSER_TIME,
) -> None:
self.aiozc = aiozc
super().__init__(aiozc.zeroconf, type_, handlers, listener, addr, port, delay) # type: ignore
self._browser_task = asyncio.ensure_future(self.async_run())
self._browser_task = cast(asyncio.Task, asyncio.ensure_future(self.async_browser_task()))

async def async_cancel(self) -> None:
"""Cancel the browser."""
self.cancel()
self._browser_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._browser_task

async def async_run(self) -> None:
"""Run the browser task."""
await self.aiozc.zeroconf.async_wait_for_start()
assert self.aiozc.zeroconf.async_condition is not None
while True:
timeout = self._seconds_to_wait()
if timeout:
async with self.aiozc.zeroconf.async_condition:
# We must check again while holding the condition
# in case the other thread has added to _handlers_to_call
# between when we checked above when we were not
# holding the condition
if not self._handlers_to_call:
await wait_condition_or_timeout(self.aiozc.zeroconf.async_condition, timeout)

outs = self.generate_ready_queries()
for out in outs:
self.aiozc.zeroconf.async_send(out, addr=self.addr, port=self.port)

if not self._handlers_to_call:
continue

(name_type, state_change) = self._handlers_to_call.popitem(False)
self._service_state_changed.fire(
zeroconf=self.aiozc,
service_type=name_type[1],
name=name_type[0],
state_change=state_change,
)
await self._async_cancel_browser()
super().cancel()


class AsyncZeroconfServiceTypes(ZeroconfServiceTypes):
Expand Down