From 85b111d0f3af70440ffbd06e904ab3d3b7ef8516 Mon Sep 17 00:00:00 2001 From: Dark_Avery Date: Thu, 19 Mar 2026 20:34:52 +0300 Subject: [PATCH] refactor(desktop): move windows linux and macos launchers to shared runtime --- linux.py | 131 ++++++++--------------------------------------------- macos.py | 124 ++++++++------------------------------------------ windows.py | 120 ++++++++---------------------------------------- 3 files changed, 57 insertions(+), 318 deletions(-) diff --git a/linux.py b/linux.py index 9b2ff9e..60816c6 100644 --- a/linux.py +++ b/linux.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio as _asyncio import json import logging import os @@ -9,7 +8,7 @@ import sys import threading import time from pathlib import Path -from typing import Dict, Optional +from typing import Optional import customtkinter as ctk import psutil @@ -18,6 +17,7 @@ import pystray from PIL import Image, ImageDraw, ImageFont import proxy.tg_ws_proxy as tg_ws_proxy +from proxy.app_runtime import ProxyAppRuntime APP_NAME = "TgWsProxy" APP_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / APP_NAME @@ -35,14 +35,20 @@ DEFAULT_CONFIG = { } -_proxy_thread: Optional[threading.Thread] = None -_async_stop: Optional[object] = None _tray_icon: Optional[object] = None _config: dict = {} _exiting: bool = False _lock_file_path: Optional[Path] = None log = logging.getLogger("tg-ws-tray") +_runtime = ProxyAppRuntime( + APP_DIR, + default_config=DEFAULT_CONFIG, + logger_name="tg-ws-tray", + on_error=lambda text: _show_error(text), +) +CONFIG_FILE = _runtime.config_file +LOG_FILE = _runtime.log_file def _same_process(lock_meta: dict, proc: psutil.Process) -> bool: @@ -126,53 +132,19 @@ def _acquire_lock() -> bool: def _ensure_dirs(): - APP_DIR.mkdir(parents=True, exist_ok=True) + _runtime.ensure_dirs() def load_config() -> dict: - _ensure_dirs() - if CONFIG_FILE.exists(): - try: - with open(CONFIG_FILE, "r", encoding="utf-8") as f: - data = json.load(f) - for k, v in DEFAULT_CONFIG.items(): - data.setdefault(k, v) - return data - except Exception as exc: - log.warning("Failed to load config: %s", exc) - return dict(DEFAULT_CONFIG) + return _runtime.load_config() def save_config(cfg: dict): - _ensure_dirs() - with open(CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(cfg, f, indent=2, ensure_ascii=False) + _runtime.save_config(cfg) def setup_logging(verbose: bool = False): - _ensure_dirs() - root = logging.getLogger() - root.setLevel(logging.DEBUG if verbose else logging.INFO) - - fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") - fh.setLevel(logging.DEBUG) - fh.setFormatter( - logging.Formatter( - "%(asctime)s %(levelname)-5s %(name)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - ) - root.addHandler(fh) - - if not getattr(sys, "frozen", False): - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(logging.DEBUG if verbose else logging.INFO) - ch.setFormatter( - logging.Formatter( - "%(asctime)s %(levelname)-5s %(message)s", datefmt="%H:%M:%S" - ) - ) - root.addHandler(ch) + _runtime.setup_logging(verbose) def _make_icon_image(size: int = 64): @@ -217,75 +189,16 @@ def _load_icon(): return _make_icon_image() -def _run_proxy_thread( - port: int, dc_opt: Dict[int, str], verbose: bool, host: str = "127.0.0.1" -): - global _async_stop - loop = _asyncio.new_event_loop() - _asyncio.set_event_loop(loop) - stop_ev = _asyncio.Event() - _async_stop = (loop, stop_ev) - - try: - loop.run_until_complete( - tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host) - ) - except Exception as exc: - log.error("Proxy thread crashed: %s", exc) - if "Address already in use" in str(exc): - _show_error( - "Не удалось запустить прокси:\nПорт уже используется другим приложением.\n\nЗакройте приложение, использующее этот порт, или измените порт в настройках прокси и перезапустите." - ) - finally: - loop.close() - _async_stop = None - - def start_proxy(): - global _proxy_thread, _config - if _proxy_thread and _proxy_thread.is_alive(): - log.info("Proxy already running") - return - - cfg = _config - port = cfg.get("port", DEFAULT_CONFIG["port"]) - host = cfg.get("host", DEFAULT_CONFIG["host"]) - dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]) - verbose = cfg.get("verbose", False) - - try: - dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list) - except ValueError as e: - log.error("Bad config dc_ip: %s", e) - _show_error(f"Ошибка конфигурации:\n{e}") - return - - log.info("Starting proxy on %s:%d ...", host, port) - _proxy_thread = threading.Thread( - target=_run_proxy_thread, - args=(port, dc_opt, verbose, host), - daemon=True, - name="proxy", - ) - _proxy_thread.start() + _runtime.start_proxy(_config) def stop_proxy(): - global _proxy_thread, _async_stop - if _async_stop: - loop, stop_ev = _async_stop - loop.call_soon_threadsafe(stop_ev.set) - if _proxy_thread: - _proxy_thread.join(timeout=2) - _proxy_thread = None - log.info("Proxy stopped") + _runtime.stop_proxy() def restart_proxy(): - log.info("Restarting proxy...") - stop_proxy() - time.sleep(0.3) - start_proxy() + _runtime.restart_proxy() def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"): @@ -789,14 +702,8 @@ def _build_menu(): def run_tray(): global _tray_icon, _config - _config = load_config() - save_config(_config) - - if LOG_FILE.exists(): - try: - LOG_FILE.unlink() - except Exception: - pass + _config = _runtime.prepare() + _runtime.reset_log_file() setup_logging(_config.get("verbose", False)) log.info("TG WS Proxy tray app starting") diff --git a/macos.py b/macos.py index e1806cf..f502656 100644 --- a/macos.py +++ b/macos.py @@ -9,9 +9,8 @@ import sys import threading import time import webbrowser -import asyncio as _asyncio from pathlib import Path -from typing import Dict, Optional +from typing import Optional try: import rumps @@ -29,6 +28,7 @@ except ImportError: pyperclip = None import proxy.tg_ws_proxy as tg_ws_proxy +from proxy.app_runtime import ProxyAppRuntime APP_NAME = "TgWsProxy" APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME @@ -45,14 +45,20 @@ DEFAULT_CONFIG = { "verbose": False, } -_proxy_thread: Optional[threading.Thread] = None -_async_stop: Optional[object] = None _app: Optional[object] = None _config: dict = {} _exiting: bool = False _lock_file_path: Optional[Path] = None log = logging.getLogger("tg-ws-tray") +_runtime = ProxyAppRuntime( + APP_DIR, + default_config=DEFAULT_CONFIG, + logger_name="tg-ws-tray", + on_error=lambda text: _show_error(text), +) +CONFIG_FILE = _runtime.config_file +LOG_FILE = _runtime.log_file # Single-instance lock @@ -130,48 +136,19 @@ def _acquire_lock() -> bool: # Filesystem helpers def _ensure_dirs(): - APP_DIR.mkdir(parents=True, exist_ok=True) + _runtime.ensure_dirs() def load_config() -> dict: - _ensure_dirs() - if CONFIG_FILE.exists(): - try: - with open(CONFIG_FILE, "r", encoding="utf-8") as f: - data = json.load(f) - for k, v in DEFAULT_CONFIG.items(): - data.setdefault(k, v) - return data - except Exception as exc: - log.warning("Failed to load config: %s", exc) - return dict(DEFAULT_CONFIG) + return _runtime.load_config() def save_config(cfg: dict): - _ensure_dirs() - with open(CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(cfg, f, indent=2, ensure_ascii=False) + _runtime.save_config(cfg) def setup_logging(verbose: bool = False): - _ensure_dirs() - root = logging.getLogger() - root.setLevel(logging.DEBUG if verbose else logging.INFO) - - fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter( - "%(asctime)s %(levelname)-5s %(name)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S")) - root.addHandler(fh) - - if not getattr(sys, "frozen", False): - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(logging.DEBUG if verbose else logging.INFO) - ch.setFormatter(logging.Formatter( - "%(asctime)s %(levelname)-5s %(message)s", - datefmt="%H:%M:%S")) - root.addHandler(ch) + _runtime.setup_logging(verbose) # Menubar icon @@ -246,73 +223,16 @@ def _ask_yes_no(text: str, title: str = "TG WS Proxy") -> bool: # Proxy lifecycle -def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool, - host: str = '127.0.0.1'): - global _async_stop - loop = _asyncio.new_event_loop() - _asyncio.set_event_loop(loop) - stop_ev = _asyncio.Event() - _async_stop = (loop, stop_ev) - - try: - loop.run_until_complete( - tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host)) - except Exception as exc: - log.error("Proxy thread crashed: %s", exc) - if "Address already in use" in str(exc): - _show_error( - "Не удалось запустить прокси:\n" - "Порт уже используется другим приложением.\n\n" - "Закройте приложение, использующее этот порт, " - "или измените порт в настройках прокси и перезапустите.") - finally: - loop.close() - _async_stop = None - - def start_proxy(): - global _proxy_thread, _config - if _proxy_thread and _proxy_thread.is_alive(): - log.info("Proxy already running") - return - - cfg = _config - port = cfg.get("port", DEFAULT_CONFIG["port"]) - host = cfg.get("host", DEFAULT_CONFIG["host"]) - dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]) - verbose = cfg.get("verbose", False) - - try: - dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list) - except ValueError as e: - log.error("Bad config dc_ip: %s", e) - _show_error(f"Ошибка конфигурации:\n{e}") - return - - log.info("Starting proxy on %s:%d ...", host, port) - _proxy_thread = threading.Thread( - target=_run_proxy_thread, - args=(port, dc_opt, verbose, host), - daemon=True, name="proxy") - _proxy_thread.start() + _runtime.start_proxy(_config) def stop_proxy(): - global _proxy_thread, _async_stop - if _async_stop: - loop, stop_ev = _async_stop - loop.call_soon_threadsafe(stop_ev.set) - if _proxy_thread: - _proxy_thread.join(timeout=2) - _proxy_thread = None - log.info("Proxy stopped") + _runtime.stop_proxy() def restart_proxy(): - log.info("Restarting proxy...") - stop_proxy() - time.sleep(0.3) - start_proxy() + _runtime.restart_proxy() # Menu callbacks @@ -572,14 +492,8 @@ class TgWsProxyApp(_TgWsProxyAppBase): def run_menubar(): global _app, _config - _config = load_config() - save_config(_config) - - if LOG_FILE.exists(): - try: - LOG_FILE.unlink() - except Exception: - pass + _config = _runtime.prepare() + _runtime.reset_log_file() setup_logging(_config.get("verbose", False)) log.info("TG WS Proxy menubar app starting") diff --git a/windows.py b/windows.py index 568fce0..422c12e 100644 --- a/windows.py +++ b/windows.py @@ -11,15 +11,15 @@ import threading import time import webbrowser import pyperclip -import asyncio as _asyncio from pathlib import Path -from typing import Dict, Optional +from typing import Optional import pystray import customtkinter as ctk from PIL import Image, ImageDraw, ImageFont import proxy.tg_ws_proxy as tg_ws_proxy +from proxy.app_runtime import ProxyAppRuntime IS_FROZEN = bool(getattr(sys, "frozen", False)) @@ -41,14 +41,20 @@ DEFAULT_CONFIG = { } -_proxy_thread: Optional[threading.Thread] = None -_async_stop: Optional[object] = None _tray_icon: Optional[object] = None _config: dict = {} _exiting: bool = False _lock_file_path: Optional[Path] = None log = logging.getLogger("tg-ws-tray") +_runtime = ProxyAppRuntime( + APP_DIR, + default_config=DEFAULT_CONFIG, + logger_name="tg-ws-tray", + on_error=lambda text: _show_error(text), +) +CONFIG_FILE = _runtime.config_file +LOG_FILE = _runtime.log_file def _same_process(lock_meta: dict, proc: psutil.Process) -> bool: @@ -125,48 +131,19 @@ def _acquire_lock() -> bool: def _ensure_dirs(): - APP_DIR.mkdir(parents=True, exist_ok=True) + _runtime.ensure_dirs() def load_config() -> dict: - _ensure_dirs() - if CONFIG_FILE.exists(): - try: - with open(CONFIG_FILE, "r", encoding="utf-8") as f: - data = json.load(f) - for k, v in DEFAULT_CONFIG.items(): - data.setdefault(k, v) - return data - except Exception as exc: - log.warning("Failed to load config: %s", exc) - return dict(DEFAULT_CONFIG) + return _runtime.load_config() def save_config(cfg: dict): - _ensure_dirs() - with open(CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(cfg, f, indent=2, ensure_ascii=False) + _runtime.save_config(cfg) def setup_logging(verbose: bool = False): - _ensure_dirs() - root = logging.getLogger() - root.setLevel(logging.DEBUG if verbose else logging.INFO) - - fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter( - "%(asctime)s %(levelname)-5s %(name)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S")) - root.addHandler(fh) - - if not getattr(sys, "frozen", False): - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(logging.DEBUG if verbose else logging.INFO) - ch.setFormatter(logging.Formatter( - "%(asctime)s %(levelname)-5s %(message)s", - datefmt="%H:%M:%S")) - root.addHandler(ch) + _runtime.setup_logging(verbose) def _autostart_reg_name() -> str: @@ -261,69 +238,16 @@ def _load_icon(): -def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool, - host: str = '127.0.0.1'): - global _async_stop - loop = _asyncio.new_event_loop() - _asyncio.set_event_loop(loop) - stop_ev = _asyncio.Event() - _async_stop = (loop, stop_ev) - - try: - loop.run_until_complete( - tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host)) - except Exception as exc: - log.error("Proxy thread crashed: %s", exc) - if "10048" in str(exc) or "Address already in use" in str(exc): - _show_error("Не удалось запустить прокси:\nПорт уже используется другим приложением.\n\nЗакройте приложение, использующее этот порт, или измените порт в настройках прокси и перезапустите.") - finally: - loop.close() - _async_stop = None - - def start_proxy(): - global _proxy_thread, _config - if _proxy_thread and _proxy_thread.is_alive(): - log.info("Proxy already running") - return - - cfg = _config - port = cfg.get("port", DEFAULT_CONFIG["port"]) - host = cfg.get("host", DEFAULT_CONFIG["host"]) - dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]) - verbose = cfg.get("verbose", False) - - try: - dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list) - except ValueError as e: - log.error("Bad config dc_ip: %s", e) - _show_error(f"Ошибка конфигурации:\n{e}") - return - - log.info("Starting proxy on %s:%d ...", host, port) - _proxy_thread = threading.Thread( - target=_run_proxy_thread, - args=(port, dc_opt, verbose, host), - daemon=True, name="proxy") - _proxy_thread.start() + _runtime.start_proxy(_config) def stop_proxy(): - global _proxy_thread, _async_stop - if _async_stop: - loop, stop_ev = _async_stop - loop.call_soon_threadsafe(stop_ev.set) - if _proxy_thread: - _proxy_thread.join(timeout=2) - _proxy_thread = None - log.info("Proxy stopped") + _runtime.stop_proxy() def restart_proxy(): - log.info("Restarting proxy...") - stop_proxy() - time.sleep(0.3) - start_proxy() + _runtime.restart_proxy() def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"): @@ -731,14 +655,8 @@ def _build_menu(): def run_tray(): global _tray_icon, _config - _config = load_config() - save_config(_config) - - if LOG_FILE.exists(): - try: - LOG_FILE.unlink() - except Exception: - pass + _config = _runtime.prepare() + _runtime.reset_log_file() setup_logging(_config.get("verbose", False)) log.info("TG WS Proxy tray app starting")