diff --git a/macos.py b/macos.py deleted file mode 100644 index b61745d..0000000 --- a/macos.py +++ /dev/null @@ -1,401 +0,0 @@ -""" -TG WS Proxy — macOS menu bar application. -Requires: pip install rumps cryptography psutil -""" -from __future__ import annotations - -import json -import logging -import os -import subprocess -import sys -import threading -import time -import webbrowser -from pathlib import Path -from typing import Optional - -import psutil -import rumps - -# ── proxy core is a sibling package ──────────────────────────────────────── -sys.path.insert(0, str(Path(__file__).parent)) -import proxy.tg_ws_proxy as tg_ws_proxy -import updater - -# ── paths ─────────────────────────────────────────────────────────────────── -APP_NAME = "TgWsProxy" -APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME -CONFIG_FILE = APP_DIR / "config.json" -LOG_FILE = APP_DIR / "proxy.log" -FIRST_RUN_MARKER = APP_DIR / ".first_run_done" - -DEFAULT_CONFIG = { - "port": 1080, - "host": "127.0.0.1", - "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], - "verbose": False, -} - -# ── state ─────────────────────────────────────────────────────────────────── -_proxy_thread: Optional[threading.Thread] = None -_stop_event: Optional[object] = None # asyncio.Event, set from thread -_config: dict = {} -_exiting: bool = False - -log = logging.getLogger("tg-ws-tray") - - -# ── helpers ───────────────────────────────────────────────────────────────── - -def _ensure_dirs(): - APP_DIR.mkdir(parents=True, exist_ok=True) - - -def _setup_logging(): - _ensure_dirs() - fmt = "%(asctime)s %(levelname)-5s %(name)s %(message)s" - logging.basicConfig( - level=logging.DEBUG if _config.get("verbose") else logging.INFO, - format=fmt, - handlers=[ - logging.FileHandler(LOG_FILE, encoding="utf-8"), - logging.StreamHandler(sys.stdout), - ], - ) - - -def _acquire_lock() -> bool: - _ensure_dirs() - lock_files = list(APP_DIR.glob("*.lock")) - for f in lock_files: - try: - pid = int(f.stem) - if psutil.pid_exists(pid): - try: - psutil.Process(pid).status() - return False - except psutil.NoSuchProcess: - pass - f.unlink(missing_ok=True) - except (ValueError, OSError): - f.unlink(missing_ok=True) - - lock_path = APP_DIR / f"{os.getpid()}.lock" - lock_path.touch() - return True - - -def _release_lock(): - for f in APP_DIR.glob("*.lock"): - try: - if int(f.stem) == os.getpid(): - f.unlink(missing_ok=True) - except (ValueError, OSError): - pass - - -def load_config() -> dict: - if CONFIG_FILE.exists(): - try: - with open(CONFIG_FILE, encoding="utf-8") as fh: - cfg = json.load(fh) - merged = {**DEFAULT_CONFIG, **cfg} - return merged - except Exception as exc: - log.warning("Config load failed: %s — using defaults", exc) - return dict(DEFAULT_CONFIG) - - -def save_config(cfg: dict): - _ensure_dirs() - with open(CONFIG_FILE, "w", encoding="utf-8") as fh: - json.dump(cfg, fh, indent=2) - - -# ── proxy lifecycle ────────────────────────────────────────────────────────── - -def _run_proxy(cfg: dict): - """Runs in a daemon thread. Blocks until the proxy stops.""" - import asyncio - - stop = asyncio.Event() - - global _stop_event - _stop_event = stop - - dc_opt = tg_ws_proxy.parse_dc_ip_list(cfg["dc_ip"]) - log.info("Starting proxy on %s:%d", cfg["host"], cfg["port"]) - - try: - tg_ws_proxy.run_proxy( - port=cfg["port"], - dc_opt=dc_opt, - stop_event=stop, - host=cfg["host"], - ) - except Exception as exc: - log.error("Proxy crashed: %s", exc) - finally: - log.info("Proxy thread exited") - - -def start_proxy(cfg: dict): - global _proxy_thread, _stop_event - _stop_event = None - t = threading.Thread(target=_run_proxy, args=(cfg,), daemon=True) - t.start() - _proxy_thread = t - log.info("Proxy thread started (port=%d)", cfg["port"]) - - -def stop_proxy(): - global _stop_event, _proxy_thread - if _stop_event is not None: - try: - # _stop_event is an asyncio.Event living in the proxy thread's loop - # We signal it thread-safely via call_soon_threadsafe on its loop - import asyncio - loop = getattr(_stop_event, "_loop", None) - if loop and not loop.is_closed(): - loop.call_soon_threadsafe(_stop_event.set) - else: - _stop_event.set() - except Exception: - pass - _stop_event = None - - if _proxy_thread and _proxy_thread.is_alive(): - _proxy_thread.join(timeout=5) - _proxy_thread = None - log.info("Proxy stopped") - - -def restart_proxy(): - stop_proxy() - time.sleep(0.5) - start_proxy(_config) - - -# ── settings window (tkinter) ──────────────────────────────────────────────── - -def open_settings(): - """Show a settings dialog using tkinter (no extra deps needed on macOS).""" - import tkinter as tk - from tkinter import messagebox, ttk - - root = tk.Tk() - root.title("TG WS Proxy — Настройки") - root.resizable(False, False) - - pad = {"padx": 10, "pady": 5} - - # Host - tk.Label(root, text="Host:").grid(row=0, column=0, sticky="e", **pad) - host_var = tk.StringVar(value=_config.get("host", "127.0.0.1")) - tk.Entry(root, textvariable=host_var, width=20).grid(row=0, column=1, sticky="w", **pad) - - # Port - tk.Label(root, text="Port:").grid(row=1, column=0, sticky="e", **pad) - port_var = tk.StringVar(value=str(_config.get("port", 1080))) - tk.Entry(root, textvariable=port_var, width=10).grid(row=1, column=1, sticky="w", **pad) - - # DC IPs - tk.Label(root, text="DC IPs\n(DC:IP, по одному\nна строку):").grid( - row=2, column=0, sticky="ne", **pad) - dc_text = tk.Text(root, width=30, height=6) - dc_text.grid(row=2, column=1, sticky="w", **pad) - dc_text.insert("1.0", "\n".join(_config.get("dc_ip", []))) - - # Verbose - verbose_var = tk.BooleanVar(value=_config.get("verbose", False)) - tk.Checkbutton(root, text="Verbose logging", variable=verbose_var).grid( - row=3, column=1, sticky="w", **pad) - - def on_save(): - try: - host_val = host_var.get().strip() - port_val = int(port_var.get().strip()) - except ValueError: - messagebox.showerror("Ошибка", "Порт должен быть числом", parent=root) - return - - lines = [l.strip() for l in dc_text.get("1.0", "end").splitlines() if l.strip()] - try: - tg_ws_proxy.parse_dc_ip_list(lines) - except ValueError as exc: - messagebox.showerror("Ошибка", str(exc), parent=root) - return - - new_cfg = { - "host": host_val, - "port": port_val, - "dc_ip": lines, - "verbose": verbose_var.get(), - } - save_config(new_cfg) - _config.update(new_cfg) - log.info("Config saved: %s", new_cfg) - - if messagebox.askyesno( - "Перезапустить?", - "Настройки сохранены.\n\nПерезапустить прокси сейчас?", - parent=root, - ): - root.destroy() - restart_proxy() - else: - root.destroy() - - btn_frame = tk.Frame(root) - btn_frame.grid(row=4, column=0, columnspan=2, pady=10) - ttk.Button(btn_frame, text="Сохранить", command=on_save).pack(side="left", padx=5) - ttk.Button(btn_frame, text="Отмена", command=root.destroy).pack(side="left", padx=5) - - root.mainloop() - - -# ── first-run dialog ───────────────────────────────────────────────────────── - -def show_first_run(): - import tkinter as tk - from tkinter import ttk - - root = tk.Tk() - root.title("TG WS Proxy — Первый запуск") - root.resizable(False, False) - - msg = ( - "TG WS Proxy запущен!\n\n" - "Чтобы подключить Telegram Desktop:\n\n" - "1. Откройте Telegram → Настройки\n" - " → Продвинутые → Тип подключения → Прокси\n\n" - "2. Добавьте прокси:\n" - f" Тип: SOCKS5\n" - f" Сервер: {_config['host']}\n" - f" Порт: {_config['port']}\n" - " Логин/Пароль: пусто\n\n" - "Или нажмите «Открыть в Telegram» в меню строки меню." - ) - - tk.Label(root, text=msg, justify="left", padx=20, pady=10).pack() - ttk.Button(root, text="Понятно", command=root.destroy).pack(pady=10) - root.mainloop() - - FIRST_RUN_MARKER.touch() - - -# ── rumps app ──────────────────────────────────────────────────────────────── - -class TgWsProxyApp(rumps.App): - def __init__(self): - super().__init__( - name=APP_NAME, - title="🔵", # menu bar icon (emoji fallback) - quit_button=None, - ) - self.menu = self._build_menu() - - def _build_menu(self): - host = _config.get("host", "127.0.0.1") - port = _config.get("port", 1080) - dc_list = ", ".join( - f"DC{e.split(':')[0]}" for e in _config.get("dc_ip", []) - ) - items = [ - rumps.MenuItem("Открыть в Telegram", callback=self.open_in_telegram), - rumps.separator, - rumps.MenuItem(f"Прокси: {host}:{port} [{dc_list}]"), - rumps.MenuItem("Перезапустить прокси", callback=self.restart), - rumps.separator, - rumps.MenuItem("Настройки…", callback=self.settings), - rumps.MenuItem("Открыть логи", callback=self.open_logs), - rumps.separator, - rumps.MenuItem("Выход", callback=self.quit_app), - ] - return items - - # ── callbacks ──────────────────────────────────────────────────────────── - - def open_in_telegram(self, _sender): - host = _config.get("host", "127.0.0.1") - port = _config.get("port", 1080) - url = f"tg://socks?server={host}&port={port}" - webbrowser.open(url) - log.info("Opened telegram socks link: %s", url) - - def restart(self, _sender): - log.info("Restart requested from tray") - threading.Thread(target=restart_proxy, daemon=True).start() - rumps.notification( - APP_NAME, "Прокси перезапускается", "", sound=False - ) - - def settings(self, _sender): - threading.Thread(target=open_settings, daemon=True).start() - - def open_logs(self, _sender): - subprocess.Popen(["open", str(LOG_FILE)]) - - def quit_app(self, _sender): - global _exiting - _exiting = True - log.info("Quit requested") - stop_proxy() - _release_lock() - rumps.quit_application() - - -# ── entry point ─────────────────────────────────────────────────────────────── - -def main(): - global _config - - _ensure_dirs() - _config = load_config() - _setup_logging() - - log.info("TG WS Proxy tray app starting (macOS)") - - if not _acquire_lock(): - rumps.alert("TG WS Proxy уже запущен!") - sys.exit(0) - - log.info("Config: %s", _config) - log.info("Log file: %s", LOG_FILE) - - # ── Auto-update proxy core at startup ──────────────────────────────── - def _do_update(): - updated = updater.check_and_update() - if updated: - log.info("Proxy core updated — reloading and restarting proxy") - import importlib - global tg_ws_proxy - try: - import proxy.tg_ws_proxy as _fresh - importlib.reload(_fresh) - tg_ws_proxy = _fresh - except Exception as exc: - log.error("Failed to reload proxy core after update: %s", exc) - restart_proxy() - rumps.notification( - APP_NAME, - "Обновление установлено", - "Proxy core обновлён и перезапущен.", - sound=False, - ) - else: - start_proxy(_config) - - threading.Thread(target=_do_update, daemon=True).start() - # ───────────────────────────────────────────────────────────────────── - - if not FIRST_RUN_MARKER.exists(): - threading.Thread(target=show_first_run, daemon=True).start() - - app = TgWsProxyApp() - app.run() - - -if __name__ == "__main__": - main()