logrotate #366; configurable pool and buffer sizes

This commit is contained in:
Flowseal 2026-03-22 02:54:03 +03:00
parent ed85e2a284
commit 18a1bced83
4 changed files with 204 additions and 53 deletions

106
linux.py
View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio as _asyncio import asyncio as _asyncio
import json import json
import logging import logging
import logging.handlers
import os import os
import subprocess import subprocess
import sys import sys
@ -32,6 +33,9 @@ DEFAULT_CONFIG = {
"host": "127.0.0.1", "host": "127.0.0.1",
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False, "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) 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() _ensure_dirs()
root = logging.getLogger() root = logging.getLogger()
root.setLevel(logging.DEBUG if verbose else logging.INFO) 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.setLevel(logging.DEBUG)
fh.setFormatter( fh.setFormatter(
logging.Formatter( logging.Formatter(
@ -261,6 +270,13 @@ def start_proxy():
return return
log.info("Starting proxy on %s:%d ...", host, port) 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( _proxy_thread = threading.Thread(
target=_run_proxy_thread, target=_run_proxy_thread,
args=(port, dc_opt, verbose, host), args=(port, dc_opt, verbose, host),
@ -363,7 +379,7 @@ def _edit_config_dialog():
TEXT_SECONDARY = "#707579" TEXT_SECONDARY = "#707579"
FONT_FAMILY = "Sans" FONT_FAMILY = "Sans"
w, h = 420, 480 w, h = 420, 540
sw = root.winfo_screenwidth() sw = root.winfo_screenwidth()
sh = root.winfo_screenheight() sh = root.winfo_screenheight()
root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")
@ -455,14 +471,29 @@ def _edit_config_dialog():
border_color=FIELD_BORDER, border_color=FIELD_BORDER,
).pack(anchor="w", pady=(0, 8)) ).pack(anchor="w", pady=(0, 8))
# Info label # Advanced: buf_kb, pool_size, log_max_mb
ctk.CTkLabel( adv_frame = ctk.CTkFrame(frame, fg_color="transparent")
frame, adv_frame.pack(anchor="w", fill="x", pady=(4, 8))
text="Изменения вступят в силу после перезапуска прокси.",
font=(FONT_FAMILY, 11), for col, (lbl, key, w_) in enumerate([
text_color=TEXT_SECONDARY, ("Буфер (KB, 256 default)", "buf_kb", 120),
anchor="w", ("WS пулов (4 default)", "pool_size", 120),
).pack(anchor="w", pady=(0, 16)) ("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(): def on_save():
import socket as _sock import socket as _sock
@ -499,6 +530,17 @@ def _edit_config_dialog():
"dc_ip": lines, "dc_ip": lines,
"verbose": verbose_var.get(), "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) save_config(new_cfg)
_config.update(new_cfg) _config.update(new_cfg)
log.info("Config saved: %s", new_cfg) log.info("Config saved: %s", new_cfg)
@ -521,33 +563,18 @@ def _edit_config_dialog():
root.destroy() root.destroy()
btn_frame = ctk.CTkFrame(frame, fg_color="transparent") btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
btn_frame.pack(fill="x") btn_frame.pack(fill="x", pady=(20, 0))
ctk.CTkButton( ctk.CTkButton(btn_frame, text="Сохранить", height=38,
btn_frame, font=(FONT_FAMILY, 14, "bold"), corner_radius=10,
text="Сохранить", fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
width=140, text_color="#ffffff",
height=38, command=on_save).pack(side="left", fill="x", expand=True, padx=(0, 8))
font=(FONT_FAMILY, 14, "bold"), ctk.CTkButton(btn_frame, text="Отмена", height=38,
corner_radius=10, font=(FONT_FAMILY, 14), corner_radius=10,
fg_color=TG_BLUE, fg_color=FIELD_BG, hover_color=FIELD_BORDER,
hover_color=TG_BLUE_HOVER, text_color=TEXT_PRIMARY, border_width=1,
text_color="#ffffff", border_color=FIELD_BORDER,
command=on_save, command=on_cancel).pack(side="right", fill="x", expand=True)
).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")
root.mainloop() root.mainloop()
@ -798,7 +825,8 @@ def run_tray():
except Exception: except Exception:
pass 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("TG WS Proxy tray app starting")
log.info("Config: %s", _config) log.info("Config: %s", _config)
log.info("Log file: %s", LOG_FILE) log.info("Log file: %s", LOG_FILE)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
import logging import logging
import logging.handlers
import os import os
import psutil import psutil
import subprocess import subprocess
@ -43,6 +44,9 @@ DEFAULT_CONFIG = {
"host": "127.0.0.1", "host": "127.0.0.1",
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False, "verbose": False,
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4,
} }
_proxy_thread: Optional[threading.Thread] = None _proxy_thread: Optional[threading.Thread] = None
@ -153,12 +157,17 @@ def save_config(cfg: dict):
json.dump(cfg, f, indent=2, ensure_ascii=False) 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() _ensure_dirs()
root = logging.getLogger() root = logging.getLogger()
root.setLevel(logging.DEBUG if verbose else logging.INFO) 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.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter( fh.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)-5s %(name)s %(message)s", "%(asctime)s %(levelname)-5s %(name)s %(message)s",
@ -290,6 +299,13 @@ def start_proxy():
return return
log.info("Starting proxy on %s:%d ...", host, port) 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( _proxy_thread = threading.Thread(
target=_run_proxy_thread, target=_run_proxy_thread,
args=(port, dc_opt, verbose, host), args=(port, dc_opt, verbose, host),
@ -438,11 +454,34 @@ def _edit_config_dialog():
# Verbose # Verbose
verbose = _ask_yes_no("Включить подробное логирование (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 = { new_cfg = {
"host": host, "host": host,
"port": port, "port": port,
"dc_ip": dc_lines, "dc_ip": dc_lines,
"verbose": verbose, "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) save_config(new_cfg)
log.info("Config saved: %s", new_cfg) log.info("Config saved: %s", new_cfg)
@ -581,7 +620,8 @@ def run_menubar():
except Exception: except Exception:
pass 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("TG WS Proxy menubar app starting")
log.info("Config: %s", _config) log.info("Config: %s", _config)
log.info("Log file: %s", LOG_FILE) log.info("Log file: %s", LOG_FILE)

View File

@ -4,6 +4,7 @@ import argparse
import asyncio import asyncio
import base64 import base64
import logging import logging
import logging.handlers
import os import os
import socket as _socket import socket as _socket
import ssl import ssl
@ -144,6 +145,7 @@ _st_I_net = struct.Struct('!I')
_st_Ih = struct.Struct('<Ih') _st_Ih = struct.Struct('<Ih')
_st_I_le = struct.Struct('<I') _st_I_le = struct.Struct('<I')
_VALID_PROTOS = frozenset((0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD)) _VALID_PROTOS = frozenset((0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD))
_WS_PING_FRAME = _st_BB4s.pack(0x80 | 0x9, 0x80 | 0, os.urandom(4))
class RawWebSocket: class RawWebSocket:
@ -154,6 +156,7 @@ class RawWebSocket:
proxy), performs the HTTP Upgrade handshake, and provides send/recv proxy), performs the HTTP Upgrade handshake, and provides send/recv
for binary frames with proper masking, ping/pong, and close handling. for binary frames with proper masking, ping/pong, and close handling.
""" """
__slots__ = ('reader', 'writer', '_closed')
OP_CONTINUATION = 0x0 OP_CONTINUATION = 0x0
OP_TEXT = 0x1 OP_TEXT = 0x1
@ -670,8 +673,7 @@ async def _bridge_ws(reader, writer, ws: RawWebSocket, label,
idle = asyncio.get_event_loop().time() - last_recv_time idle = asyncio.get_event_loop().time() - last_recv_time
if idle >= 2 and not ws._closed: if idle >= 2 and not ws._closed:
try: try:
ws.writer.write( ws.writer.write(_WS_PING_FRAME)
ws._build_frame(ws.OP_PING, b'', mask=True))
await ws.writer.drain() await ws.writer.drain()
log.debug("[%s] %s WS PING (idle %.1fs)", log.debug("[%s] %s WS PING (idle %.1fs)",
label, dc_tag, idle) label, dc_tag, idle)
@ -1156,6 +1158,16 @@ def main():
' --dc-ip 2:149.154.167.220') ' --dc-ip 2:149.154.167.220')
ap.add_argument('-v', '--verbose', action='store_true', ap.add_argument('-v', '--verbose', action='store_true',
help='Debug logging') 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() args = ap.parse_args()
if not args.dc_ip: if not args.dc_ip:
@ -1167,11 +1179,30 @@ def main():
log.error(str(e)) log.error(str(e))
sys.exit(1) sys.exit(1)
logging.basicConfig( log_level = logging.DEBUG if args.verbose else logging.INFO
level=logging.DEBUG if args.verbose else logging.INFO, log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
format='%(asctime)s %(levelname)-5s %(message)s', datefmt='%H:%M:%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: try:
asyncio.run(_run(args.port, dc_opt, host=args.host)) asyncio.run(_run(args.port, dc_opt, host=args.host))

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import ctypes import ctypes
import json import json
import logging import logging
import logging.handlers
import os import os
import winreg import winreg
import psutil import psutil
@ -38,6 +39,9 @@ DEFAULT_CONFIG = {
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
"verbose": False, "verbose": False,
"autostart": 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) 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() _ensure_dirs()
root = logging.getLogger() root = logging.getLogger()
root.setLevel(logging.DEBUG if verbose else logging.INFO) 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.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter( fh.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)-5s %(name)s %(message)s", "%(asctime)s %(levelname)-5s %(name)s %(message)s",
@ -301,6 +310,13 @@ def start_proxy():
return return
log.info("Starting proxy on %s:%d ...", host, port) 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( _proxy_thread = threading.Thread(
target=_run_proxy_thread, target=_run_proxy_thread,
args=(port, dc_opt, verbose, host), args=(port, dc_opt, verbose, host),
@ -395,7 +411,7 @@ def _edit_config_dialog():
TEXT_SECONDARY = "#707579" TEXT_SECONDARY = "#707579"
FONT_FAMILY = "Segoe UI" FONT_FAMILY = "Segoe UI"
w, h = 420, 460 w, h = 420, 540
if _supports_autostart(): if _supports_autostart():
h += 70 h += 70
@ -450,6 +466,30 @@ def _edit_config_dialog():
corner_radius=6, border_width=2, corner_radius=6, border_width=2,
border_color=FIELD_BORDER).pack(anchor="w", pady=(0, 8)) 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 autostart_var = None
if _supports_autostart(): if _supports_autostart():
autostart_var = ctk.BooleanVar(value=cfg["autostart"]) autostart_var = ctk.BooleanVar(value=cfg["autostart"])
@ -495,6 +535,17 @@ def _edit_config_dialog():
"verbose": verbose_var.get(), "verbose": verbose_var.get(),
"autostart": (autostart_var.get() if autostart_var is not None else False), "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) save_config(new_cfg)
_config.update(new_cfg) _config.update(new_cfg)
log.info("Config saved: %s", new_cfg) log.info("Config saved: %s", new_cfg)
@ -740,7 +791,8 @@ def run_tray():
except Exception: except Exception:
pass 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("TG WS Proxy tray app starting")
log.info("Config: %s", _config) log.info("Config: %s", _config)
log.info("Log file: %s", LOG_FILE) log.info("Log file: %s", LOG_FILE)