Delete macos.py
This commit is contained in:
parent
01396dae78
commit
2b9e6c8b48
401
macos.py
401
macos.py
|
|
@ -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()
|
|
||||||
Loading…
Reference in New Issue