diff --git a/src/zeroconf/_services/info.py b/src/zeroconf/_services/info.py index f7d33b67..8ff1f665 100644 --- a/src/zeroconf/_services/info.py +++ b/src/zeroconf/_services/info.py @@ -55,6 +55,7 @@ _DNS_OTHER_TTL, _FLAGS_QR_QUERY, _LISTENER_TIME, + _MDNS_PORT, _TYPE_A, _TYPE_AAAA, _TYPE_NSEC, @@ -616,7 +617,12 @@ def _is_complete(self) -> bool: 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 + 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. @@ -628,13 +634,29 @@ def request( 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), zc.loop, timeout)) + return bool( + run_coro_with_timeout( + self.async_request(zc, timeout, question_type, addr, port), zc.loop, timeout + ) + ) async def async_request( - self, zc: 'Zeroconf', timeout: float, question_type: Optional[DNSQuestionType] = None + 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() @@ -658,7 +680,7 @@ async def async_request( first_request = False if not out.questions: return self.load_from_cache(zc) - zc.async_send(out) + zc.async_send(out, addr, port) next_ = now + delay delay *= 2 next_ += random.randint(*_AVOID_SYNC_DELAY_RANDOM_INTERVAL) diff --git a/tests/services/test_info.py b/tests/services/test_info.py index f24baa8f..13f48392 100644 --- a/tests/services/test_info.py +++ b/tests/services/test_info.py @@ -956,6 +956,89 @@ async def test_port_changes_are_seen(): await aiozc.async_close() +@pytest.mark.asyncio +async def test_port_changes_are_seen_with_directed_request(): + """Test that port changes are seen by async_request with a directed request.""" + type_ = "_http._tcp.local." + registration_name = "multiarec.%s" % type_ + desc = {'path': '/~paulsm/'} + aiozc = AsyncZeroconf(interfaces=['127.0.0.1']) + host = "multahost.local." + + # New kwarg way + generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) + generated.add_answer_at_time( + r.DNSNsec( + registration_name, + const._TYPE_NSEC, + const._CLASS_IN | const._CLASS_UNIQUE, + const._DNS_OTHER_TTL, + registration_name, + [const._TYPE_AAAA], + ), + 0, + ) + generated.add_answer_at_time( + r.DNSService( + registration_name, + const._TYPE_SRV, + const._CLASS_IN | const._CLASS_UNIQUE, + 10000, + 0, + 0, + 80, + host, + ), + 0, + ) + generated.add_answer_at_time( + r.DNSAddress( + host, + const._TYPE_A, + const._CLASS_IN, + 10000, + b'\x7f\x00\x00\x01', + ), + 0, + ) + generated.add_answer_at_time( + r.DNSText( + registration_name, + const._TYPE_TXT, + const._CLASS_IN | const._CLASS_UNIQUE, + 10000, + b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==', + ), + 0, + ) + await aiozc.zeroconf.async_wait_for_start() + await asyncio.sleep(0) + aiozc.zeroconf.handle_response(r.DNSIncoming(generated.packets()[0])) + + generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE) + generated.add_answer_at_time( + r.DNSService( + registration_name, + const._TYPE_SRV, + const._CLASS_IN | const._CLASS_UNIQUE, + 10000, + 90, + 90, + 81, + host, + ), + 0, + ) + aiozc.zeroconf.handle_response(r.DNSIncoming(generated.packets()[0])) + + info = ServiceInfo(type_, registration_name, 80, 10, 10, desc, host) + await info.async_request(aiozc.zeroconf, timeout=200, addr="127.0.0.1", port=5353) + assert info.port == 81 + assert info.priority == 90 + assert info.weight == 90 + await aiozc.async_close() + + @pytest.mark.asyncio async def test_ipv4_changes_are_seen(): """Test that ipv4 changes are seen by async_request."""