#!/usr/bin/env python3 from __future__ import annotations import re from typing import cast from api_client import CmdResult, Event from .views import ActionView, RoutesNftProgressView, RoutesResolveSummaryView, ServiceAction class RoutesControllerMixin: def routes_service_action(self, action: str) -> ActionView: act = action.strip().lower() if act not in ("start", "stop", "restart"): raise ValueError(f"Invalid routes action: {action}") res = self.client.routes_service(cast(ServiceAction, act)) return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res)) def routes_clear(self) -> ActionView: res = self.client.routes_clear() return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res)) def routes_cache_restore(self) -> ActionView: res = self.client.routes_cache_restore() return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res)) def routes_precheck_debug(self, run_now: bool = True) -> ActionView: res = self.client.routes_precheck_debug(run_now=run_now) return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res)) def routes_fix_policy_route(self) -> ActionView: res = self.client.routes_fix_policy_route() return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res)) def routes_timer_enabled(self) -> bool: st = self.client.routes_timer_get() return bool(st.enabled) def routes_timer_set(self, enabled: bool) -> ActionView: res = self.client.routes_timer_set(bool(enabled)) return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res)) def routes_resolve_summary_view(self) -> RoutesResolveSummaryView: dump = self.client.trace_get("full") lines = list(getattr(dump, "lines", []) or []) line = "" for raw in reversed(lines): s = str(raw or "") if "resolve summary:" in s: line = s break if not line: return RoutesResolveSummaryView( available=False, text="Resolve summary: no data yet", recheck_text="Timeout recheck: —", color="gray", recheck_color="gray", ) tail = line.split("resolve summary:", 1)[1] pairs: dict[str, int] = {} for m in re.finditer(r"([a-zA-Z0-9_]+)=(-?\d+)", tail): k = str(m.group(1) or "").strip().lower() try: pairs[k] = int(m.group(2)) except Exception: continue unique_ips = int(pairs.get("unique_ips", 0)) direct_ips = int(pairs.get("direct_ips", 0)) wildcard_ips = int(pairs.get("wildcard_ips", 0)) unresolved = int(pairs.get("unresolved", 0)) unresolved_live = int(pairs.get("unresolved_live", 0)) unresolved_suppressed = int(pairs.get("unresolved_suppressed", 0)) q_hits = int(pairs.get("quarantine_hits", 0)) dns_attempts = int(pairs.get("dns_attempts", 0)) dns_timeout = int(pairs.get("dns_timeout", 0)) dns_cooldown_skips = int(pairs.get("dns_cooldown_skips", 0)) live_batch_target = int(pairs.get("live_batch_target", 0)) live_batch_deferred = int(pairs.get("live_batch_deferred", 0)) live_batch_p1 = int(pairs.get("live_batch_p1", 0)) live_batch_p2 = int(pairs.get("live_batch_p2", 0)) live_batch_p3 = int(pairs.get("live_batch_p3", 0)) live_batch_nxheavy_pct = int(pairs.get("live_batch_nxheavy_pct", 0)) live_batch_nxheavy_skip = int(pairs.get("live_batch_nxheavy_skip", 0)) r_checked = int(pairs.get("timeout_recheck_checked", 0)) r_recovered = int(pairs.get("timeout_recheck_recovered", 0)) r_recovered_ips = int(pairs.get("timeout_recheck_recovered_ips", 0)) r_still_timeout = int(pairs.get("timeout_recheck_still_timeout", 0)) r_now_nx = int(pairs.get("timeout_recheck_now_nxdomain", 0)) r_now_tmp = int(pairs.get("timeout_recheck_now_temporary", 0)) text = ( f"Resolve: ips={unique_ips} (direct={direct_ips}, wildcard={wildcard_ips}, " f"+recheck_ips={r_recovered_ips}) | unresolved={unresolved} " f"(live={unresolved_live}, suppressed={unresolved_suppressed}) | " f"quarantine_hits={q_hits} | dns_timeout={dns_timeout} " f"| cooldown_skips={dns_cooldown_skips} | attempts={dns_attempts} " f"| live_batch={live_batch_target} deferred={live_batch_deferred} " f"(p1={live_batch_p1}, p2={live_batch_p2}, p3={live_batch_p3}, nx_pct={live_batch_nxheavy_pct}, nx_skip={live_batch_nxheavy_skip})" ) recheck_text = ( f"Timeout recheck: checked={r_checked} recovered={r_recovered} " f"still_timeout={r_still_timeout} now_nxdomain={r_now_nx} now_temporary={r_now_tmp}" ) color = "green" if unresolved < 4000 else ("#b58900" if unresolved < 10000 else "red") if dns_timeout > 500 and color == "green": color = "#b58900" if live_batch_p3 > 0 and (live_batch_p1+live_batch_p2) > 0: ratio = float(live_batch_p3) / float(live_batch_p1 + live_batch_p2 + live_batch_p3) if ratio > 0.8: color = "#b58900" if color == "green" else color if ratio > 0.95: color = "red" recheck_color = "green" if r_still_timeout <= 20 else ("#b58900" if r_still_timeout <= 100 else "red") return RoutesResolveSummaryView( available=True, text=text, recheck_text=recheck_text, color=color, recheck_color=recheck_color, ) def routes_nft_progress_from_event(self, ev: Event) -> RoutesNftProgressView: """ Превращает Event(kind='routes_nft_progress') в удобную модель для прогресс-бара/лейбла. """ payload = ( getattr(ev, "data", None) or getattr(ev, "payload", None) or getattr(ev, "extra", None) or {} ) if not isinstance(payload, dict): payload = {} try: percent = int(payload.get("percent", 0)) except Exception: percent = 0 msg = str(payload.get("message", "")) if payload is not None else "" if not msg: msg = "Updating nft set…" active = 0 <= percent < 100 return RoutesNftProgressView( percent=percent, message=msg, active=active, ) # -------- DNS / SmartDNS --------