diff --git a/pddnsc/base.py b/pddnsc/base.py index 8d33fd8..c7cc7c7 100644 --- a/pddnsc/base.py +++ b/pddnsc/base.py @@ -51,6 +51,7 @@ class BaseSourceProvider(ABC): async def fetch_all(self) -> IPAddreses: """Метод для получения всех IP адресов сразу""" + results = await asyncio.gather( self.fetch_v4(), self.fetch_v6(), return_exceptions=True ) @@ -61,12 +62,18 @@ class BaseSourceProvider(ABC): async def fetch_v4(self) -> str: """Метод внешнего интерфейса для получения IPv4""" - ipv4 = await self.fetch_v4_impl() + result = await asyncio.gather(self.fetch_v4_impl(), return_exceptions=True) + ipv4 = "" + if not isinstance(result[0], Exception): + ipv4 = result[0] return self.filter_ipv4(ipv4) async def fetch_v6(self) -> str: """Метод внешнего интерфейса для получения IPv6""" - ipv6 = await self.fetch_v6_impl() + result = await asyncio.gather(self.fetch_v6_impl(), return_exceptions=True) + ipv6 = "" + if not isinstance(result[0], Exception): + ipv6 = result[0] return self.filter_ipv6(ipv6) def __init_subclass__(cls) -> None: diff --git a/pddnsc/cli.py b/pddnsc/cli.py index 5270748..a5cd3ea 100644 --- a/pddnsc/cli.py +++ b/pddnsc/cli.py @@ -5,7 +5,20 @@ import asyncio import toml from .base import BaseFilterProvider, BaseSourceProvider, BaseOutputProvider, IPAddreses from .plugins import use_plugins -from typing import Optional +from typing import Optional, NamedTuple + + +class NeededAddrs(NamedTuple): + ipv4: bool + ipv6: bool + + @classmethod + def from_config(cls, config: dict) -> "NeededAddrs": + need_ipv4 = config.get("require_ipv4", False) + need_ipv6 = config.get("require_ipv6", False) + if "require_ipv4" not in config and "require_ipv6" not in config: + need_ipv4 = need_ipv6 = True + return cls(need_ipv4, need_ipv6) def is_valid_addreses(addrs: IPAddreses, config: dict) -> bool: @@ -23,12 +36,133 @@ def is_valid_addreses(addrs: IPAddreses, config: dict) -> bool: result = result and addrs.ipv4 if config.get("require_ipv6"): result = result and addrs.ipv6 - return result + return bool(result) + + +def is_ipv4_ok(ipv4: str, config: dict) -> bool: + """Проверка IPv4 адреса, подходит или нет + + Args: + ipv4 (str): IPv4 адрес + config (dict): общая конфигурация + + Returns: + bool: подходит или нет + """ + return bool(ipv4) if NeededAddrs.from_config(config).ipv4 else True + + +def is_ipv6_ok(ipv6: str, config: dict) -> bool: + """Проверка IPv6 адреса, подходит или нет + + Args: + ipv6 (str): IPv6 адрес + config (dict): общая конфигурация + + Returns: + bool: подходит или нет + """ + return bool(ipv6) if NeededAddrs.from_config(config).ipv6 else True async def get_ip_addresses(config: dict) -> Optional[IPAddreses]: """Получение всех IP адресов из всех источников + Args: + config (dict): общая конфигурация + + Returns: + Optional[IPAddreses]: результат получения, либо None + """ + + unit_mode = config.get("unit_mode", False) + if unit_mode: + result = await get_ip_addresses_unit(config) + else: + result = await get_ip_addresses_any(config) + return result + + +async def get_ip_addresses_any(config: dict) -> Optional[IPAddreses]: + """Получение всех IP адресов из всех источников (режим any) + + Получает разные адреса от любых источников + + Args: + config (dict): общая конфигурация + + Returns: + Optional[IPAddreses]: результат получения, либо None + """ + need = NeededAddrs.from_config(config) + providers = BaseSourceProvider.registred.values() + ip_addresses, is_done = None, False + last_src, last_ipv4, last_ipv6 = "", "", "" + ok_ipv4, ok_ipv6 = False, False + drop = [] + pending = [] + if need.ipv4: + pending += [ + asyncio.create_task(p.fetch_v4(), name=f"{p.name}.v4") for p in providers + ] + if need.ipv6: + pending += [ + asyncio.create_task(p.fetch_v6(), name=f"{p.name}.v6") for p in providers + ] + while not is_done and pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) + for x in done: + ip_addr = x.result() + if x.get_name().endswith(".v6"): + last_ipv6 = last_ipv6 if ok_ipv6 else ip_addr + else: + last_ipv4 = last_ipv4 if ok_ipv4 else ip_addr + if config.get("debug"): + print("debug:", "get", x.get_name(), ip_addr) + last_src = x.get_name().rsplit(".", maxsplit=1)[0] + ok_ipv4 = is_ipv4_ok(last_ipv4, config) + ok_ipv6 = is_ipv6_ok(last_ipv6, config) + pending_v4 = [*filter(lambda i: i.get_name().endswith(".v4"), pending)] + pending_v6 = [*filter(lambda i: i.get_name().endswith(".v6"), pending)] + if ok_ipv4 and pending_v4: + drop += pending_v4 + pending = pending_v6 + for i in pending_v4: + i.cancel() + pending_v4 = [] + if ok_ipv6 and pending_v6: + drop += pending_v6 + pending = pending_v4 + for i in pending_v6: + i.cancel() + pending_v6 = [] + if not (ok_ipv4 and ok_ipv6): + continue + ip_addresses = IPAddreses(last_src, last_ipv4, last_ipv6) + if is_valid_addreses(ip_addresses, config): + is_done = True + break + ip_addresses = None + + if not (ok_ipv4 and ok_ipv6): + ip_addresses = IPAddreses(last_src, last_ipv4, last_ipv6) + + drop = drop + list(pending) + if drop: + gather = asyncio.gather(*drop) + gather.cancel() + try: + await gather + except asyncio.CancelledError: + pass + return ip_addresses + + +async def get_ip_addresses_unit(config: dict) -> Optional[IPAddreses]: + """Получение всех IP адресов из всех источников (режим unit) + + Получает адреса только от одного источника + Args: config (dict): общая конфигурация