uh oh im bundling the deps
This commit is contained in:
parent
ae28da8d60
commit
ecca301ceb
584 changed files with 119933 additions and 24 deletions
BIN
resources/lib/deps/zeroconf/_services/__init__.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_services/__init__.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
11
resources/lib/deps/zeroconf/_services/__init__.pxd
Normal file
11
resources/lib/deps/zeroconf/_services/__init__.pxd
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
import cython
|
||||
|
||||
|
||||
cdef class Signal:
|
||||
|
||||
cdef list _handlers
|
||||
|
||||
cdef class SignalRegistrationInterface:
|
||||
|
||||
cdef list _handlers
|
75
resources/lib/deps/zeroconf/_services/__init__.py
Normal file
75
resources/lib/deps/zeroconf/_services/__init__.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
import enum
|
||||
from typing import TYPE_CHECKING, Any, Callable, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Zeroconf
|
||||
|
||||
|
||||
@enum.unique
|
||||
class ServiceStateChange(enum.Enum):
|
||||
Added = 1
|
||||
Removed = 2
|
||||
Updated = 3
|
||||
|
||||
|
||||
class ServiceListener:
|
||||
def add_service(self, zc: 'Zeroconf', type_: str, name: str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove_service(self, zc: 'Zeroconf', type_: str, name: str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def update_service(self, zc: 'Zeroconf', type_: str, name: str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Signal:
|
||||
__slots__ = ('_handlers',)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._handlers: List[Callable[..., None]] = []
|
||||
|
||||
def fire(self, **kwargs: Any) -> None:
|
||||
for h in self._handlers[:]:
|
||||
h(**kwargs)
|
||||
|
||||
@property
|
||||
def registration_interface(self) -> 'SignalRegistrationInterface':
|
||||
return SignalRegistrationInterface(self._handlers)
|
||||
|
||||
|
||||
class SignalRegistrationInterface:
|
||||
__slots__ = ('_handlers',)
|
||||
|
||||
def __init__(self, handlers: List[Callable[..., None]]) -> None:
|
||||
self._handlers = handlers
|
||||
|
||||
def register_handler(self, handler: Callable[..., None]) -> 'SignalRegistrationInterface':
|
||||
self._handlers.append(handler)
|
||||
return self
|
||||
|
||||
def unregister_handler(self, handler: Callable[..., None]) -> 'SignalRegistrationInterface':
|
||||
self._handlers.remove(handler)
|
||||
return self
|
BIN
resources/lib/deps/zeroconf/_services/browser.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_services/browser.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
117
resources/lib/deps/zeroconf/_services/browser.pxd
Normal file
117
resources/lib/deps/zeroconf/_services/browser.pxd
Normal file
|
@ -0,0 +1,117 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .._cache cimport DNSCache
|
||||
from .._history cimport QuestionHistory
|
||||
from .._protocol.outgoing cimport DNSOutgoing, DNSPointer, DNSQuestion, DNSRecord
|
||||
from .._record_update cimport RecordUpdate
|
||||
from .._updates cimport RecordUpdateListener
|
||||
from .._utils.time cimport current_time_millis, millis_to_seconds
|
||||
from . cimport Signal, SignalRegistrationInterface
|
||||
|
||||
|
||||
cdef bint TYPE_CHECKING
|
||||
cdef object cached_possible_types
|
||||
cdef cython.uint _EXPIRE_REFRESH_TIME_PERCENT, _MAX_MSG_TYPICAL, _DNS_PACKET_HEADER_LEN
|
||||
cdef cython.uint _TYPE_PTR
|
||||
cdef object _CLASS_IN
|
||||
cdef object SERVICE_STATE_CHANGE_ADDED, SERVICE_STATE_CHANGE_REMOVED, SERVICE_STATE_CHANGE_UPDATED
|
||||
cdef cython.set _ADDRESS_RECORD_TYPES
|
||||
cdef float RESCUE_RECORD_RETRY_TTL_PERCENTAGE
|
||||
|
||||
cdef object _MDNS_PORT, _BROWSER_TIME
|
||||
|
||||
cdef object QU_QUESTION
|
||||
|
||||
cdef object _FLAGS_QR_QUERY
|
||||
|
||||
cdef object heappop, heappush
|
||||
|
||||
cdef class _ScheduledPTRQuery:
|
||||
|
||||
cdef public str alias
|
||||
cdef public str name
|
||||
cdef public unsigned int ttl
|
||||
cdef public bint cancelled
|
||||
cdef public double expire_time_millis
|
||||
cdef public double when_millis
|
||||
|
||||
cdef class _DNSPointerOutgoingBucket:
|
||||
|
||||
cdef public double now_millis
|
||||
cdef public DNSOutgoing out
|
||||
cdef public cython.uint bytes
|
||||
|
||||
cpdef add(self, cython.uint max_compressed_size, DNSQuestion question, cython.set answers)
|
||||
|
||||
@cython.locals(cache=DNSCache, question_history=QuestionHistory, record=DNSRecord, qu_question=bint)
|
||||
cpdef list generate_service_query(
|
||||
object zc,
|
||||
double now_millis,
|
||||
set types_,
|
||||
bint multicast,
|
||||
object question_type
|
||||
)
|
||||
|
||||
@cython.locals(answer=DNSPointer, query_buckets=list, question=DNSQuestion, max_compressed_size=cython.uint, max_bucket_size=cython.uint, query_bucket=_DNSPointerOutgoingBucket)
|
||||
cdef list _group_ptr_queries_with_known_answers(double now_millis, bint multicast, cython.dict question_with_known_answers)
|
||||
|
||||
cdef class QueryScheduler:
|
||||
|
||||
cdef object _zc
|
||||
cdef set _types
|
||||
cdef str _addr
|
||||
cdef int _port
|
||||
cdef bint _multicast
|
||||
cdef tuple _first_random_delay_interval
|
||||
cdef double _min_time_between_queries_millis
|
||||
cdef object _loop
|
||||
cdef unsigned int _startup_queries_sent
|
||||
cdef public dict _next_scheduled_for_alias
|
||||
cdef public list _query_heap
|
||||
cdef object _next_run
|
||||
cdef double _clock_resolution_millis
|
||||
cdef object _question_type
|
||||
|
||||
cdef void _schedule_ptr_refresh(self, DNSPointer pointer, double expire_time_millis, double refresh_time_millis)
|
||||
|
||||
cdef void _schedule_ptr_query(self, _ScheduledPTRQuery scheduled_query)
|
||||
|
||||
@cython.locals(scheduled=_ScheduledPTRQuery)
|
||||
cpdef void cancel_ptr_refresh(self, DNSPointer pointer)
|
||||
|
||||
@cython.locals(current=_ScheduledPTRQuery, expire_time=double)
|
||||
cpdef void reschedule_ptr_first_refresh(self, DNSPointer pointer)
|
||||
|
||||
@cython.locals(ttl_millis='unsigned int', additional_wait=double, next_query_time=double)
|
||||
cpdef void schedule_rescue_query(self, _ScheduledPTRQuery query, double now_millis, float additional_percentage)
|
||||
|
||||
cpdef void _process_startup_queries(self)
|
||||
|
||||
@cython.locals(query=_ScheduledPTRQuery, next_scheduled=_ScheduledPTRQuery, next_when=double)
|
||||
cpdef void _process_ready_types(self)
|
||||
|
||||
cpdef void async_send_ready_queries(self, bint first_request, double now_millis, set ready_types)
|
||||
|
||||
cdef class _ServiceBrowserBase(RecordUpdateListener):
|
||||
|
||||
cdef public cython.set types
|
||||
cdef public object zc
|
||||
cdef DNSCache _cache
|
||||
cdef object _loop
|
||||
cdef public cython.dict _pending_handlers
|
||||
cdef public object _service_state_changed
|
||||
cdef public QueryScheduler query_scheduler
|
||||
cdef public bint done
|
||||
cdef public object _query_sender_task
|
||||
|
||||
cpdef void _enqueue_callback(self, object state_change, object type_, object name)
|
||||
|
||||
@cython.locals(record_update=RecordUpdate, record=DNSRecord, cache=DNSCache, service=DNSRecord, pointer=DNSPointer)
|
||||
cpdef void async_update_records(self, object zc, double now, cython.list records)
|
||||
|
||||
cpdef cython.list _names_matching_types(self, object types)
|
||||
|
||||
cpdef _fire_service_state_changed_event(self, cython.tuple event)
|
||||
|
||||
cpdef void async_update_records_complete(self)
|
806
resources/lib/deps/zeroconf/_services/browser.py
Normal file
806
resources/lib/deps/zeroconf/_services/browser.py
Normal file
|
@ -0,0 +1,806 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import heapq
|
||||
import queue
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import warnings
|
||||
from functools import partial
|
||||
from types import TracebackType # noqa # used in type hints
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from .._dns import DNSPointer, DNSQuestion, DNSQuestionType
|
||||
from .._logger import log
|
||||
from .._protocol.outgoing import DNSOutgoing
|
||||
from .._record_update import RecordUpdate
|
||||
from .._services import (
|
||||
ServiceListener,
|
||||
ServiceStateChange,
|
||||
Signal,
|
||||
SignalRegistrationInterface,
|
||||
)
|
||||
from .._updates import RecordUpdateListener
|
||||
from .._utils.name import cached_possible_types, service_type_name
|
||||
from .._utils.time import current_time_millis, millis_to_seconds
|
||||
from ..const import (
|
||||
_ADDRESS_RECORD_TYPES,
|
||||
_BROWSER_TIME,
|
||||
_CLASS_IN,
|
||||
_DNS_PACKET_HEADER_LEN,
|
||||
_EXPIRE_REFRESH_TIME_PERCENT,
|
||||
_FLAGS_QR_QUERY,
|
||||
_MAX_MSG_TYPICAL,
|
||||
_MDNS_ADDR,
|
||||
_MDNS_ADDR6,
|
||||
_MDNS_PORT,
|
||||
_TYPE_PTR,
|
||||
)
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc6762#section-5.2
|
||||
_FIRST_QUERY_DELAY_RANDOM_INTERVAL = (20, 120) # ms
|
||||
|
||||
_ON_CHANGE_DISPATCH = {
|
||||
ServiceStateChange.Added: "add_service",
|
||||
ServiceStateChange.Removed: "remove_service",
|
||||
ServiceStateChange.Updated: "update_service",
|
||||
}
|
||||
|
||||
SERVICE_STATE_CHANGE_ADDED = ServiceStateChange.Added
|
||||
SERVICE_STATE_CHANGE_REMOVED = ServiceStateChange.Removed
|
||||
SERVICE_STATE_CHANGE_UPDATED = ServiceStateChange.Updated
|
||||
|
||||
QU_QUESTION = DNSQuestionType.QU
|
||||
|
||||
STARTUP_QUERIES = 4
|
||||
|
||||
RESCUE_RECORD_RETRY_TTL_PERCENTAGE = 0.1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Zeroconf
|
||||
|
||||
float_ = float
|
||||
int_ = int
|
||||
bool_ = bool
|
||||
str_ = str
|
||||
|
||||
_QuestionWithKnownAnswers = Dict[DNSQuestion, Set[DNSPointer]]
|
||||
|
||||
heappop = heapq.heappop
|
||||
heappush = heapq.heappush
|
||||
|
||||
|
||||
class _ScheduledPTRQuery:
|
||||
|
||||
__slots__ = ('alias', 'name', 'ttl', 'cancelled', 'expire_time_millis', 'when_millis')
|
||||
|
||||
def __init__(
|
||||
self, alias: str, name: str, ttl: int, expire_time_millis: float, when_millis: float
|
||||
) -> None:
|
||||
"""Create a scheduled query."""
|
||||
self.alias = alias
|
||||
self.name = name
|
||||
self.ttl = ttl
|
||||
# Since queries are stored in a heap we need to track if they are cancelled
|
||||
# so we can remove them from the heap when they are cancelled as it would
|
||||
# be too expensive to search the heap for the record to remove and instead
|
||||
# we just mark it as cancelled and ignore it when we pop it off the heap
|
||||
# when the query is due.
|
||||
self.cancelled = False
|
||||
# Expire time millis is the actual millisecond time the record will expire
|
||||
self.expire_time_millis = expire_time_millis
|
||||
# When millis is the millisecond time the query should be sent
|
||||
# For the first query this is the refresh time which is 75% of the TTL
|
||||
#
|
||||
# For subsequent queries we increase the time by 10% of the TTL
|
||||
# until we reach the expire time and then we stop because it means
|
||||
# we failed to rescue the record.
|
||||
self.when_millis = when_millis
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the scheduled query."""
|
||||
return (
|
||||
f"<{self.__class__.__name__} "
|
||||
f"alias={self.alias} "
|
||||
f"name={self.name} "
|
||||
f"ttl={self.ttl} "
|
||||
f"cancelled={self.cancelled} "
|
||||
f"expire_time_millis={self.expire_time_millis} "
|
||||
f"when_millis={self.when_millis}"
|
||||
">"
|
||||
)
|
||||
|
||||
def __lt__(self, other: '_ScheduledPTRQuery') -> bool:
|
||||
"""Compare two scheduled queries."""
|
||||
if type(other) is _ScheduledPTRQuery:
|
||||
return self.when_millis < other.when_millis
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other: '_ScheduledPTRQuery') -> bool:
|
||||
"""Compare two scheduled queries."""
|
||||
if type(other) is _ScheduledPTRQuery:
|
||||
return self.when_millis < other.when_millis or self.__eq__(other)
|
||||
return NotImplemented
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Compare two scheduled queries."""
|
||||
if type(other) is _ScheduledPTRQuery:
|
||||
return self.when_millis == other.when_millis
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other: '_ScheduledPTRQuery') -> bool:
|
||||
"""Compare two scheduled queries."""
|
||||
if type(other) is _ScheduledPTRQuery:
|
||||
return self.when_millis > other.when_millis or self.__eq__(other)
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other: '_ScheduledPTRQuery') -> bool:
|
||||
"""Compare two scheduled queries."""
|
||||
if type(other) is _ScheduledPTRQuery:
|
||||
return self.when_millis > other.when_millis
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class _DNSPointerOutgoingBucket:
|
||||
"""A DNSOutgoing bucket."""
|
||||
|
||||
__slots__ = ('now_millis', 'out', 'bytes')
|
||||
|
||||
def __init__(self, now_millis: float, multicast: bool) -> None:
|
||||
"""Create a bucket to wrap a DNSOutgoing."""
|
||||
self.now_millis = now_millis
|
||||
self.out = DNSOutgoing(_FLAGS_QR_QUERY, multicast)
|
||||
self.bytes = 0
|
||||
|
||||
def add(self, max_compressed_size: int_, question: DNSQuestion, answers: Set[DNSPointer]) -> None:
|
||||
"""Add a new set of questions and known answers to the outgoing."""
|
||||
self.out.add_question(question)
|
||||
for answer in answers:
|
||||
self.out.add_answer_at_time(answer, self.now_millis)
|
||||
self.bytes += max_compressed_size
|
||||
|
||||
|
||||
def group_ptr_queries_with_known_answers(
|
||||
now: float_, multicast: bool_, question_with_known_answers: _QuestionWithKnownAnswers
|
||||
) -> List[DNSOutgoing]:
|
||||
"""Aggregate queries so that as many known answers as possible fit in the same packet
|
||||
without having known answers spill over into the next packet unless the
|
||||
question and known answers are always going to exceed the packet size.
|
||||
|
||||
Some responders do not implement multi-packet known answer suppression
|
||||
so we try to keep all the known answers in the same packet as the
|
||||
questions.
|
||||
"""
|
||||
return _group_ptr_queries_with_known_answers(now, multicast, question_with_known_answers)
|
||||
|
||||
|
||||
def _group_ptr_queries_with_known_answers(
|
||||
now_millis: float_, multicast: bool_, question_with_known_answers: _QuestionWithKnownAnswers
|
||||
) -> List[DNSOutgoing]:
|
||||
"""Inner wrapper for group_ptr_queries_with_known_answers."""
|
||||
# This is the maximum size the query + known answers can be with name compression.
|
||||
# The actual size of the query + known answers may be a bit smaller since other
|
||||
# parts may be shared when the final DNSOutgoing packets are constructed. The
|
||||
# goal of this algorithm is to quickly bucket the query + known answers without
|
||||
# the overhead of actually constructing the packets.
|
||||
query_by_size: Dict[DNSQuestion, int] = {
|
||||
question: (question.max_size + sum(answer.max_size_compressed for answer in known_answers))
|
||||
for question, known_answers in question_with_known_answers.items()
|
||||
}
|
||||
max_bucket_size = _MAX_MSG_TYPICAL - _DNS_PACKET_HEADER_LEN
|
||||
query_buckets: List[_DNSPointerOutgoingBucket] = []
|
||||
for question in sorted(
|
||||
query_by_size,
|
||||
key=query_by_size.get, # type: ignore
|
||||
reverse=True,
|
||||
):
|
||||
max_compressed_size = query_by_size[question]
|
||||
answers = question_with_known_answers[question]
|
||||
for query_bucket in query_buckets:
|
||||
if query_bucket.bytes + max_compressed_size <= max_bucket_size:
|
||||
query_bucket.add(max_compressed_size, question, answers)
|
||||
break
|
||||
else:
|
||||
# If a single question and known answers won't fit in a packet
|
||||
# we will end up generating multiple packets, but there will never
|
||||
# be multiple questions
|
||||
query_bucket = _DNSPointerOutgoingBucket(now_millis, multicast)
|
||||
query_bucket.add(max_compressed_size, question, answers)
|
||||
query_buckets.append(query_bucket)
|
||||
|
||||
return [query_bucket.out for query_bucket in query_buckets]
|
||||
|
||||
|
||||
def generate_service_query(
|
||||
zc: 'Zeroconf',
|
||||
now_millis: float_,
|
||||
types_: Set[str],
|
||||
multicast: bool,
|
||||
question_type: Optional[DNSQuestionType],
|
||||
) -> List[DNSOutgoing]:
|
||||
"""Generate a service query for sending with zeroconf.send."""
|
||||
questions_with_known_answers: _QuestionWithKnownAnswers = {}
|
||||
qu_question = not multicast if question_type is None else question_type is QU_QUESTION
|
||||
question_history = zc.question_history
|
||||
cache = zc.cache
|
||||
for type_ in types_:
|
||||
question = DNSQuestion(type_, _TYPE_PTR, _CLASS_IN)
|
||||
question.unicast = qu_question
|
||||
known_answers = {
|
||||
record
|
||||
for record in cache.get_all_by_details(type_, _TYPE_PTR, _CLASS_IN)
|
||||
if not record.is_stale(now_millis)
|
||||
}
|
||||
if not qu_question and question_history.suppresses(question, now_millis, known_answers):
|
||||
log.debug("Asking %s was suppressed by the question history", question)
|
||||
continue
|
||||
if TYPE_CHECKING:
|
||||
pointer_known_answers = cast(Set[DNSPointer], known_answers)
|
||||
else:
|
||||
pointer_known_answers = known_answers
|
||||
questions_with_known_answers[question] = pointer_known_answers
|
||||
if not qu_question:
|
||||
question_history.add_question_at_time(question, now_millis, known_answers)
|
||||
|
||||
return _group_ptr_queries_with_known_answers(now_millis, multicast, questions_with_known_answers)
|
||||
|
||||
|
||||
def _on_change_dispatcher(
|
||||
listener: ServiceListener,
|
||||
zeroconf: 'Zeroconf',
|
||||
service_type: str,
|
||||
name: str,
|
||||
state_change: ServiceStateChange,
|
||||
) -> None:
|
||||
"""Dispatch a service state change to a listener."""
|
||||
getattr(listener, _ON_CHANGE_DISPATCH[state_change])(zeroconf, service_type, name)
|
||||
|
||||
|
||||
def _service_state_changed_from_listener(listener: ServiceListener) -> Callable[..., None]:
|
||||
"""Generate a service_state_changed handlers from a listener."""
|
||||
assert listener is not None
|
||||
if not hasattr(listener, 'update_service'):
|
||||
warnings.warn(
|
||||
"%r has no update_service method. Provide one (it can be empty if you "
|
||||
"don't care about the updates), it'll become mandatory." % (listener,),
|
||||
FutureWarning,
|
||||
)
|
||||
return partial(_on_change_dispatcher, listener)
|
||||
|
||||
|
||||
class QueryScheduler:
|
||||
"""Schedule outgoing PTR queries for Continuous Multicast DNS Querying
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc6762#section-5.2
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_zc',
|
||||
'_types',
|
||||
'_addr',
|
||||
'_port',
|
||||
'_multicast',
|
||||
'_first_random_delay_interval',
|
||||
'_min_time_between_queries_millis',
|
||||
'_loop',
|
||||
'_startup_queries_sent',
|
||||
'_next_scheduled_for_alias',
|
||||
'_query_heap',
|
||||
'_next_run',
|
||||
'_clock_resolution_millis',
|
||||
'_question_type',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
zc: "Zeroconf",
|
||||
types: Set[str],
|
||||
addr: Optional[str],
|
||||
port: int,
|
||||
multicast: bool,
|
||||
delay: int,
|
||||
first_random_delay_interval: Tuple[int, int],
|
||||
question_type: Optional[DNSQuestionType],
|
||||
) -> None:
|
||||
self._zc = zc
|
||||
self._types = types
|
||||
self._addr = addr
|
||||
self._port = port
|
||||
self._multicast = multicast
|
||||
self._first_random_delay_interval = first_random_delay_interval
|
||||
self._min_time_between_queries_millis = delay
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._startup_queries_sent = 0
|
||||
self._next_scheduled_for_alias: Dict[str, _ScheduledPTRQuery] = {}
|
||||
self._query_heap: list[_ScheduledPTRQuery] = []
|
||||
self._next_run: Optional[asyncio.TimerHandle] = None
|
||||
self._clock_resolution_millis = time.get_clock_info('monotonic').resolution * 1000
|
||||
self._question_type = question_type
|
||||
|
||||
def start(self, loop: asyncio.AbstractEventLoop) -> None:
|
||||
"""Start the scheduler.
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc6762#section-5.2
|
||||
To avoid accidental synchronization when, for some reason, multiple
|
||||
clients begin querying at exactly the same moment (e.g., because of
|
||||
some common external trigger event), a Multicast DNS querier SHOULD
|
||||
also delay the first query of the series by a randomly chosen amount
|
||||
in the range 20-120 ms.
|
||||
"""
|
||||
start_delay = millis_to_seconds(random.randint(*self._first_random_delay_interval))
|
||||
self._loop = loop
|
||||
self._next_run = loop.call_later(start_delay, self._process_startup_queries)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the scheduler."""
|
||||
if self._next_run is not None:
|
||||
self._next_run.cancel()
|
||||
self._next_run = None
|
||||
self._next_scheduled_for_alias.clear()
|
||||
self._query_heap.clear()
|
||||
|
||||
def _schedule_ptr_refresh(
|
||||
self, pointer: DNSPointer, expire_time_millis: float_, refresh_time_millis: float_
|
||||
) -> None:
|
||||
"""Schedule a query for a pointer."""
|
||||
ttl = int(pointer.ttl) if isinstance(pointer.ttl, float) else pointer.ttl
|
||||
scheduled_ptr_query = _ScheduledPTRQuery(
|
||||
pointer.alias, pointer.name, ttl, expire_time_millis, refresh_time_millis
|
||||
)
|
||||
self._schedule_ptr_query(scheduled_ptr_query)
|
||||
|
||||
def _schedule_ptr_query(self, scheduled_query: _ScheduledPTRQuery) -> None:
|
||||
"""Schedule a query for a pointer."""
|
||||
self._next_scheduled_for_alias[scheduled_query.alias] = scheduled_query
|
||||
heappush(self._query_heap, scheduled_query)
|
||||
|
||||
def cancel_ptr_refresh(self, pointer: DNSPointer) -> None:
|
||||
"""Cancel a query for a pointer."""
|
||||
scheduled = self._next_scheduled_for_alias.pop(pointer.alias, None)
|
||||
if scheduled:
|
||||
scheduled.cancelled = True
|
||||
|
||||
def reschedule_ptr_first_refresh(self, pointer: DNSPointer) -> None:
|
||||
"""Reschedule a query for a pointer."""
|
||||
current = self._next_scheduled_for_alias.get(pointer.alias)
|
||||
refresh_time_millis = pointer.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT)
|
||||
if current is not None:
|
||||
# If the expire time is within self._min_time_between_queries_millis
|
||||
# of the current scheduled time avoid churn by not rescheduling
|
||||
if (
|
||||
-self._min_time_between_queries_millis
|
||||
<= refresh_time_millis - current.when_millis
|
||||
<= self._min_time_between_queries_millis
|
||||
):
|
||||
return
|
||||
current.cancelled = True
|
||||
del self._next_scheduled_for_alias[pointer.alias]
|
||||
expire_time_millis = pointer.get_expiration_time(100)
|
||||
self._schedule_ptr_refresh(pointer, expire_time_millis, refresh_time_millis)
|
||||
|
||||
def schedule_rescue_query(
|
||||
self, query: _ScheduledPTRQuery, now_millis: float_, additional_percentage: float_
|
||||
) -> None:
|
||||
"""Reschedule a query for a pointer at an additional percentage of expiration."""
|
||||
ttl_millis = query.ttl * 1000
|
||||
additional_wait = ttl_millis * additional_percentage
|
||||
next_query_time = now_millis + additional_wait
|
||||
if next_query_time >= query.expire_time_millis:
|
||||
# If we would schedule past the expire time
|
||||
# there is no point in scheduling as we already
|
||||
# tried to rescue the record and failed
|
||||
return
|
||||
scheduled_ptr_query = _ScheduledPTRQuery(
|
||||
query.alias, query.name, query.ttl, query.expire_time_millis, next_query_time
|
||||
)
|
||||
self._schedule_ptr_query(scheduled_ptr_query)
|
||||
|
||||
def _process_startup_queries(self) -> None:
|
||||
if TYPE_CHECKING:
|
||||
assert self._loop is not None
|
||||
# This is a safety to ensure we stop sending queries if Zeroconf instance
|
||||
# is stopped without the browser being cancelled
|
||||
if self._zc.done:
|
||||
return
|
||||
|
||||
now_millis = current_time_millis()
|
||||
|
||||
# At first we will send STARTUP_QUERIES queries to get the cache populated
|
||||
self.async_send_ready_queries(self._startup_queries_sent == 0, now_millis, self._types)
|
||||
self._startup_queries_sent += 1
|
||||
|
||||
# Once we finish sending the initial queries we will
|
||||
# switch to a strategy of sending queries only when we
|
||||
# need to refresh records that are about to expire
|
||||
if self._startup_queries_sent >= STARTUP_QUERIES:
|
||||
self._next_run = self._loop.call_at(
|
||||
millis_to_seconds(now_millis + self._min_time_between_queries_millis),
|
||||
self._process_ready_types,
|
||||
)
|
||||
return
|
||||
|
||||
self._next_run = self._loop.call_later(self._startup_queries_sent**2, self._process_startup_queries)
|
||||
|
||||
def _process_ready_types(self) -> None:
|
||||
"""Generate a list of ready types that is due and schedule the next time."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._loop is not None
|
||||
# This is a safety to ensure we stop sending queries if Zeroconf instance
|
||||
# is stopped without the browser being cancelled
|
||||
if self._zc.done:
|
||||
return
|
||||
|
||||
now_millis = current_time_millis()
|
||||
# Refresh records that are about to expire (aka
|
||||
# _EXPIRE_REFRESH_TIME_PERCENT which is currently 75% of the TTL) and
|
||||
# additional rescue queries if the 75% query failed to refresh the record
|
||||
# with a minimum time between queries of _min_time_between_queries
|
||||
# which defaults to 10s
|
||||
|
||||
ready_types: Set[str] = set()
|
||||
next_scheduled: Optional[_ScheduledPTRQuery] = None
|
||||
end_time_millis = now_millis + self._clock_resolution_millis
|
||||
schedule_rescue: List[_ScheduledPTRQuery] = []
|
||||
|
||||
while self._query_heap:
|
||||
query = self._query_heap[0]
|
||||
if query.cancelled:
|
||||
heappop(self._query_heap)
|
||||
continue
|
||||
if query.when_millis > end_time_millis:
|
||||
next_scheduled = query
|
||||
break
|
||||
query = heappop(self._query_heap)
|
||||
ready_types.add(query.name)
|
||||
del self._next_scheduled_for_alias[query.alias]
|
||||
# If there is still more than 10% of the TTL remaining
|
||||
# schedule a query again to try to rescue the record
|
||||
# from expiring. If the record is refreshed before
|
||||
# the query, the query will get cancelled.
|
||||
schedule_rescue.append(query)
|
||||
|
||||
for query in schedule_rescue:
|
||||
self.schedule_rescue_query(query, now_millis, RESCUE_RECORD_RETRY_TTL_PERCENTAGE)
|
||||
|
||||
if ready_types:
|
||||
self.async_send_ready_queries(False, now_millis, ready_types)
|
||||
|
||||
next_time_millis = now_millis + self._min_time_between_queries_millis
|
||||
|
||||
if next_scheduled is not None and next_scheduled.when_millis > next_time_millis:
|
||||
next_when_millis = next_scheduled.when_millis
|
||||
else:
|
||||
next_when_millis = next_time_millis
|
||||
|
||||
self._next_run = self._loop.call_at(millis_to_seconds(next_when_millis), self._process_ready_types)
|
||||
|
||||
def async_send_ready_queries(
|
||||
self, first_request: bool, now_millis: float_, ready_types: Set[str]
|
||||
) -> None:
|
||||
"""Send any ready queries."""
|
||||
# If they did not specify and this is the first request, ask QU questions
|
||||
# https://datatracker.ietf.org/doc/html/rfc6762#section-5.4 since we are
|
||||
# just starting up and we know our cache is likely empty. This ensures
|
||||
# the next outgoing will be sent with the known answers list.
|
||||
question_type = QU_QUESTION if self._question_type is None and first_request else self._question_type
|
||||
outs = generate_service_query(self._zc, now_millis, ready_types, self._multicast, question_type)
|
||||
if outs:
|
||||
for out in outs:
|
||||
self._zc.async_send(out, self._addr, self._port)
|
||||
|
||||
|
||||
class _ServiceBrowserBase(RecordUpdateListener):
|
||||
"""Base class for ServiceBrowser."""
|
||||
|
||||
__slots__ = (
|
||||
'types',
|
||||
'zc',
|
||||
'_cache',
|
||||
'_loop',
|
||||
'_pending_handlers',
|
||||
'_service_state_changed',
|
||||
'query_scheduler',
|
||||
'done',
|
||||
'_query_sender_task',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
zc: 'Zeroconf',
|
||||
type_: Union[str, list],
|
||||
handlers: Optional[Union[ServiceListener, List[Callable[..., None]]]] = None,
|
||||
listener: Optional[ServiceListener] = None,
|
||||
addr: Optional[str] = None,
|
||||
port: int = _MDNS_PORT,
|
||||
delay: int = _BROWSER_TIME,
|
||||
question_type: Optional[DNSQuestionType] = None,
|
||||
) -> None:
|
||||
"""Used to browse for a service for specific type(s).
|
||||
|
||||
Constructor parameters are as follows:
|
||||
|
||||
* `zc`: A Zeroconf instance
|
||||
* `type_`: fully qualified service type name
|
||||
* `handler`: ServiceListener or Callable that knows how to process ServiceStateChange events
|
||||
* `listener`: ServiceListener
|
||||
* `addr`: address to send queries (will default to multicast)
|
||||
* `port`: port to send queries (will default to mdns 5353)
|
||||
* `delay`: The initial delay between answering questions
|
||||
* `question_type`: The type of questions to ask (DNSQuestionType.QM or DNSQuestionType.QU)
|
||||
|
||||
The listener object will have its add_service() and
|
||||
remove_service() methods called when this browser
|
||||
discovers changes in the services availability.
|
||||
"""
|
||||
assert handlers or listener, 'You need to specify at least one handler'
|
||||
self.types: Set[str] = set(type_ if isinstance(type_, list) else [type_])
|
||||
for check_type_ in self.types:
|
||||
# Will generate BadTypeInNameException on a bad name
|
||||
service_type_name(check_type_, strict=False)
|
||||
self.zc = zc
|
||||
self._cache = zc.cache
|
||||
assert zc.loop is not None
|
||||
self._loop = zc.loop
|
||||
self._pending_handlers: Dict[Tuple[str, str], ServiceStateChange] = {}
|
||||
self._service_state_changed = Signal()
|
||||
self.query_scheduler = QueryScheduler(
|
||||
zc,
|
||||
self.types,
|
||||
addr,
|
||||
port,
|
||||
addr in (None, _MDNS_ADDR, _MDNS_ADDR6),
|
||||
delay,
|
||||
_FIRST_QUERY_DELAY_RANDOM_INTERVAL,
|
||||
question_type,
|
||||
)
|
||||
self.done = False
|
||||
self._query_sender_task: Optional[asyncio.Task] = None
|
||||
|
||||
if hasattr(handlers, 'add_service'):
|
||||
listener = cast('ServiceListener', handlers)
|
||||
handlers = None
|
||||
|
||||
handlers = cast(List[Callable[..., None]], handlers or [])
|
||||
|
||||
if listener:
|
||||
handlers.append(_service_state_changed_from_listener(listener))
|
||||
|
||||
for h in handlers:
|
||||
self.service_state_changed.register_handler(h)
|
||||
|
||||
def _async_start(self) -> None:
|
||||
"""Generate the next time and setup listeners.
|
||||
|
||||
Must be called by uses of this base class after they
|
||||
have finished setting their properties.
|
||||
"""
|
||||
self.zc.async_add_listener(self, [DNSQuestion(type_, _TYPE_PTR, _CLASS_IN) for type_ in self.types])
|
||||
# Only start queries after the listener is installed
|
||||
self._query_sender_task = asyncio.ensure_future(self._async_start_query_sender())
|
||||
|
||||
@property
|
||||
def service_state_changed(self) -> SignalRegistrationInterface:
|
||||
return self._service_state_changed.registration_interface
|
||||
|
||||
def _names_matching_types(self, names: Iterable[str]) -> List[Tuple[str, str]]:
|
||||
"""Return the type and name for records matching the types we are browsing."""
|
||||
return [
|
||||
(type_, name) for name in names for type_ in self.types.intersection(cached_possible_types(name))
|
||||
]
|
||||
|
||||
def _enqueue_callback(
|
||||
self,
|
||||
state_change: ServiceStateChange,
|
||||
type_: str_,
|
||||
name: str_,
|
||||
) -> None:
|
||||
# Code to ensure we only do a single update message
|
||||
# Precedence is; Added, Remove, Update
|
||||
key = (name, type_)
|
||||
if (
|
||||
state_change is SERVICE_STATE_CHANGE_ADDED
|
||||
or (
|
||||
state_change is SERVICE_STATE_CHANGE_REMOVED
|
||||
and self._pending_handlers.get(key) is not SERVICE_STATE_CHANGE_ADDED
|
||||
)
|
||||
or (state_change is SERVICE_STATE_CHANGE_UPDATED and key not in self._pending_handlers)
|
||||
):
|
||||
self._pending_handlers[key] = state_change
|
||||
|
||||
def async_update_records(self, zc: 'Zeroconf', now: float_, records: List[RecordUpdate]) -> None:
|
||||
"""Callback invoked by Zeroconf when new information arrives.
|
||||
|
||||
Updates information required by browser in the Zeroconf cache.
|
||||
|
||||
Ensures that there is are no unnecessary duplicates in the list.
|
||||
|
||||
This method will be run in the event loop.
|
||||
"""
|
||||
for record_update in records:
|
||||
record = record_update.new
|
||||
old_record = record_update.old
|
||||
record_type = record.type
|
||||
|
||||
if record_type is _TYPE_PTR:
|
||||
if TYPE_CHECKING:
|
||||
record = cast(DNSPointer, record)
|
||||
pointer = record
|
||||
for type_ in self.types.intersection(cached_possible_types(pointer.name)):
|
||||
if old_record is None:
|
||||
self._enqueue_callback(SERVICE_STATE_CHANGE_ADDED, type_, pointer.alias)
|
||||
self.query_scheduler.reschedule_ptr_first_refresh(pointer)
|
||||
elif pointer.is_expired(now):
|
||||
self._enqueue_callback(SERVICE_STATE_CHANGE_REMOVED, type_, pointer.alias)
|
||||
self.query_scheduler.cancel_ptr_refresh(pointer)
|
||||
else:
|
||||
self.query_scheduler.reschedule_ptr_first_refresh(pointer)
|
||||
continue
|
||||
|
||||
# If its expired or already exists in the cache it cannot be updated.
|
||||
if old_record is not None or record.is_expired(now):
|
||||
continue
|
||||
|
||||
if record_type in _ADDRESS_RECORD_TYPES:
|
||||
cache = self._cache
|
||||
names = {service.name for service in cache.async_entries_with_server(record.name)}
|
||||
# Iterate through the DNSCache and callback any services that use this address
|
||||
for type_, name in self._names_matching_types(names):
|
||||
self._enqueue_callback(SERVICE_STATE_CHANGE_UPDATED, type_, name)
|
||||
continue
|
||||
|
||||
for type_, name in self._names_matching_types((record.name,)):
|
||||
self._enqueue_callback(SERVICE_STATE_CHANGE_UPDATED, type_, name)
|
||||
|
||||
def async_update_records_complete(self) -> None:
|
||||
"""Called when a record update has completed for all handlers.
|
||||
|
||||
At this point the cache will have the new records.
|
||||
|
||||
This method will be run in the event loop.
|
||||
|
||||
This method is expected to be overridden by subclasses.
|
||||
"""
|
||||
for pending in self._pending_handlers.items():
|
||||
self._fire_service_state_changed_event(pending)
|
||||
self._pending_handlers.clear()
|
||||
|
||||
def _fire_service_state_changed_event(self, event: Tuple[Tuple[str, str], ServiceStateChange]) -> None:
|
||||
"""Fire a service state changed event.
|
||||
|
||||
When running with ServiceBrowser, this will happen in the dedicated
|
||||
thread.
|
||||
|
||||
When running with AsyncServiceBrowser, this will happen in the event loop.
|
||||
"""
|
||||
name_type = event[0]
|
||||
state_change = event[1]
|
||||
self._service_state_changed.fire(
|
||||
zeroconf=self.zc,
|
||||
service_type=name_type[1],
|
||||
name=name_type[0],
|
||||
state_change=state_change,
|
||||
)
|
||||
|
||||
def _async_cancel(self) -> None:
|
||||
"""Cancel the browser."""
|
||||
self.done = True
|
||||
self.query_scheduler.stop()
|
||||
self.zc.async_remove_listener(self)
|
||||
assert self._query_sender_task is not None, "Attempted to cancel a browser that was not started"
|
||||
self._query_sender_task.cancel()
|
||||
self._query_sender_task = None
|
||||
|
||||
async def _async_start_query_sender(self) -> None:
|
||||
"""Start scheduling queries."""
|
||||
if not self.zc.started:
|
||||
await self.zc.async_wait_for_start()
|
||||
self.query_scheduler.start(self._loop)
|
||||
|
||||
|
||||
class ServiceBrowser(_ServiceBrowserBase, threading.Thread):
|
||||
"""Used to browse for a service of a specific type.
|
||||
|
||||
The listener object will have its add_service() and
|
||||
remove_service() methods called when this browser
|
||||
discovers changes in the services availability."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
zc: 'Zeroconf',
|
||||
type_: Union[str, list],
|
||||
handlers: Optional[Union[ServiceListener, List[Callable[..., None]]]] = None,
|
||||
listener: Optional[ServiceListener] = None,
|
||||
addr: Optional[str] = None,
|
||||
port: int = _MDNS_PORT,
|
||||
delay: int = _BROWSER_TIME,
|
||||
question_type: Optional[DNSQuestionType] = None,
|
||||
) -> None:
|
||||
assert zc.loop is not None
|
||||
if not zc.loop.is_running():
|
||||
raise RuntimeError("The event loop is not running")
|
||||
threading.Thread.__init__(self)
|
||||
super().__init__(zc, type_, handlers, listener, addr, port, delay, question_type)
|
||||
# Add the queue before the listener is installed in _setup
|
||||
# to ensure that events run in the dedicated thread and do
|
||||
# not block the event loop
|
||||
self.queue: queue.SimpleQueue = queue.SimpleQueue()
|
||||
self.daemon = True
|
||||
self.start()
|
||||
zc.loop.call_soon_threadsafe(self._async_start)
|
||||
self.name = "zeroconf-ServiceBrowser-{}-{}".format(
|
||||
'-'.join([type_[:-7] for type_ in self.types]),
|
||||
getattr(self, 'native_id', self.ident),
|
||||
)
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel the browser."""
|
||||
assert self.zc.loop is not None
|
||||
self.queue.put(None)
|
||||
self.zc.loop.call_soon_threadsafe(self._async_cancel)
|
||||
self.join()
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the browser thread."""
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
if event is None:
|
||||
return
|
||||
self._fire_service_state_changed_event(event)
|
||||
|
||||
def async_update_records_complete(self) -> None:
|
||||
"""Called when a record update has completed for all handlers.
|
||||
|
||||
At this point the cache will have the new records.
|
||||
|
||||
This method will be run in the event loop.
|
||||
"""
|
||||
for pending in self._pending_handlers.items():
|
||||
self.queue.put(pending)
|
||||
self._pending_handlers.clear()
|
||||
|
||||
def __enter__(self) -> 'ServiceBrowser':
|
||||
return self
|
||||
|
||||
def __exit__( # pylint: disable=useless-return
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
self.cancel()
|
||||
return None
|
BIN
resources/lib/deps/zeroconf/_services/info.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_services/info.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
158
resources/lib/deps/zeroconf/_services/info.pxd
Normal file
158
resources/lib/deps/zeroconf/_services/info.pxd
Normal file
|
@ -0,0 +1,158 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .._cache cimport DNSCache
|
||||
from .._dns cimport (
|
||||
DNSAddress,
|
||||
DNSNsec,
|
||||
DNSPointer,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
DNSService,
|
||||
DNSText,
|
||||
)
|
||||
from .._history cimport QuestionHistory
|
||||
from .._protocol.outgoing cimport DNSOutgoing
|
||||
from .._record_update cimport RecordUpdate
|
||||
from .._updates cimport RecordUpdateListener
|
||||
from .._utils.ipaddress cimport (
|
||||
get_ip_address_object_from_record,
|
||||
ip_bytes_and_scope_to_address,
|
||||
str_without_scope_id,
|
||||
)
|
||||
from .._utils.time cimport current_time_millis
|
||||
|
||||
|
||||
cdef object _resolve_all_futures_to_none
|
||||
|
||||
cdef object _TYPE_SRV
|
||||
cdef object _TYPE_TXT
|
||||
cdef object _TYPE_A
|
||||
cdef object _TYPE_AAAA
|
||||
cdef object _TYPE_PTR
|
||||
cdef object _TYPE_NSEC
|
||||
cdef object _CLASS_IN
|
||||
cdef object _FLAGS_QR_QUERY
|
||||
|
||||
cdef object service_type_name
|
||||
|
||||
cdef object QU_QUESTION
|
||||
cdef object QM_QUESTION
|
||||
|
||||
cdef object _IPVersion_All_value
|
||||
cdef object _IPVersion_V4Only_value
|
||||
|
||||
cdef cython.set _ADDRESS_RECORD_TYPES
|
||||
|
||||
cdef unsigned int _DUPLICATE_QUESTION_INTERVAL
|
||||
|
||||
cdef bint TYPE_CHECKING
|
||||
cdef bint IPADDRESS_SUPPORTS_SCOPE_ID
|
||||
cdef object cached_ip_addresses
|
||||
|
||||
cdef object randint
|
||||
|
||||
cdef class ServiceInfo(RecordUpdateListener):
|
||||
|
||||
cdef public cython.bytes text
|
||||
cdef public str type
|
||||
cdef str _name
|
||||
cdef public str key
|
||||
cdef public cython.list _ipv4_addresses
|
||||
cdef public cython.list _ipv6_addresses
|
||||
cdef public object port
|
||||
cdef public object weight
|
||||
cdef public object priority
|
||||
cdef public str server
|
||||
cdef public str server_key
|
||||
cdef public cython.dict _properties
|
||||
cdef public cython.dict _decoded_properties
|
||||
cdef public object host_ttl
|
||||
cdef public object other_ttl
|
||||
cdef public object interface_index
|
||||
cdef public cython.set _new_records_futures
|
||||
cdef public DNSPointer _dns_pointer_cache
|
||||
cdef public DNSService _dns_service_cache
|
||||
cdef public DNSText _dns_text_cache
|
||||
cdef public cython.list _dns_address_cache
|
||||
cdef public cython.set _get_address_and_nsec_records_cache
|
||||
|
||||
@cython.locals(record_update=RecordUpdate, update=bint, cache=DNSCache)
|
||||
cpdef void async_update_records(self, object zc, double now, cython.list records)
|
||||
|
||||
@cython.locals(cache=DNSCache)
|
||||
cpdef bint _load_from_cache(self, object zc, double now)
|
||||
|
||||
@cython.locals(length="unsigned char", index="unsigned int", key_value=bytes, key_sep_value=tuple)
|
||||
cdef void _unpack_text_into_properties(self)
|
||||
|
||||
@cython.locals(k=bytes, v=bytes)
|
||||
cdef void _generate_decoded_properties(self)
|
||||
|
||||
@cython.locals(properties_contain_str=bint)
|
||||
cpdef void _set_properties(self, cython.dict properties)
|
||||
|
||||
cdef void _set_text(self, cython.bytes text)
|
||||
|
||||
@cython.locals(record=DNSAddress)
|
||||
cdef _get_ip_addresses_from_cache_lifo(self, object zc, double now, object type)
|
||||
|
||||
@cython.locals(
|
||||
dns_service_record=DNSService,
|
||||
dns_text_record=DNSText,
|
||||
dns_address_record=DNSAddress
|
||||
)
|
||||
cdef bint _process_record_threadsafe(self, object zc, DNSRecord record, double now)
|
||||
|
||||
@cython.locals(cache=DNSCache)
|
||||
cdef cython.list _get_address_records_from_cache_by_type(self, object zc, object _type)
|
||||
|
||||
cdef void _set_ipv4_addresses_from_cache(self, object zc, double now)
|
||||
|
||||
cdef void _set_ipv6_addresses_from_cache(self, object zc, double now)
|
||||
|
||||
cdef cython.list _ip_addresses_by_version_value(self, object version_value)
|
||||
|
||||
cpdef addresses_by_version(self, object version)
|
||||
|
||||
cpdef ip_addresses_by_version(self, object version)
|
||||
|
||||
@cython.locals(cacheable=cython.bint)
|
||||
cdef cython.list _dns_addresses(self, object override_ttls, object version)
|
||||
|
||||
@cython.locals(cacheable=cython.bint)
|
||||
cdef DNSPointer _dns_pointer(self, object override_ttl)
|
||||
|
||||
@cython.locals(cacheable=cython.bint)
|
||||
cdef DNSService _dns_service(self, object override_ttl)
|
||||
|
||||
@cython.locals(cacheable=cython.bint)
|
||||
cdef DNSText _dns_text(self, object override_ttl)
|
||||
|
||||
cdef DNSNsec _dns_nsec(self, cython.list missing_types, object override_ttl)
|
||||
|
||||
@cython.locals(cacheable=cython.bint)
|
||||
cdef cython.set _get_address_and_nsec_records(self, object override_ttl)
|
||||
|
||||
cpdef void async_clear_cache(self)
|
||||
|
||||
@cython.locals(cache=DNSCache, history=QuestionHistory, out=DNSOutgoing, qu_question=bint)
|
||||
cdef DNSOutgoing _generate_request_query(self, object zc, double now, object question_type)
|
||||
|
||||
@cython.locals(question=DNSQuestion, answer=DNSRecord)
|
||||
cdef void _add_question_with_known_answers(
|
||||
self,
|
||||
DNSOutgoing out,
|
||||
bint qu_question,
|
||||
QuestionHistory question_history,
|
||||
DNSCache cache,
|
||||
double now,
|
||||
str name,
|
||||
object type_,
|
||||
object class_,
|
||||
bint skip_if_known_answers
|
||||
)
|
||||
|
||||
cdef double _get_initial_delay(self)
|
||||
|
||||
cdef double _get_random_delay(self)
|
926
resources/lib/deps/zeroconf/_services/info.py
Normal file
926
resources/lib/deps/zeroconf/_services/info.py
Normal file
|
@ -0,0 +1,926 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import sys
|
||||
from ipaddress import IPv4Address, IPv6Address, _BaseAddress
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union, cast
|
||||
|
||||
from .._cache import DNSCache
|
||||
from .._dns import (
|
||||
DNSAddress,
|
||||
DNSNsec,
|
||||
DNSPointer,
|
||||
DNSQuestion,
|
||||
DNSQuestionType,
|
||||
DNSRecord,
|
||||
DNSService,
|
||||
DNSText,
|
||||
)
|
||||
from .._exceptions import BadTypeInNameException
|
||||
from .._history import QuestionHistory
|
||||
from .._logger import log
|
||||
from .._protocol.outgoing import DNSOutgoing
|
||||
from .._record_update import RecordUpdate
|
||||
from .._updates import RecordUpdateListener
|
||||
from .._utils.asyncio import (
|
||||
_resolve_all_futures_to_none,
|
||||
get_running_loop,
|
||||
run_coro_with_timeout,
|
||||
wait_for_future_set_or_timeout,
|
||||
)
|
||||
from .._utils.ipaddress import (
|
||||
cached_ip_addresses,
|
||||
get_ip_address_object_from_record,
|
||||
ip_bytes_and_scope_to_address,
|
||||
str_without_scope_id,
|
||||
)
|
||||
from .._utils.name import service_type_name
|
||||
from .._utils.net import IPVersion, _encode_address
|
||||
from .._utils.time import current_time_millis
|
||||
from ..const import (
|
||||
_ADDRESS_RECORD_TYPES,
|
||||
_CLASS_IN,
|
||||
_CLASS_IN_UNIQUE,
|
||||
_DNS_HOST_TTL,
|
||||
_DNS_OTHER_TTL,
|
||||
_DUPLICATE_QUESTION_INTERVAL,
|
||||
_FLAGS_QR_QUERY,
|
||||
_LISTENER_TIME,
|
||||
_MDNS_PORT,
|
||||
_TYPE_A,
|
||||
_TYPE_AAAA,
|
||||
_TYPE_NSEC,
|
||||
_TYPE_PTR,
|
||||
_TYPE_SRV,
|
||||
_TYPE_TXT,
|
||||
)
|
||||
|
||||
IPADDRESS_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0)
|
||||
|
||||
_IPVersion_All_value = IPVersion.All.value
|
||||
_IPVersion_V4Only_value = IPVersion.V4Only.value
|
||||
# https://datatracker.ietf.org/doc/html/rfc6762#section-5.2
|
||||
# The most common case for calling ServiceInfo is from a
|
||||
# ServiceBrowser. After the first request we add a few random
|
||||
# milliseconds to the delay between requests to reduce the chance
|
||||
# that there are multiple ServiceBrowser callbacks running on
|
||||
# the network that are firing at the same time when they
|
||||
# see the same multicast response and decide to refresh
|
||||
# the A/AAAA/SRV records for a host.
|
||||
_AVOID_SYNC_DELAY_RANDOM_INTERVAL = (20, 120)
|
||||
|
||||
bytes_ = bytes
|
||||
float_ = float
|
||||
int_ = int
|
||||
str_ = str
|
||||
|
||||
QU_QUESTION = DNSQuestionType.QU
|
||||
QM_QUESTION = DNSQuestionType.QM
|
||||
|
||||
randint = random.randint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._core import Zeroconf
|
||||
|
||||
|
||||
def instance_name_from_service_info(info: "ServiceInfo", strict: bool = True) -> str:
|
||||
"""Calculate the instance name from the ServiceInfo."""
|
||||
# This is kind of funky because of the subtype based tests
|
||||
# need to make subtypes a first class citizen
|
||||
service_name = service_type_name(info.name, strict=strict)
|
||||
if not info.type.endswith(service_name):
|
||||
raise BadTypeInNameException
|
||||
return info.name[: -len(service_name) - 1]
|
||||
|
||||
|
||||
class ServiceInfo(RecordUpdateListener):
|
||||
"""Service information.
|
||||
|
||||
Constructor parameters are as follows:
|
||||
|
||||
* `type_`: fully qualified service type name
|
||||
* `name`: fully qualified service name
|
||||
* `port`: port that the service runs on
|
||||
* `weight`: weight of the service
|
||||
* `priority`: priority of the service
|
||||
* `properties`: dictionary of properties (or a bytes object holding the contents of the `text` field).
|
||||
converted to str and then encoded to bytes using UTF-8. Keys with `None` values are converted to
|
||||
value-less attributes.
|
||||
* `server`: fully qualified name for service host (defaults to name)
|
||||
* `host_ttl`: ttl used for A/SRV records
|
||||
* `other_ttl`: ttl used for PTR/TXT records
|
||||
* `addresses` and `parsed_addresses`: List of IP addresses (either as bytes, network byte order,
|
||||
or in parsed form as text; at most one of those parameters can be provided)
|
||||
* interface_index: scope_id or zone_id for IPv6 link-local addresses i.e. an identifier of the interface
|
||||
where the peer is connected to
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"text",
|
||||
"type",
|
||||
"_name",
|
||||
"key",
|
||||
"_ipv4_addresses",
|
||||
"_ipv6_addresses",
|
||||
"port",
|
||||
"weight",
|
||||
"priority",
|
||||
"server",
|
||||
"server_key",
|
||||
"_properties",
|
||||
"_decoded_properties",
|
||||
"host_ttl",
|
||||
"other_ttl",
|
||||
"interface_index",
|
||||
"_new_records_futures",
|
||||
"_dns_pointer_cache",
|
||||
"_dns_service_cache",
|
||||
"_dns_text_cache",
|
||||
"_dns_address_cache",
|
||||
"_get_address_and_nsec_records_cache",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type_: str,
|
||||
name: str,
|
||||
port: Optional[int] = None,
|
||||
weight: int = 0,
|
||||
priority: int = 0,
|
||||
properties: Union[bytes, Dict] = b'',
|
||||
server: Optional[str] = None,
|
||||
host_ttl: int = _DNS_HOST_TTL,
|
||||
other_ttl: int = _DNS_OTHER_TTL,
|
||||
*,
|
||||
addresses: Optional[List[bytes]] = None,
|
||||
parsed_addresses: Optional[List[str]] = None,
|
||||
interface_index: Optional[int] = None,
|
||||
) -> None:
|
||||
# Accept both none, or one, but not both.
|
||||
if addresses is not None and parsed_addresses is not None:
|
||||
raise TypeError("addresses and parsed_addresses cannot be provided together")
|
||||
if not type_.endswith(service_type_name(name, strict=False)):
|
||||
raise BadTypeInNameException
|
||||
self.interface_index = interface_index
|
||||
self.text = b''
|
||||
self.type = type_
|
||||
self._name = name
|
||||
self.key = name.lower()
|
||||
self._ipv4_addresses: List[IPv4Address] = []
|
||||
self._ipv6_addresses: List[IPv6Address] = []
|
||||
if addresses is not None:
|
||||
self.addresses = addresses
|
||||
elif parsed_addresses is not None:
|
||||
self.addresses = [_encode_address(a) for a in parsed_addresses]
|
||||
self.port = port
|
||||
self.weight = weight
|
||||
self.priority = priority
|
||||
self.server = server if server else None
|
||||
self.server_key = server.lower() if server else None
|
||||
self._properties: Optional[Dict[bytes, Optional[bytes]]] = None
|
||||
self._decoded_properties: Optional[Dict[str, Optional[str]]] = None
|
||||
if isinstance(properties, bytes):
|
||||
self._set_text(properties)
|
||||
else:
|
||||
self._set_properties(properties)
|
||||
self.host_ttl = host_ttl
|
||||
self.other_ttl = other_ttl
|
||||
self._new_records_futures: Optional[Set[asyncio.Future]] = None
|
||||
self._dns_address_cache: Optional[List[DNSAddress]] = None
|
||||
self._dns_pointer_cache: Optional[DNSPointer] = None
|
||||
self._dns_service_cache: Optional[DNSService] = None
|
||||
self._dns_text_cache: Optional[DNSText] = None
|
||||
self._get_address_and_nsec_records_cache: Optional[Set[DNSRecord]] = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name of the service."""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name: str) -> None:
|
||||
"""Replace the the name and reset the key."""
|
||||
self._name = name
|
||||
self.key = name.lower()
|
||||
self._dns_service_cache = None
|
||||
self._dns_pointer_cache = None
|
||||
self._dns_text_cache = None
|
||||
|
||||
@property
|
||||
def addresses(self) -> List[bytes]:
|
||||
"""IPv4 addresses of this service.
|
||||
|
||||
Only IPv4 addresses are returned for backward compatibility.
|
||||
Use :meth:`addresses_by_version` or :meth:`parsed_addresses` to
|
||||
include IPv6 addresses as well.
|
||||
"""
|
||||
return self.addresses_by_version(IPVersion.V4Only)
|
||||
|
||||
@addresses.setter
|
||||
def addresses(self, value: List[bytes]) -> None:
|
||||
"""Replace the addresses list.
|
||||
|
||||
This replaces all currently stored addresses, both IPv4 and IPv6.
|
||||
"""
|
||||
self._ipv4_addresses.clear()
|
||||
self._ipv6_addresses.clear()
|
||||
self._dns_address_cache = None
|
||||
self._get_address_and_nsec_records_cache = None
|
||||
|
||||
for address in value:
|
||||
if IPADDRESS_SUPPORTS_SCOPE_ID and len(address) == 16 and self.interface_index is not None:
|
||||
addr = ip_bytes_and_scope_to_address(address, self.interface_index)
|
||||
else:
|
||||
addr = cached_ip_addresses(address)
|
||||
if addr is None:
|
||||
raise TypeError(
|
||||
"Addresses must either be IPv4 or IPv6 strings, bytes, or integers;"
|
||||
f" got {address!r}. Hint: convert string addresses with socket.inet_pton"
|
||||
)
|
||||
if addr.version == 4:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(addr, IPv4Address)
|
||||
self._ipv4_addresses.append(addr)
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(addr, IPv6Address)
|
||||
self._ipv6_addresses.append(addr)
|
||||
|
||||
@property
|
||||
def properties(self) -> Dict[bytes, Optional[bytes]]:
|
||||
"""Return properties as bytes."""
|
||||
if self._properties is None:
|
||||
self._unpack_text_into_properties()
|
||||
if TYPE_CHECKING:
|
||||
assert self._properties is not None
|
||||
return self._properties
|
||||
|
||||
@property
|
||||
def decoded_properties(self) -> Dict[str, Optional[str]]:
|
||||
"""Return properties as strings."""
|
||||
if self._decoded_properties is None:
|
||||
self._generate_decoded_properties()
|
||||
if TYPE_CHECKING:
|
||||
assert self._decoded_properties is not None
|
||||
return self._decoded_properties
|
||||
|
||||
def async_clear_cache(self) -> None:
|
||||
"""Clear the cache for this service info."""
|
||||
self._dns_address_cache = None
|
||||
self._dns_pointer_cache = None
|
||||
self._dns_service_cache = None
|
||||
self._dns_text_cache = None
|
||||
self._get_address_and_nsec_records_cache = None
|
||||
|
||||
async def async_wait(self, timeout: float, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
||||
"""Calling task waits for a given number of milliseconds or until notified."""
|
||||
if not self._new_records_futures:
|
||||
self._new_records_futures = set()
|
||||
await wait_for_future_set_or_timeout(
|
||||
loop or asyncio.get_running_loop(), self._new_records_futures, timeout
|
||||
)
|
||||
|
||||
def addresses_by_version(self, version: IPVersion) -> List[bytes]:
|
||||
"""List addresses matching IP version.
|
||||
|
||||
Addresses are guaranteed to be returned in LIFO (last in, first out)
|
||||
order with IPv4 addresses first and IPv6 addresses second.
|
||||
|
||||
This means the first address will always be the most recently added
|
||||
address of the given IP version.
|
||||
"""
|
||||
version_value = version.value
|
||||
if version_value == _IPVersion_All_value:
|
||||
ip_v4_packed = [addr.packed for addr in self._ipv4_addresses]
|
||||
ip_v6_packed = [addr.packed for addr in self._ipv6_addresses]
|
||||
return [*ip_v4_packed, *ip_v6_packed]
|
||||
if version_value == _IPVersion_V4Only_value:
|
||||
return [addr.packed for addr in self._ipv4_addresses]
|
||||
return [addr.packed for addr in self._ipv6_addresses]
|
||||
|
||||
def ip_addresses_by_version(
|
||||
self, version: IPVersion
|
||||
) -> Union[List[IPv4Address], List[IPv6Address], List[_BaseAddress]]:
|
||||
"""List ip_address objects matching IP version.
|
||||
|
||||
Addresses are guaranteed to be returned in LIFO (last in, first out)
|
||||
order with IPv4 addresses first and IPv6 addresses second.
|
||||
|
||||
This means the first address will always be the most recently added
|
||||
address of the given IP version.
|
||||
"""
|
||||
return self._ip_addresses_by_version_value(version.value)
|
||||
|
||||
def _ip_addresses_by_version_value(
|
||||
self, version_value: int_
|
||||
) -> Union[List[IPv4Address], List[IPv6Address]]:
|
||||
"""Backend for addresses_by_version that uses the raw value."""
|
||||
if version_value == _IPVersion_All_value:
|
||||
return [*self._ipv4_addresses, *self._ipv6_addresses] # type: ignore[return-value]
|
||||
if version_value == _IPVersion_V4Only_value:
|
||||
return self._ipv4_addresses
|
||||
return self._ipv6_addresses
|
||||
|
||||
def parsed_addresses(self, version: IPVersion = IPVersion.All) -> List[str]:
|
||||
"""List addresses in their parsed string form.
|
||||
|
||||
Addresses are guaranteed to be returned in LIFO (last in, first out)
|
||||
order with IPv4 addresses first and IPv6 addresses second.
|
||||
|
||||
This means the first address will always be the most recently added
|
||||
address of the given IP version.
|
||||
"""
|
||||
return [str_without_scope_id(addr) for addr in self._ip_addresses_by_version_value(version.value)]
|
||||
|
||||
def parsed_scoped_addresses(self, version: IPVersion = IPVersion.All) -> List[str]:
|
||||
"""Equivalent to parsed_addresses, with the exception that IPv6 Link-Local
|
||||
addresses are qualified with %<interface_index> when available
|
||||
|
||||
Addresses are guaranteed to be returned in LIFO (last in, first out)
|
||||
order with IPv4 addresses first and IPv6 addresses second.
|
||||
|
||||
This means the first address will always be the most recently added
|
||||
address of the given IP version.
|
||||
"""
|
||||
return [str(addr) for addr in self._ip_addresses_by_version_value(version.value)]
|
||||
|
||||
def _set_properties(self, properties: Dict[Union[str, bytes], Optional[Union[str, bytes]]]) -> None:
|
||||
"""Sets properties and text of this info from a dictionary"""
|
||||
list_: List[bytes] = []
|
||||
properties_contain_str = False
|
||||
result = b''
|
||||
for key, value in properties.items():
|
||||
if isinstance(key, str):
|
||||
key = key.encode('utf-8')
|
||||
properties_contain_str = True
|
||||
|
||||
record = key
|
||||
if value is not None:
|
||||
if not isinstance(value, bytes):
|
||||
value = str(value).encode('utf-8')
|
||||
properties_contain_str = True
|
||||
record += b'=' + value
|
||||
list_.append(record)
|
||||
for item in list_:
|
||||
result = b''.join((result, bytes((len(item),)), item))
|
||||
if not properties_contain_str:
|
||||
# If there are no str keys or values, we can use the properties
|
||||
# as-is, without decoding them, otherwise calling
|
||||
# self.properties will lazy decode them, which is expensive.
|
||||
if TYPE_CHECKING:
|
||||
self._properties = cast("Dict[bytes, Optional[bytes]]", properties)
|
||||
else:
|
||||
self._properties = properties
|
||||
self.text = result
|
||||
|
||||
def _set_text(self, text: bytes) -> None:
|
||||
"""Sets properties and text given a text field"""
|
||||
if text == self.text:
|
||||
return
|
||||
self.text = text
|
||||
# Clear the properties cache
|
||||
self._properties = None
|
||||
self._decoded_properties = None
|
||||
|
||||
def _generate_decoded_properties(self) -> None:
|
||||
"""Generates decoded properties from the properties"""
|
||||
self._decoded_properties = {
|
||||
k.decode("ascii", "replace"): None if v is None else v.decode("utf-8", "replace")
|
||||
for k, v in self.properties.items()
|
||||
}
|
||||
|
||||
def _unpack_text_into_properties(self) -> None:
|
||||
"""Unpacks the text field into properties"""
|
||||
text = self.text
|
||||
end = len(text)
|
||||
if end == 0:
|
||||
# Properties should be set atomically
|
||||
# in case another thread is reading them
|
||||
self._properties = {}
|
||||
return
|
||||
|
||||
index = 0
|
||||
properties: Dict[bytes, Optional[bytes]] = {}
|
||||
while index < end:
|
||||
length = text[index]
|
||||
index += 1
|
||||
key_value = text[index : index + length]
|
||||
key_sep_value = key_value.partition(b'=')
|
||||
key = key_sep_value[0]
|
||||
if key not in properties:
|
||||
properties[key] = key_sep_value[2] or None
|
||||
index += length
|
||||
|
||||
self._properties = properties
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Name accessor"""
|
||||
return self._name[: len(self._name) - len(self.type) - 1]
|
||||
|
||||
def _get_ip_addresses_from_cache_lifo(
|
||||
self, zc: 'Zeroconf', now: float_, type: int_
|
||||
) -> List[Union[IPv4Address, IPv6Address]]:
|
||||
"""Set IPv6 addresses from the cache."""
|
||||
address_list: List[Union[IPv4Address, IPv6Address]] = []
|
||||
for record in self._get_address_records_from_cache_by_type(zc, type):
|
||||
if record.is_expired(now):
|
||||
continue
|
||||
ip_addr = get_ip_address_object_from_record(record)
|
||||
if ip_addr is not None and ip_addr not in address_list:
|
||||
address_list.append(ip_addr)
|
||||
address_list.reverse() # Reverse to get LIFO order
|
||||
return address_list
|
||||
|
||||
def _set_ipv6_addresses_from_cache(self, zc: 'Zeroconf', now: float_) -> None:
|
||||
"""Set IPv6 addresses from the cache."""
|
||||
if TYPE_CHECKING:
|
||||
self._ipv6_addresses = cast(
|
||||
"List[IPv6Address]", self._get_ip_addresses_from_cache_lifo(zc, now, _TYPE_AAAA)
|
||||
)
|
||||
else:
|
||||
self._ipv6_addresses = self._get_ip_addresses_from_cache_lifo(zc, now, _TYPE_AAAA)
|
||||
|
||||
def _set_ipv4_addresses_from_cache(self, zc: 'Zeroconf', now: float_) -> None:
|
||||
"""Set IPv4 addresses from the cache."""
|
||||
if TYPE_CHECKING:
|
||||
self._ipv4_addresses = cast(
|
||||
"List[IPv4Address]", self._get_ip_addresses_from_cache_lifo(zc, now, _TYPE_A)
|
||||
)
|
||||
else:
|
||||
self._ipv4_addresses = self._get_ip_addresses_from_cache_lifo(zc, now, _TYPE_A)
|
||||
|
||||
def async_update_records(self, zc: 'Zeroconf', now: float_, records: List[RecordUpdate]) -> None:
|
||||
"""Updates service information from a DNS record.
|
||||
|
||||
This method will be run in the event loop.
|
||||
"""
|
||||
new_records_futures = self._new_records_futures
|
||||
updated: bool = False
|
||||
for record_update in records:
|
||||
updated |= self._process_record_threadsafe(zc, record_update.new, now)
|
||||
if updated and new_records_futures:
|
||||
_resolve_all_futures_to_none(new_records_futures)
|
||||
|
||||
def _process_record_threadsafe(self, zc: 'Zeroconf', record: DNSRecord, now: float_) -> bool:
|
||||
"""Thread safe record updating.
|
||||
|
||||
Returns True if a new record was added.
|
||||
"""
|
||||
if record.is_expired(now):
|
||||
return False
|
||||
|
||||
record_key = record.key
|
||||
record_type = type(record)
|
||||
if record_type is DNSAddress and record_key == self.server_key:
|
||||
dns_address_record = record
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(dns_address_record, DNSAddress)
|
||||
ip_addr = get_ip_address_object_from_record(dns_address_record)
|
||||
if ip_addr is None:
|
||||
log.warning(
|
||||
"Encountered invalid address while processing %s: %s",
|
||||
dns_address_record,
|
||||
dns_address_record.address,
|
||||
)
|
||||
return False
|
||||
|
||||
if ip_addr.version == 4:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(ip_addr, IPv4Address)
|
||||
ipv4_addresses = self._ipv4_addresses
|
||||
if ip_addr not in ipv4_addresses:
|
||||
ipv4_addresses.insert(0, ip_addr)
|
||||
return True
|
||||
elif ip_addr != ipv4_addresses[0]:
|
||||
ipv4_addresses.remove(ip_addr)
|
||||
ipv4_addresses.insert(0, ip_addr)
|
||||
|
||||
return False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(ip_addr, IPv6Address)
|
||||
ipv6_addresses = self._ipv6_addresses
|
||||
if ip_addr not in self._ipv6_addresses:
|
||||
ipv6_addresses.insert(0, ip_addr)
|
||||
return True
|
||||
elif ip_addr != self._ipv6_addresses[0]:
|
||||
ipv6_addresses.remove(ip_addr)
|
||||
ipv6_addresses.insert(0, ip_addr)
|
||||
|
||||
return False
|
||||
|
||||
if record_key != self.key:
|
||||
return False
|
||||
|
||||
if record_type is DNSText:
|
||||
dns_text_record = record
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(dns_text_record, DNSText)
|
||||
self._set_text(dns_text_record.text)
|
||||
return True
|
||||
|
||||
if record_type is DNSService:
|
||||
dns_service_record = record
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(dns_service_record, DNSService)
|
||||
old_server_key = self.server_key
|
||||
self._name = dns_service_record.name
|
||||
self.key = dns_service_record.key
|
||||
self.server = dns_service_record.server
|
||||
self.server_key = dns_service_record.server_key
|
||||
self.port = dns_service_record.port
|
||||
self.weight = dns_service_record.weight
|
||||
self.priority = dns_service_record.priority
|
||||
if old_server_key != self.server_key:
|
||||
self._set_ipv4_addresses_from_cache(zc, now)
|
||||
self._set_ipv6_addresses_from_cache(zc, now)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dns_addresses(
|
||||
self,
|
||||
override_ttl: Optional[int] = None,
|
||||
version: IPVersion = IPVersion.All,
|
||||
) -> List[DNSAddress]:
|
||||
"""Return matching DNSAddress from ServiceInfo."""
|
||||
return self._dns_addresses(override_ttl, version)
|
||||
|
||||
def _dns_addresses(
|
||||
self,
|
||||
override_ttl: Optional[int],
|
||||
version: IPVersion,
|
||||
) -> List[DNSAddress]:
|
||||
"""Return matching DNSAddress from ServiceInfo."""
|
||||
cacheable = version is IPVersion.All and override_ttl is None
|
||||
if self._dns_address_cache is not None and cacheable:
|
||||
return self._dns_address_cache
|
||||
name = self.server or self._name
|
||||
ttl = override_ttl if override_ttl is not None else self.host_ttl
|
||||
class_ = _CLASS_IN_UNIQUE
|
||||
version_value = version.value
|
||||
records = [
|
||||
DNSAddress(
|
||||
name,
|
||||
_TYPE_AAAA if ip_addr.version == 6 else _TYPE_A,
|
||||
class_,
|
||||
ttl,
|
||||
ip_addr.packed,
|
||||
created=0.0,
|
||||
)
|
||||
for ip_addr in self._ip_addresses_by_version_value(version_value)
|
||||
]
|
||||
if cacheable:
|
||||
self._dns_address_cache = records
|
||||
return records
|
||||
|
||||
def dns_pointer(self, override_ttl: Optional[int] = None) -> DNSPointer:
|
||||
"""Return DNSPointer from ServiceInfo."""
|
||||
return self._dns_pointer(override_ttl)
|
||||
|
||||
def _dns_pointer(self, override_ttl: Optional[int]) -> DNSPointer:
|
||||
"""Return DNSPointer from ServiceInfo."""
|
||||
cacheable = override_ttl is None
|
||||
if self._dns_pointer_cache is not None and cacheable:
|
||||
return self._dns_pointer_cache
|
||||
record = DNSPointer(
|
||||
self.type,
|
||||
_TYPE_PTR,
|
||||
_CLASS_IN,
|
||||
override_ttl if override_ttl is not None else self.other_ttl,
|
||||
self._name,
|
||||
0.0,
|
||||
)
|
||||
if cacheable:
|
||||
self._dns_pointer_cache = record
|
||||
return record
|
||||
|
||||
def dns_service(self, override_ttl: Optional[int] = None) -> DNSService:
|
||||
"""Return DNSService from ServiceInfo."""
|
||||
return self._dns_service(override_ttl)
|
||||
|
||||
def _dns_service(self, override_ttl: Optional[int]) -> DNSService:
|
||||
"""Return DNSService from ServiceInfo."""
|
||||
cacheable = override_ttl is None
|
||||
if self._dns_service_cache is not None and cacheable:
|
||||
return self._dns_service_cache
|
||||
port = self.port
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(port, int)
|
||||
record = DNSService(
|
||||
self._name,
|
||||
_TYPE_SRV,
|
||||
_CLASS_IN_UNIQUE,
|
||||
override_ttl if override_ttl is not None else self.host_ttl,
|
||||
self.priority,
|
||||
self.weight,
|
||||
port,
|
||||
self.server or self._name,
|
||||
0.0,
|
||||
)
|
||||
if cacheable:
|
||||
self._dns_service_cache = record
|
||||
return record
|
||||
|
||||
def dns_text(self, override_ttl: Optional[int] = None) -> DNSText:
|
||||
"""Return DNSText from ServiceInfo."""
|
||||
return self._dns_text(override_ttl)
|
||||
|
||||
def _dns_text(self, override_ttl: Optional[int]) -> DNSText:
|
||||
"""Return DNSText from ServiceInfo."""
|
||||
cacheable = override_ttl is None
|
||||
if self._dns_text_cache is not None and cacheable:
|
||||
return self._dns_text_cache
|
||||
record = DNSText(
|
||||
self._name,
|
||||
_TYPE_TXT,
|
||||
_CLASS_IN_UNIQUE,
|
||||
override_ttl if override_ttl is not None else self.other_ttl,
|
||||
self.text,
|
||||
0.0,
|
||||
)
|
||||
if cacheable:
|
||||
self._dns_text_cache = record
|
||||
return record
|
||||
|
||||
def dns_nsec(self, missing_types: List[int], override_ttl: Optional[int] = None) -> DNSNsec:
|
||||
"""Return DNSNsec from ServiceInfo."""
|
||||
return self._dns_nsec(missing_types, override_ttl)
|
||||
|
||||
def _dns_nsec(self, missing_types: List[int], override_ttl: Optional[int]) -> DNSNsec:
|
||||
"""Return DNSNsec from ServiceInfo."""
|
||||
return DNSNsec(
|
||||
self._name,
|
||||
_TYPE_NSEC,
|
||||
_CLASS_IN_UNIQUE,
|
||||
override_ttl if override_ttl is not None else self.host_ttl,
|
||||
self._name,
|
||||
missing_types,
|
||||
0.0,
|
||||
)
|
||||
|
||||
def get_address_and_nsec_records(self, override_ttl: Optional[int] = None) -> Set[DNSRecord]:
|
||||
"""Build a set of address records and NSEC records for non-present record types."""
|
||||
return self._get_address_and_nsec_records(override_ttl)
|
||||
|
||||
def _get_address_and_nsec_records(self, override_ttl: Optional[int]) -> Set[DNSRecord]:
|
||||
"""Build a set of address records and NSEC records for non-present record types."""
|
||||
cacheable = override_ttl is None
|
||||
if self._get_address_and_nsec_records_cache is not None and cacheable:
|
||||
return self._get_address_and_nsec_records_cache
|
||||
missing_types: Set[int] = _ADDRESS_RECORD_TYPES.copy()
|
||||
records: Set[DNSRecord] = set()
|
||||
for dns_address in self._dns_addresses(override_ttl, IPVersion.All):
|
||||
missing_types.discard(dns_address.type)
|
||||
records.add(dns_address)
|
||||
if missing_types:
|
||||
assert self.server is not None, "Service server must be set for NSEC record."
|
||||
records.add(self._dns_nsec(list(missing_types), override_ttl))
|
||||
if cacheable:
|
||||
self._get_address_and_nsec_records_cache = records
|
||||
return records
|
||||
|
||||
def _get_address_records_from_cache_by_type(self, zc: 'Zeroconf', _type: int_) -> List[DNSAddress]:
|
||||
"""Get the addresses from the cache."""
|
||||
if self.server_key is None:
|
||||
return []
|
||||
cache = zc.cache
|
||||
if TYPE_CHECKING:
|
||||
records = cast("List[DNSAddress]", cache.get_all_by_details(self.server_key, _type, _CLASS_IN))
|
||||
else:
|
||||
records = cache.get_all_by_details(self.server_key, _type, _CLASS_IN)
|
||||
return records
|
||||
|
||||
def set_server_if_missing(self) -> None:
|
||||
"""Set the server if it is missing.
|
||||
|
||||
This function is for backwards compatibility.
|
||||
"""
|
||||
if self.server is None:
|
||||
self.server = self._name
|
||||
self.server_key = self.key
|
||||
|
||||
def load_from_cache(self, zc: 'Zeroconf', now: Optional[float_] = None) -> bool:
|
||||
"""Populate the service info from the cache.
|
||||
|
||||
This method is designed to be threadsafe.
|
||||
"""
|
||||
return self._load_from_cache(zc, now or current_time_millis())
|
||||
|
||||
def _load_from_cache(self, zc: 'Zeroconf', now: float_) -> bool:
|
||||
"""Populate the service info from the cache.
|
||||
|
||||
This method is designed to be threadsafe.
|
||||
"""
|
||||
cache = zc.cache
|
||||
original_server_key = self.server_key
|
||||
cached_srv_record = cache.get_by_details(self._name, _TYPE_SRV, _CLASS_IN)
|
||||
if cached_srv_record:
|
||||
self._process_record_threadsafe(zc, cached_srv_record, now)
|
||||
cached_txt_record = cache.get_by_details(self._name, _TYPE_TXT, _CLASS_IN)
|
||||
if cached_txt_record:
|
||||
self._process_record_threadsafe(zc, cached_txt_record, now)
|
||||
if original_server_key == self.server_key:
|
||||
# If there is a srv which changes the server_key,
|
||||
# A and AAAA will already be loaded from the cache
|
||||
# and we do not want to do it twice
|
||||
for record in self._get_address_records_from_cache_by_type(zc, _TYPE_A):
|
||||
self._process_record_threadsafe(zc, record, now)
|
||||
for record in self._get_address_records_from_cache_by_type(zc, _TYPE_AAAA):
|
||||
self._process_record_threadsafe(zc, record, now)
|
||||
return self._is_complete
|
||||
|
||||
@property
|
||||
def _is_complete(self) -> bool:
|
||||
"""The ServiceInfo has all expected properties."""
|
||||
return bool(self.text is not None and (self._ipv4_addresses or self._ipv6_addresses))
|
||||
|
||||
def request(
|
||||
self,
|
||||
zc: 'Zeroconf',
|
||||
timeout: float,
|
||||
question_type: Optional[DNSQuestionType] = None,
|
||||
addr: Optional[str] = None,
|
||||
port: int = _MDNS_PORT,
|
||||
) -> 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():
|
||||
raise RuntimeError("Use AsyncServiceInfo.async_request from the event loop")
|
||||
return bool(
|
||||
run_coro_with_timeout(
|
||||
self.async_request(zc, timeout, question_type, addr, port), zc.loop, timeout
|
||||
)
|
||||
)
|
||||
|
||||
def _get_initial_delay(self) -> float_:
|
||||
return _LISTENER_TIME
|
||||
|
||||
def _get_random_delay(self) -> int_:
|
||||
return randint(*_AVOID_SYNC_DELAY_RANDOM_INTERVAL)
|
||||
|
||||
async def async_request(
|
||||
self,
|
||||
zc: 'Zeroconf',
|
||||
timeout: float,
|
||||
question_type: Optional[DNSQuestionType] = None,
|
||||
addr: Optional[str] = None,
|
||||
port: int = _MDNS_PORT,
|
||||
) -> bool:
|
||||
"""Returns true if the service could be discovered on the
|
||||
network, and updates this object with details discovered.
|
||||
|
||||
This method will be run in the event loop.
|
||||
|
||||
Passing addr and port is optional, and will default to the
|
||||
mDNS multicast address and port. This is useful for directing
|
||||
requests to a specific host that may be able to respond across
|
||||
subnets.
|
||||
"""
|
||||
if not zc.started:
|
||||
await zc.async_wait_for_start()
|
||||
|
||||
now = current_time_millis()
|
||||
|
||||
if self._load_from_cache(zc, now):
|
||||
return True
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert zc.loop is not None
|
||||
|
||||
first_request = True
|
||||
delay = self._get_initial_delay()
|
||||
next_ = now
|
||||
last = now + timeout
|
||||
try:
|
||||
zc.async_add_listener(self, None)
|
||||
while not self._is_complete:
|
||||
if last <= now:
|
||||
return False
|
||||
if next_ <= now:
|
||||
this_question_type = question_type or QU_QUESTION if first_request else QM_QUESTION
|
||||
out = self._generate_request_query(zc, now, this_question_type)
|
||||
first_request = False
|
||||
if out.questions:
|
||||
# All questions may have been suppressed
|
||||
# by the question history, so nothing to send,
|
||||
# but keep waiting for answers in case another
|
||||
# client on the network is asking the same
|
||||
# question or they have not arrived yet.
|
||||
zc.async_send(out, addr, port)
|
||||
next_ = now + delay
|
||||
next_ += self._get_random_delay()
|
||||
if this_question_type is QM_QUESTION and delay < _DUPLICATE_QUESTION_INTERVAL:
|
||||
# If we just asked a QM question, we need to
|
||||
# wait at least the duplicate question interval
|
||||
# before asking another QM question otherwise
|
||||
# its likely to be suppressed by the question
|
||||
# history of the remote responder.
|
||||
delay = _DUPLICATE_QUESTION_INTERVAL
|
||||
|
||||
await self.async_wait(min(next_, last) - now, zc.loop)
|
||||
now = current_time_millis()
|
||||
finally:
|
||||
zc.async_remove_listener(self)
|
||||
|
||||
return True
|
||||
|
||||
def _add_question_with_known_answers(
|
||||
self,
|
||||
out: DNSOutgoing,
|
||||
qu_question: bool,
|
||||
question_history: QuestionHistory,
|
||||
cache: DNSCache,
|
||||
now: float_,
|
||||
name: str_,
|
||||
type_: int_,
|
||||
class_: int_,
|
||||
skip_if_known_answers: bool,
|
||||
) -> None:
|
||||
"""Add a question with known answers if its not suppressed."""
|
||||
known_answers = {
|
||||
answer for answer in cache.get_all_by_details(name, type_, class_) if not answer.is_stale(now)
|
||||
}
|
||||
if skip_if_known_answers and known_answers:
|
||||
return
|
||||
question = DNSQuestion(name, type_, class_)
|
||||
if qu_question:
|
||||
question.unicast = True
|
||||
elif question_history.suppresses(question, now, known_answers):
|
||||
return
|
||||
else:
|
||||
question_history.add_question_at_time(question, now, known_answers)
|
||||
out.add_question(question)
|
||||
for answer in known_answers:
|
||||
out.add_answer_at_time(answer, now)
|
||||
|
||||
def _generate_request_query(
|
||||
self, zc: 'Zeroconf', now: float_, question_type: DNSQuestionType
|
||||
) -> DNSOutgoing:
|
||||
"""Generate the request query."""
|
||||
out = DNSOutgoing(_FLAGS_QR_QUERY)
|
||||
name = self._name
|
||||
server = self.server or name
|
||||
cache = zc.cache
|
||||
history = zc.question_history
|
||||
qu_question = question_type is QU_QUESTION
|
||||
self._add_question_with_known_answers(
|
||||
out, qu_question, history, cache, now, name, _TYPE_SRV, _CLASS_IN, True
|
||||
)
|
||||
self._add_question_with_known_answers(
|
||||
out, qu_question, history, cache, now, name, _TYPE_TXT, _CLASS_IN, True
|
||||
)
|
||||
self._add_question_with_known_answers(
|
||||
out, qu_question, history, cache, now, server, _TYPE_A, _CLASS_IN, False
|
||||
)
|
||||
self._add_question_with_known_answers(
|
||||
out, qu_question, history, cache, now, server, _TYPE_AAAA, _CLASS_IN, False
|
||||
)
|
||||
return out
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation"""
|
||||
return '{}({})'.format(
|
||||
type(self).__name__,
|
||||
', '.join(
|
||||
f'{name}={getattr(self, name)!r}'
|
||||
for name in (
|
||||
'type',
|
||||
'name',
|
||||
'addresses',
|
||||
'port',
|
||||
'weight',
|
||||
'priority',
|
||||
'server',
|
||||
'properties',
|
||||
'interface_index',
|
||||
)
|
||||
),
|
||||
)
|
BIN
resources/lib/deps/zeroconf/_services/registry.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
resources/lib/deps/zeroconf/_services/registry.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
33
resources/lib/deps/zeroconf/_services/registry.pxd
Normal file
33
resources/lib/deps/zeroconf/_services/registry.pxd
Normal file
|
@ -0,0 +1,33 @@
|
|||
|
||||
import cython
|
||||
|
||||
from .info cimport ServiceInfo
|
||||
|
||||
|
||||
cdef class ServiceRegistry:
|
||||
|
||||
cdef cython.dict _services
|
||||
cdef public cython.dict types
|
||||
cdef public cython.dict servers
|
||||
cdef public bint has_entries
|
||||
|
||||
@cython.locals(
|
||||
record_list=cython.list,
|
||||
)
|
||||
cdef cython.list _async_get_by_index(self, cython.dict records, str key)
|
||||
|
||||
cdef _add(self, ServiceInfo info)
|
||||
|
||||
@cython.locals(
|
||||
info=ServiceInfo,
|
||||
old_service_info=ServiceInfo
|
||||
)
|
||||
cdef _remove(self, cython.list infos)
|
||||
|
||||
cpdef ServiceInfo async_get_info_name(self, str name)
|
||||
|
||||
cpdef cython.list async_get_types(self)
|
||||
|
||||
cpdef cython.list async_get_infos_type(self, str type_)
|
||||
|
||||
cpdef cython.list async_get_infos_server(self, str server)
|
112
resources/lib/deps/zeroconf/_services/registry.py
Normal file
112
resources/lib/deps/zeroconf/_services/registry.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from .._exceptions import ServiceNameAlreadyRegistered
|
||||
from .info import ServiceInfo
|
||||
|
||||
_str = str
|
||||
|
||||
|
||||
class ServiceRegistry:
|
||||
"""A registry to keep track of services.
|
||||
|
||||
The registry must only be accessed from
|
||||
the event loop as it is not thread safe.
|
||||
"""
|
||||
|
||||
__slots__ = ("_services", "types", "servers", "has_entries")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
) -> None:
|
||||
"""Create the ServiceRegistry class."""
|
||||
self._services: Dict[str, ServiceInfo] = {}
|
||||
self.types: Dict[str, List] = {}
|
||||
self.servers: Dict[str, List] = {}
|
||||
self.has_entries: bool = False
|
||||
|
||||
def async_add(self, info: ServiceInfo) -> None:
|
||||
"""Add a new service to the registry."""
|
||||
self._add(info)
|
||||
|
||||
def async_remove(self, info: Union[List[ServiceInfo], ServiceInfo]) -> None:
|
||||
"""Remove a new service from the registry."""
|
||||
self._remove(info if isinstance(info, list) else [info])
|
||||
|
||||
def async_update(self, info: ServiceInfo) -> None:
|
||||
"""Update new service in the registry."""
|
||||
self._remove([info])
|
||||
self._add(info)
|
||||
|
||||
def async_get_service_infos(self) -> List[ServiceInfo]:
|
||||
"""Return all ServiceInfo."""
|
||||
return list(self._services.values())
|
||||
|
||||
def async_get_info_name(self, name: str) -> Optional[ServiceInfo]:
|
||||
"""Return all ServiceInfo for the name."""
|
||||
return self._services.get(name)
|
||||
|
||||
def async_get_types(self) -> List[str]:
|
||||
"""Return all types."""
|
||||
return list(self.types)
|
||||
|
||||
def async_get_infos_type(self, type_: str) -> List[ServiceInfo]:
|
||||
"""Return all ServiceInfo matching type."""
|
||||
return self._async_get_by_index(self.types, type_)
|
||||
|
||||
def async_get_infos_server(self, server: str) -> List[ServiceInfo]:
|
||||
"""Return all ServiceInfo matching server."""
|
||||
return self._async_get_by_index(self.servers, server)
|
||||
|
||||
def _async_get_by_index(self, records: Dict[str, List], key: _str) -> List[ServiceInfo]:
|
||||
"""Return all ServiceInfo matching the index."""
|
||||
record_list = records.get(key)
|
||||
if record_list is None:
|
||||
return []
|
||||
return [self._services[name] for name in record_list]
|
||||
|
||||
def _add(self, info: ServiceInfo) -> None:
|
||||
"""Add a new service under the lock."""
|
||||
assert info.server_key is not None, "ServiceInfo must have a server"
|
||||
if info.key in self._services:
|
||||
raise ServiceNameAlreadyRegistered
|
||||
|
||||
info.async_clear_cache()
|
||||
self._services[info.key] = info
|
||||
self.types.setdefault(info.type.lower(), []).append(info.key)
|
||||
self.servers.setdefault(info.server_key, []).append(info.key)
|
||||
self.has_entries = True
|
||||
|
||||
def _remove(self, infos: List[ServiceInfo]) -> None:
|
||||
"""Remove a services under the lock."""
|
||||
for info in infos:
|
||||
old_service_info = self._services.get(info.key)
|
||||
if old_service_info is None:
|
||||
continue
|
||||
assert old_service_info.server_key is not None
|
||||
self.types[old_service_info.type.lower()].remove(info.key)
|
||||
self.servers[old_service_info.server_key].remove(info.key)
|
||||
del self._services[info.key]
|
||||
|
||||
self.has_entries = bool(self._services)
|
83
resources/lib/deps/zeroconf/_services/types.py
Normal file
83
resources/lib/deps/zeroconf/_services/types.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
|
||||
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
|
||||
|
||||
This module provides a framework for the use of DNS Service Discovery
|
||||
using IP multicast.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Optional, Set, Tuple, Union
|
||||
|
||||
from .._core import Zeroconf
|
||||
from .._services import ServiceListener
|
||||
from .._utils.net import InterfaceChoice, InterfacesType, IPVersion
|
||||
from ..const import _SERVICE_TYPE_ENUMERATION_NAME
|
||||
from .browser import ServiceBrowser
|
||||
|
||||
|
||||
class ZeroconfServiceTypes(ServiceListener):
|
||||
"""
|
||||
Return all of the advertised services on any local networks
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Keep track of found services in a set."""
|
||||
self.found_services: Set[str] = set()
|
||||
|
||||
def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
||||
"""Service added."""
|
||||
self.found_services.add(name)
|
||||
|
||||
def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
||||
"""Service updated."""
|
||||
|
||||
def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
|
||||
"""Service removed."""
|
||||
|
||||
@classmethod
|
||||
def find(
|
||||
cls,
|
||||
zc: Optional[Zeroconf] = None,
|
||||
timeout: Union[int, float] = 5,
|
||||
interfaces: InterfacesType = InterfaceChoice.All,
|
||||
ip_version: Optional[IPVersion] = None,
|
||||
) -> Tuple[str, ...]:
|
||||
"""
|
||||
Return all of the advertised services on any local networks.
|
||||
|
||||
:param zc: Zeroconf() instance. Pass in if already have an
|
||||
instance running or if non-default interfaces are needed
|
||||
:param timeout: seconds to wait for any responses
|
||||
:param interfaces: interfaces to listen on.
|
||||
:param ip_version: IP protocol version to use.
|
||||
:return: tuple of service type strings
|
||||
"""
|
||||
local_zc = zc or Zeroconf(interfaces=interfaces, ip_version=ip_version)
|
||||
listener = cls()
|
||||
browser = ServiceBrowser(local_zc, _SERVICE_TYPE_ENUMERATION_NAME, listener=listener)
|
||||
|
||||
# wait for responses
|
||||
time.sleep(timeout)
|
||||
|
||||
browser.cancel()
|
||||
|
||||
# close down anything we opened
|
||||
if zc is None:
|
||||
local_zc.close()
|
||||
|
||||
return tuple(sorted(listener.found_services))
|
Loading…
Add table
Add a link
Reference in a new issue