diff --git a/macos.py b/macos.py index 896e77f8..8bc602a2 100644 --- a/macos.py +++ b/macos.py @@ -32,6 +32,7 @@ LOG_FILE, acquire_lock, apply_proxy_config, ensure_dirs, load_config, log, release_lock, save_config, setup_logging, stop_proxy, tg_proxy_url, ) +from utils.diagnostics import diagnose_listen_error MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png" @@ -184,13 +185,9 @@ def _run_proxy_thread() -> None: loop.run_until_complete(_run(stop_event=stop_ev)) except Exception as exc: log.error("Proxy thread crashed: %s", exc) - if "Address already in use" in str(exc): - _show_error( - "Не удалось запустить прокси:\n" - "Порт уже используется другим приложением.\n\n" - "Закройте приложение, использующее этот порт, " - "или измените порт в настройках прокси и перезапустите." - ) + msg = diagnose_listen_error(exc) + if msg: + _show_error(msg) finally: loop.close() _async_stop = None diff --git a/utils/diagnostics.py b/utils/diagnostics.py new file mode 100644 index 00000000..43014968 --- /dev/null +++ b/utils/diagnostics.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import errno + +from typing import Optional + + +MSG_PORT_BUSY = ( + "Не удалось запустить прокси:\n" + "Порт уже используется другим приложением.\n\n" + "Закройте приложение, использующее этот порт, " + "или измените порт в настройках прокси и перезапустите." +) + +MSG_PERMISSION = ( + "Не удалось запустить прокси:\n" + "Доступ к адресу/порту запрещён " + "(брандмауэр, антивирус или права доступа).\n\n" + "Измените порт на случайный в диапазоне 10000–50000 в настройках, " + "проверьте брандмауэр/антивирус и перезапустите." +) + +MSG_BAD_ADDRESS = ( + "Не удалось запустить прокси:\n" + "Некорректный или недоступный адрес для прослушивания.\n\n" + "Проверьте host и порт в настройках прокси и перезапустите." +) + +# Windows WinSock error codes (exc.winerror); errno may differ from POSIX. +_WSA_EACCES = 10013 +_WSA_EFAULT = 10014 +_WSA_EADDRINUSE = 10048 +_WSA_EADDRNOTAVAIL = 10049 + + +def diagnose_listen_error(exc: BaseException) -> Optional[str]: + """Map a listen-socket bind failure to a user-facing message. + + Returns None when the exception is not a recognizable bind failure, + so callers can fall back to generic handling. + """ + if not isinstance(exc, OSError): + return None + + err = exc.errno + winerror = getattr(exc, "winerror", None) + + if err == errno.EADDRINUSE or winerror == _WSA_EADDRINUSE: + return MSG_PORT_BUSY + if err == errno.EACCES or winerror == _WSA_EACCES: + return MSG_PERMISSION + if (winerror in (_WSA_EFAULT, _WSA_EADDRNOTAVAIL) + or err in (errno.EADDRNOTAVAIL, errno.EFAULT)): + return MSG_BAD_ADDRESS + return None diff --git a/utils/tray_common.py b/utils/tray_common.py index e553a35a..08b3399f 100644 --- a/utils/tray_common.py +++ b/utils/tray_common.py @@ -17,6 +17,7 @@ from proxy import __version__, get_link_host, parse_dc_ip_list, proxy_config, coerce_domain_list from proxy.tg_ws_proxy import _run from utils.default_config import default_tray_config +from utils.diagnostics import diagnose_listen_error log = logging.getLogger("tg-ws-tray") @@ -243,13 +244,9 @@ def _run_proxy_thread(on_port_busy: Callable[[str], None]) -> None: loop.run_until_complete(_run(stop_event=stop_ev)) except Exception as exc: log.error("Proxy thread crashed: %s", repr(exc)) - if "Address already in use" in str(exc) or "10048" in str(exc): - on_port_busy( - "Не удалось запустить прокси:\n" - "Порт уже используется другим приложением.\n\n" - "Закройте приложение, использующее этот порт, " - "или измените порт в настройках прокси и перезапустите." - ) + msg = diagnose_listen_error(exc) + if msg: + on_port_busy(msg) finally: loop.close() _async_stop = None