diff --git a/linux.py b/linux.py index 9b2ff9e..664c948 100644 --- a/linux.py +++ b/linux.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio as _asyncio import json import logging +import logging.handlers import os import subprocess import sys @@ -32,6 +33,9 @@ DEFAULT_CONFIG = { "host": "127.0.0.1", "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], "verbose": False, + "log_max_mb": 5, + "buf_kb": 256, + "pool_size": 4, } @@ -149,12 +153,17 @@ def save_config(cfg: dict): json.dump(cfg, f, indent=2, ensure_ascii=False) -def setup_logging(verbose: bool = False): +def setup_logging(verbose: bool = False, log_max_mb: float = 5): _ensure_dirs() root = logging.getLogger() root.setLevel(logging.DEBUG if verbose else logging.INFO) - fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") + fh = logging.handlers.RotatingFileHandler( + str(LOG_FILE), + maxBytes=max(32 * 1024, log_max_mb * 1024 * 1024), + backupCount=0, + encoding='utf-8', + ) fh.setLevel(logging.DEBUG) fh.setFormatter( logging.Formatter( @@ -261,6 +270,13 @@ def start_proxy(): return log.info("Starting proxy on %s:%d ...", host, port) + + buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"]) + pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"]) + tg_ws_proxy._RECV_BUF = max(4, buf_kb) * 1024 + tg_ws_proxy._SEND_BUF = tg_ws_proxy._RECV_BUF + tg_ws_proxy._WS_POOL_SIZE = max(0, pool_size) + _proxy_thread = threading.Thread( target=_run_proxy_thread, args=(port, dc_opt, verbose, host), @@ -363,7 +379,7 @@ def _edit_config_dialog(): TEXT_SECONDARY = "#707579" FONT_FAMILY = "Sans" - w, h = 420, 480 + w, h = 420, 540 sw = root.winfo_screenwidth() sh = root.winfo_screenheight() root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") @@ -455,14 +471,29 @@ def _edit_config_dialog(): border_color=FIELD_BORDER, ).pack(anchor="w", pady=(0, 8)) - # Info label - ctk.CTkLabel( - frame, - text="Изменения вступят в силу после перезапуска прокси.", - font=(FONT_FAMILY, 11), - text_color=TEXT_SECONDARY, - anchor="w", - ).pack(anchor="w", pady=(0, 16)) + # Advanced: buf_kb, pool_size, log_max_mb + adv_frame = ctk.CTkFrame(frame, fg_color="transparent") + adv_frame.pack(anchor="w", fill="x", pady=(4, 8)) + + for col, (lbl, key, w_) in enumerate([ + ("Буфер (KB, 256 default)", "buf_kb", 120), + ("WS пулов (4 default)", "pool_size", 120), + ("Log size (MB, 5 def)", "log_max_mb", 120), + ]): + col_frame = ctk.CTkFrame(adv_frame, fg_color="transparent") + col_frame.pack(side="left", padx=(0, 10)) + ctk.CTkLabel(col_frame, text=lbl, font=(FONT_FAMILY, 11), + text_color=TEXT_SECONDARY, anchor="w").pack(anchor="w") + ctk.CTkEntry(col_frame, width=w_, height=30, font=(FONT_FAMILY, 12), + corner_radius=8, fg_color=FIELD_BG, + border_color=FIELD_BORDER, border_width=1, + text_color=TEXT_PRIMARY, + textvariable=ctk.StringVar( + value=str(cfg.get(key, DEFAULT_CONFIG[key])) + )).pack(anchor="w") + + _adv_entries = list(adv_frame.winfo_children()) + _adv_keys = ["buf_kb", "pool_size", "log_max_mb"] def on_save(): import socket as _sock @@ -499,6 +530,17 @@ def _edit_config_dialog(): "dc_ip": lines, "verbose": verbose_var.get(), } + + for i, key in enumerate(_adv_keys): + col_frame = _adv_entries[i] + entry = col_frame.winfo_children()[1] + try: + val = float(entry.get().strip()) + if key in ("buf_kb", "pool_size"): + val = int(val) + new_cfg[key] = val + except ValueError: + new_cfg[key] = DEFAULT_CONFIG[key] save_config(new_cfg) _config.update(new_cfg) log.info("Config saved: %s", new_cfg) @@ -521,33 +563,18 @@ def _edit_config_dialog(): root.destroy() btn_frame = ctk.CTkFrame(frame, fg_color="transparent") - btn_frame.pack(fill="x") - ctk.CTkButton( - btn_frame, - text="Сохранить", - width=140, - height=38, - font=(FONT_FAMILY, 14, "bold"), - corner_radius=10, - fg_color=TG_BLUE, - hover_color=TG_BLUE_HOVER, - text_color="#ffffff", - command=on_save, - ).pack(side="left", padx=(0, 10)) - ctk.CTkButton( - btn_frame, - text="Отмена", - width=140, - height=38, - font=(FONT_FAMILY, 14), - corner_radius=10, - fg_color=FIELD_BG, - hover_color=FIELD_BORDER, - text_color=TEXT_PRIMARY, - border_width=1, - border_color=FIELD_BORDER, - command=on_cancel, - ).pack(side="left") + btn_frame.pack(fill="x", pady=(20, 0)) + ctk.CTkButton(btn_frame, text="Сохранить", height=38, + font=(FONT_FAMILY, 14, "bold"), corner_radius=10, + fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER, + text_color="#ffffff", + command=on_save).pack(side="left", fill="x", expand=True, padx=(0, 8)) + ctk.CTkButton(btn_frame, text="Отмена", height=38, + font=(FONT_FAMILY, 14), corner_radius=10, + fg_color=FIELD_BG, hover_color=FIELD_BORDER, + text_color=TEXT_PRIMARY, border_width=1, + border_color=FIELD_BORDER, + command=on_cancel).pack(side="right", fill="x", expand=True) root.mainloop() @@ -798,7 +825,8 @@ def run_tray(): except Exception: pass - setup_logging(_config.get("verbose", False)) + setup_logging(_config.get("verbose", False), + log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])) log.info("TG WS Proxy tray app starting") log.info("Config: %s", _config) log.info("Log file: %s", LOG_FILE) diff --git a/macos.py b/macos.py index e1806cf..8241b38 100644 --- a/macos.py +++ b/macos.py @@ -2,6 +2,7 @@ from __future__ import annotations import json import logging +import logging.handlers import os import psutil import subprocess @@ -43,6 +44,9 @@ DEFAULT_CONFIG = { "host": "127.0.0.1", "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], "verbose": False, + "log_max_mb": 5, + "buf_kb": 256, + "pool_size": 4, } _proxy_thread: Optional[threading.Thread] = None @@ -153,12 +157,17 @@ def save_config(cfg: dict): json.dump(cfg, f, indent=2, ensure_ascii=False) -def setup_logging(verbose: bool = False): +def setup_logging(verbose: bool = False, log_max_mb: float = 5): _ensure_dirs() root = logging.getLogger() root.setLevel(logging.DEBUG if verbose else logging.INFO) - fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") + fh = logging.handlers.RotatingFileHandler( + str(LOG_FILE), + maxBytes=max(32 * 1024, log_max_mb * 1024 * 1024), + backupCount=0, + encoding='utf-8', + ) fh.setLevel(logging.DEBUG) fh.setFormatter(logging.Formatter( "%(asctime)s %(levelname)-5s %(name)s %(message)s", @@ -290,6 +299,13 @@ def start_proxy(): return log.info("Starting proxy on %s:%d ...", host, port) + + buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"]) + pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"]) + tg_ws_proxy._RECV_BUF = max(4, buf_kb) * 1024 + tg_ws_proxy._SEND_BUF = tg_ws_proxy._RECV_BUF + tg_ws_proxy._WS_POOL_SIZE = max(0, pool_size) + _proxy_thread = threading.Thread( target=_run_proxy_thread, args=(port, dc_opt, verbose, host), @@ -438,11 +454,34 @@ def _edit_config_dialog(): # Verbose verbose = _ask_yes_no("Включить подробное логирование (verbose)?") + # Advanced settings + adv_str = _osascript_input( + "Расширенные настройки (буфер KB, WS пул, лог MB):\n" + "Формат: buf_kb,pool_size,log_max_mb", + f"{cfg.get('buf_kb', DEFAULT_CONFIG['buf_kb'])}," + f"{cfg.get('pool_size', DEFAULT_CONFIG['pool_size'])}," + f"{cfg.get('log_max_mb', DEFAULT_CONFIG['log_max_mb'])}") + + adv = {} + if adv_str: + parts = [s.strip() for s in adv_str.split(',')] + keys = [("buf_kb", int), ("pool_size", int), + ("log_max_mb", float)] + for i, (k, typ) in enumerate(keys): + if i < len(parts): + try: + adv[k] = typ(parts[i]) + except ValueError: + pass + new_cfg = { "host": host, "port": port, "dc_ip": dc_lines, "verbose": verbose, + "buf_kb": adv.get("buf_kb", cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])), + "pool_size": adv.get("pool_size", cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])), + "log_max_mb": adv.get("log_max_mb", cfg.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])), } save_config(new_cfg) log.info("Config saved: %s", new_cfg) @@ -581,7 +620,8 @@ def run_menubar(): except Exception: pass - setup_logging(_config.get("verbose", False)) + setup_logging(_config.get("verbose", False), + log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])) log.info("TG WS Proxy menubar app starting") log.info("Config: %s", _config) log.info("Log file: %s", LOG_FILE) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index 934ce17..0c5427b 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -4,6 +4,7 @@ import argparse import asyncio import base64 import logging +import logging.handlers import os import socket as _socket import ssl @@ -144,6 +145,7 @@ _st_I_net = struct.Struct('!I') _st_Ih = struct.Struct('= 2 and not ws._closed: try: - ws.writer.write( - ws._build_frame(ws.OP_PING, b'', mask=True)) + ws.writer.write(_WS_PING_FRAME) await ws.writer.drain() log.debug("[%s] %s WS PING (idle %.1fs)", label, dc_tag, idle) @@ -1156,6 +1158,16 @@ def main(): ' --dc-ip 2:149.154.167.220') ap.add_argument('-v', '--verbose', action='store_true', help='Debug logging') + ap.add_argument('--log-file', type=str, default=None, metavar='PATH', + help='Log to file with rotation (default: stderr only)') + ap.add_argument('--log-max-mb', type=float, default=5, metavar='MB', + help='Max log file size in MB before rotation (default 5)') + ap.add_argument('--log-backups', type=int, default=0, metavar='N', + help='Number of rotated log files to keep (default 0)') + ap.add_argument('--buf-kb', type=int, default=256, metavar='KB', + help='Socket send/recv buffer size in KB (default 256)') + ap.add_argument('--pool-size', type=int, default=4, metavar='N', + help='WS connection pool size per DC (default 4, min 0)') args = ap.parse_args() if not args.dc_ip: @@ -1167,11 +1179,30 @@ def main(): log.error(str(e)) sys.exit(1) - logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.INFO, - format='%(asctime)s %(levelname)-5s %(message)s', - datefmt='%H:%M:%S', - ) + log_level = logging.DEBUG if args.verbose else logging.INFO + log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s', + datefmt='%H:%M:%S') + root = logging.getLogger() + root.setLevel(log_level) + + console = logging.StreamHandler() + console.setFormatter(log_fmt) + root.addHandler(console) + + if args.log_file: + fh = logging.handlers.RotatingFileHandler( + args.log_file, + maxBytes=max(32 * 1024, args.log_max_mb * 1024 * 1024), + backupCount=max(0, args.log_backups), + encoding='utf-8', + ) + fh.setFormatter(log_fmt) + root.addHandler(fh) + + global _RECV_BUF, _SEND_BUF, _WS_POOL_SIZE + _RECV_BUF = max(4, args.buf_kb) * 1024 + _SEND_BUF = _RECV_BUF + _WS_POOL_SIZE = max(0, args.pool_size) try: asyncio.run(_run(args.port, dc_opt, host=args.host)) diff --git a/windows.py b/windows.py index 568fce0..6eaad3f 100644 --- a/windows.py +++ b/windows.py @@ -3,6 +3,7 @@ from __future__ import annotations import ctypes import json import logging +import logging.handlers import os import winreg import psutil @@ -38,6 +39,9 @@ DEFAULT_CONFIG = { "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], "verbose": False, "autostart": False, + "log_max_mb": 5, + "buf_kb": 256, + "pool_size": 4, } @@ -148,12 +152,17 @@ def save_config(cfg: dict): json.dump(cfg, f, indent=2, ensure_ascii=False) -def setup_logging(verbose: bool = False): +def setup_logging(verbose: bool = False, log_max_mb: float = 5): _ensure_dirs() root = logging.getLogger() root.setLevel(logging.DEBUG if verbose else logging.INFO) - fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") + fh = logging.handlers.RotatingFileHandler( + str(LOG_FILE), + maxBytes=max(32 * 1024, log_max_mb * 1024 * 1024), + backupCount=0, + encoding='utf-8', + ) fh.setLevel(logging.DEBUG) fh.setFormatter(logging.Formatter( "%(asctime)s %(levelname)-5s %(name)s %(message)s", @@ -301,6 +310,13 @@ def start_proxy(): return log.info("Starting proxy on %s:%d ...", host, port) + + buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"]) + pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"]) + tg_ws_proxy._RECV_BUF = max(4, buf_kb) * 1024 + tg_ws_proxy._SEND_BUF = tg_ws_proxy._RECV_BUF + tg_ws_proxy._WS_POOL_SIZE = max(0, pool_size) + _proxy_thread = threading.Thread( target=_run_proxy_thread, args=(port, dc_opt, verbose, host), @@ -395,7 +411,7 @@ def _edit_config_dialog(): TEXT_SECONDARY = "#707579" FONT_FAMILY = "Segoe UI" - w, h = 420, 460 + w, h = 420, 540 if _supports_autostart(): h += 70 @@ -450,6 +466,30 @@ def _edit_config_dialog(): corner_radius=6, border_width=2, border_color=FIELD_BORDER).pack(anchor="w", pady=(0, 8)) + # Advanced: buf_kb, pool_size, log_max_mb + adv_frame = ctk.CTkFrame(frame, fg_color="transparent") + adv_frame.pack(anchor="w", fill="x", pady=(4, 8)) + + for col, (lbl, key, w_) in enumerate([ + ("Буфер (KB, 256 default)", "buf_kb", 120), + ("WS пулов (4 default)", "pool_size", 120), + ("Log size (MB, 5 def)", "log_max_mb", 120), + ]): + col_frame = ctk.CTkFrame(adv_frame, fg_color="transparent") + col_frame.pack(side="left", padx=(0, 10)) + ctk.CTkLabel(col_frame, text=lbl, font=(FONT_FAMILY, 11), + text_color=TEXT_SECONDARY, anchor="w").pack(anchor="w") + ctk.CTkEntry(col_frame, width=w_, height=30, font=(FONT_FAMILY, 12), + corner_radius=8, fg_color=FIELD_BG, + border_color=FIELD_BORDER, border_width=1, + text_color=TEXT_PRIMARY, + textvariable=ctk.StringVar( + value=str(cfg.get(key, DEFAULT_CONFIG[key])) + )).pack(anchor="w") + + _adv_entries = list(adv_frame.winfo_children()) + _adv_keys = ["buf_kb", "pool_size", "log_max_mb"] + autostart_var = None if _supports_autostart(): autostart_var = ctk.BooleanVar(value=cfg["autostart"]) @@ -495,6 +535,17 @@ def _edit_config_dialog(): "verbose": verbose_var.get(), "autostart": (autostart_var.get() if autostart_var is not None else False), } + + for i, key in enumerate(_adv_keys): + col_frame = _adv_entries[i] + entry = col_frame.winfo_children()[1] + try: + val = float(entry.get().strip()) + if key in ("buf_kb", "pool_size"): + val = int(val) + new_cfg[key] = val + except ValueError: + new_cfg[key] = DEFAULT_CONFIG[key] save_config(new_cfg) _config.update(new_cfg) log.info("Config saved: %s", new_cfg) @@ -740,7 +791,8 @@ def run_tray(): except Exception: pass - setup_logging(_config.get("verbose", False)) + setup_logging(_config.get("verbose", False), + log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])) log.info("TG WS Proxy tray app starting") log.info("Config: %s", _config) log.info("Log file: %s", LOG_FILE)