refactor(desktop): move windows linux and macos launchers to shared runtime

This commit is contained in:
Dark_Avery 2026-03-19 20:34:52 +03:00
parent 3d10eb9113
commit 85b111d0f3
3 changed files with 57 additions and 318 deletions

131
linux.py
View File

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio as _asyncio
import json import json
import logging import logging
import os import os
@ -9,7 +8,7 @@ import sys
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Optional
import customtkinter as ctk import customtkinter as ctk
import psutil import psutil
@ -18,6 +17,7 @@ import pystray
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy.app_runtime import ProxyAppRuntime
APP_NAME = "TgWsProxy" APP_NAME = "TgWsProxy"
APP_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / APP_NAME 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 _tray_icon: Optional[object] = None
_config: dict = {} _config: dict = {}
_exiting: bool = False _exiting: bool = False
_lock_file_path: Optional[Path] = None _lock_file_path: Optional[Path] = None
log = logging.getLogger("tg-ws-tray") 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: def _same_process(lock_meta: dict, proc: psutil.Process) -> bool:
@ -126,53 +132,19 @@ def _acquire_lock() -> bool:
def _ensure_dirs(): def _ensure_dirs():
APP_DIR.mkdir(parents=True, exist_ok=True) _runtime.ensure_dirs()
def load_config() -> dict: def load_config() -> dict:
_ensure_dirs() return _runtime.load_config()
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)
def save_config(cfg: dict): def save_config(cfg: dict):
_ensure_dirs() _runtime.save_config(cfg)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
def setup_logging(verbose: bool = False): def setup_logging(verbose: bool = False):
_ensure_dirs() _runtime.setup_logging(verbose)
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)
def _make_icon_image(size: int = 64): def _make_icon_image(size: int = 64):
@ -217,75 +189,16 @@ def _load_icon():
return _make_icon_image() 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(): def start_proxy():
global _proxy_thread, _config _runtime.start_proxy(_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()
def stop_proxy(): def stop_proxy():
global _proxy_thread, _async_stop _runtime.stop_proxy()
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")
def restart_proxy(): def restart_proxy():
log.info("Restarting proxy...") _runtime.restart_proxy()
stop_proxy()
time.sleep(0.3)
start_proxy()
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"): def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"):
@ -789,14 +702,8 @@ def _build_menu():
def run_tray(): def run_tray():
global _tray_icon, _config global _tray_icon, _config
_config = load_config() _config = _runtime.prepare()
save_config(_config) _runtime.reset_log_file()
if LOG_FILE.exists():
try:
LOG_FILE.unlink()
except Exception:
pass
setup_logging(_config.get("verbose", False)) setup_logging(_config.get("verbose", False))
log.info("TG WS Proxy tray app starting") log.info("TG WS Proxy tray app starting")

124
macos.py
View File

@ -9,9 +9,8 @@ import sys
import threading import threading
import time import time
import webbrowser import webbrowser
import asyncio as _asyncio
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Optional
try: try:
import rumps import rumps
@ -29,6 +28,7 @@ except ImportError:
pyperclip = None pyperclip = None
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy.app_runtime import ProxyAppRuntime
APP_NAME = "TgWsProxy" APP_NAME = "TgWsProxy"
APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME
@ -45,14 +45,20 @@ DEFAULT_CONFIG = {
"verbose": False, "verbose": False,
} }
_proxy_thread: Optional[threading.Thread] = None
_async_stop: Optional[object] = None
_app: Optional[object] = None _app: Optional[object] = None
_config: dict = {} _config: dict = {}
_exiting: bool = False _exiting: bool = False
_lock_file_path: Optional[Path] = None _lock_file_path: Optional[Path] = None
log = logging.getLogger("tg-ws-tray") 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 # Single-instance lock
@ -130,48 +136,19 @@ def _acquire_lock() -> bool:
# Filesystem helpers # Filesystem helpers
def _ensure_dirs(): def _ensure_dirs():
APP_DIR.mkdir(parents=True, exist_ok=True) _runtime.ensure_dirs()
def load_config() -> dict: def load_config() -> dict:
_ensure_dirs() return _runtime.load_config()
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)
def save_config(cfg: dict): def save_config(cfg: dict):
_ensure_dirs() _runtime.save_config(cfg)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
def setup_logging(verbose: bool = False): def setup_logging(verbose: bool = False):
_ensure_dirs() _runtime.setup_logging(verbose)
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)
# Menubar icon # Menubar icon
@ -246,73 +223,16 @@ def _ask_yes_no(text: str, title: str = "TG WS Proxy") -> bool:
# Proxy lifecycle # 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(): def start_proxy():
global _proxy_thread, _config _runtime.start_proxy(_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()
def stop_proxy(): def stop_proxy():
global _proxy_thread, _async_stop _runtime.stop_proxy()
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")
def restart_proxy(): def restart_proxy():
log.info("Restarting proxy...") _runtime.restart_proxy()
stop_proxy()
time.sleep(0.3)
start_proxy()
# Menu callbacks # Menu callbacks
@ -572,14 +492,8 @@ class TgWsProxyApp(_TgWsProxyAppBase):
def run_menubar(): def run_menubar():
global _app, _config global _app, _config
_config = load_config() _config = _runtime.prepare()
save_config(_config) _runtime.reset_log_file()
if LOG_FILE.exists():
try:
LOG_FILE.unlink()
except Exception:
pass
setup_logging(_config.get("verbose", False)) setup_logging(_config.get("verbose", False))
log.info("TG WS Proxy menubar app starting") log.info("TG WS Proxy menubar app starting")

View File

@ -11,15 +11,15 @@ import threading
import time import time
import webbrowser import webbrowser
import pyperclip import pyperclip
import asyncio as _asyncio
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Optional
import pystray import pystray
import customtkinter as ctk import customtkinter as ctk
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy.app_runtime import ProxyAppRuntime
IS_FROZEN = bool(getattr(sys, "frozen", False)) 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 _tray_icon: Optional[object] = None
_config: dict = {} _config: dict = {}
_exiting: bool = False _exiting: bool = False
_lock_file_path: Optional[Path] = None _lock_file_path: Optional[Path] = None
log = logging.getLogger("tg-ws-tray") 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: def _same_process(lock_meta: dict, proc: psutil.Process) -> bool:
@ -125,48 +131,19 @@ def _acquire_lock() -> bool:
def _ensure_dirs(): def _ensure_dirs():
APP_DIR.mkdir(parents=True, exist_ok=True) _runtime.ensure_dirs()
def load_config() -> dict: def load_config() -> dict:
_ensure_dirs() return _runtime.load_config()
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)
def save_config(cfg: dict): def save_config(cfg: dict):
_ensure_dirs() _runtime.save_config(cfg)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
def setup_logging(verbose: bool = False): def setup_logging(verbose: bool = False):
_ensure_dirs() _runtime.setup_logging(verbose)
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)
def _autostart_reg_name() -> str: 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(): def start_proxy():
global _proxy_thread, _config _runtime.start_proxy(_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()
def stop_proxy(): def stop_proxy():
global _proxy_thread, _async_stop _runtime.stop_proxy()
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")
def restart_proxy(): def restart_proxy():
log.info("Restarting proxy...") _runtime.restart_proxy()
stop_proxy()
time.sleep(0.3)
start_proxy()
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"): def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"):
@ -731,14 +655,8 @@ def _build_menu():
def run_tray(): def run_tray():
global _tray_icon, _config global _tray_icon, _config
_config = load_config() _config = _runtime.prepare()
save_config(_config) _runtime.reset_log_file()
if LOG_FILE.exists():
try:
LOG_FILE.unlink()
except Exception:
pass
setup_logging(_config.get("verbose", False)) setup_logging(_config.get("verbose", False))
log.info("TG WS Proxy tray app starting") log.info("TG WS Proxy tray app starting")