Files
site/WESP_REL/app/services/sync_error_display.py
T
2026-07-17 12:57:18 +03:00

139 lines
5.0 KiB
Python

"""Фильтрация и человекочитаемые сообщения об ошибках синхронизации (UI и логи)."""
from __future__ import annotations
import logging
from urllib.parse import urlparse
# Фрагменты типичных сообщений urllib3/requests и ОС о недоступности сервера / DNS / таймаутах.
_SERVER_CONNECTION_MARKERS = (
"connection refused",
"connection reset",
"connection aborted",
"broken pipe",
"server has gone away",
"remote end closed connection",
"connection timed out",
"timed out",
"timeout",
"read timed out",
"connect timeout",
"name or service not known",
"nodename nor servname",
"getaddrinfo failed",
"temporary failure in name resolution",
"failed to resolve",
"network is unreachable",
"no route to host",
"could not connect",
"unable to connect",
"failed to establish a new connection",
"max retries exceeded",
"newconnectionerror",
"connecttimeouterror",
"connectionerror",
"httpconnectionpool",
"httpsconnectionpool",
"errno 111",
"errno 110",
"errno 113",
"econnrefused",
"etimedout",
"ehostunreach",
"enotfound",
"winerror 10061",
"10060", # Windows WSAETIMEDOUT
"10065", # WSAEHOSTUNREACH
# Русские формулировки (системные / локализованные)
"отказ в подключении",
"время ожидания истекло",
"нет маршрута до узла",
"сеть недоступна",
)
def is_server_connection_error_message(message: str | None) -> bool:
"""True, если текст похож на ошибку доставки до сервера (TCP/DNS/таймаут), а не на логику sync."""
if message is None:
return False
s = str(message).strip().lower()
if not s:
return False
return any(marker in s for marker in _SERVER_CONNECTION_MARKERS)
def sync_error_text_for_display(message: str | None) -> str | None:
"""Текст ошибки для показа оператору или None, если скрываем (ошибка подключения к серверу)."""
if message is None:
return None
raw = str(message).strip()
if not raw:
return None
if is_server_connection_error_message(raw):
return None
return raw
def _host_from_server_url(server_url: str) -> str:
try:
host = (urlparse(server_url or "").hostname or "").strip()
return host or "?"
except Exception:
return "?"
def is_transient_sync_transport_error(exc: BaseException) -> bool:
"""Сетевые сбои: DNS, таймаут, отказ соединения — фоновый sync может повторить позже."""
try:
import requests
except ImportError:
requests = None # type: ignore
if requests is not None and isinstance(exc, (requests.Timeout, requests.ConnectionError)):
return True
if requests is not None and isinstance(exc, requests.RequestException):
return is_server_connection_error_message(str(exc))
return is_server_connection_error_message(str(exc))
def friendly_sync_transport_message(
exc: BaseException,
*,
server_url: str = "",
) -> str:
"""Короткое сообщение для логов (без дампа urllib3)."""
host = _host_from_server_url(server_url)
raw = str(exc).strip()
lower = raw.lower()
if "failed to resolve" in lower or "getaddrinfo" in lower or "name resolution" in lower:
return (
f"Сервер синхронизации не найден в сети ({host}). "
"mDNS (.local) в этой сети может не работать — укажите IP сервера в настройках sync."
)
if "connection refused" in lower or "errno 111" in lower:
return (
f"Сервер {host} недоступен (отказ в подключении). "
"Убедитесь, что центральный узел запущен и порт верный."
)
if "timed out" in lower or "timeout" in lower:
return f"Таймаут при обращении к серверу синхронизации ({host})."
if is_server_connection_error_message(raw):
return f"Нет связи с сервером синхронизации ({host})."
return raw[:500] if raw else exc.__class__.__name__
def log_sync_transport_problem(
logger: logging.Logger,
context: str,
exc: BaseException,
*,
server_url: str = "",
) -> None:
"""WARNING для ожидаемых сетевых сбоев, ERROR + traceback для прочего."""
msg = friendly_sync_transport_message(exc, server_url=server_url)
if is_transient_sync_transport_error(exc):
logger.warning("sync: %s: %s", context, msg)
else:
logger.error("sync: %s: %s", context, msg, exc_info=True)