feat(lite): add detailed runtime logs and publish 9999091817

This commit is contained in:
RoyirinRosmontis
2026-09-18 17:10:03 +08:00
parent 384a6107a8
commit 01de67610d
3 changed files with 230 additions and 7 deletions
+121 -3
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect, re, json, uuid
from datetime import datetime
from pathlib import Path
from PyQt5.QtWidgets import (
@@ -14,7 +15,8 @@ from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
from rrpp_ui.theme import C_TITLE, C_GREEN, C_YELLOW, C_RED, C_BG, C_CARD, C_BORDER, C_TEXT, C_SUBTEXT
from rrpp_ui.widgets import NodeLabel, StatusDot, ToggleSwitch
from rrpp_ui.dialogs import (
LoginDialog, LoginServices, SettingsDialog, SettingsServices, ThirdPartyDialog, ThirdPartyServices,
LoginDialog, LoginServices, SettingsDialog, SettingsServices, LogDialog, LogServices,
ThirdPartyDialog, ThirdPartyServices,
WhitelistDialog, WhitelistServices, FixedRouteDialog, FixedRouteServices,
JumpDialog, JumpServices,
)
@@ -933,17 +935,62 @@ def _stop_pac_server():
# ── 日志输出 ──────────────────────────────────────────
# GUI 初始化后会把 _log_func 设为 MainWindow.log_signal.emit
_log_func = None
_LOG_HISTORY_LIMIT = 2000
_LOG_HISTORY = []
_LOG_HISTORY_LOCK = threading.RLock()
def _plain_log_message(msg: str) -> str:
"""去掉红色警告日志的 HTML 包装,供日志弹窗显示纯文本。"""
return re.sub(r"<[^>]+>", "", str(msg))
def _record_log(msg: str) -> str:
"""记录带毫秒时间戳的日志并返回弹窗使用的纯文本行。"""
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
line = f"[{timestamp}] {_plain_log_message(msg)}"
with _LOG_HISTORY_LOCK:
_LOG_HISTORY.append(line)
if len(_LOG_HISTORY) > _LOG_HISTORY_LIMIT:
del _LOG_HISTORY[:-_LOG_HISTORY_LIMIT]
return line
def _log_history() -> list[str]:
"""返回日志快照,避免日志弹窗读取时阻塞连接线程。"""
with _LOG_HISTORY_LOCK:
return list(_LOG_HISTORY)
def log(msg: str):
"""输出一行日志到 GUI 日志区。"""
_record_log(msg)
if _log_func:
_log_func(msg)
def log_detail(msg: str):
"""仅写入详细日志历史,不打扰主界面的简要日志区。"""
_record_log(msg)
def log_red(msg: str):
"""输出一行红色警告日志到 GUI 日志区。"""
_record_log(msg)
if _log_func:
_log_func(f'<span style="color:{C_RED};">{msg}</span>')
_CONNECTION_ID = 0
_CONNECTION_ID_LOCK = threading.Lock()
def _next_connection_id() -> int:
"""为每条转发连接分配单调递增的进程内编号。"""
global _CONNECTION_ID
with _CONNECTION_ID_LOCK:
_CONNECTION_ID += 1
return _CONNECTION_ID
# ── 通用工具函数 ──────────────────────────────────────
def _run(cmd, warn=True):
@@ -2192,10 +2239,30 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
jump_chain=None):
"""处理一条客户端连接:内网(bypass_lan)/中国IP(智能分流)目标直接连接;其余走远端隧道。"""
import socket, select
connection_id = _next_connection_id()
started_at = time.monotonic()
try:
peer = client.getpeername()
peer_text = f"{peer[0]}:{peer[1]}" if isinstance(peer, tuple) and len(peer) >= 2 else str(peer)
except Exception:
peer_text = "未知"
def _chain_detail(chain):
return "→".join(
f"{node.get('domain') or '?'}:{_node_http_port(node)}"
for node in (chain or [])
)
client.settimeout(2.0)
buf = b""
is_http = None
is_connect = False
first_line = ""
target = None
request_type = "未知请求"
target_text = "未解析"
rule_match = "否"
route_summary = f"上游节点 {_chain_detail(jump_chain) or f'{remote_host}:{remote_port}'}"
injector = None
direct_target = None
allow_direct_fallback = False
@@ -2226,16 +2293,24 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
if is_http and b"\r\n\r\n" in buf:
is_connect = buf.startswith(b"CONNECT ")
first_line = buf.split(b"\r\n", 1)[0].decode("latin-1", "replace")
request_type = "CONNECT" if is_connect else (first_line.split(" ", 1)[0] or "HTTP")
if is_connect:
target = _parse_connect_authority(first_line)
else:
target = _parse_http_absolute(first_line)
if target:
target_text = f"{target[0]}:{target[1]}"
log_detail(f"[连接 #{connection_id}] 收到请求: {request_type} {target_text},客户端={peer_text}")
forced_rule = _route_rule_for_host(target[0])
if forced_rule and forced_rule.get("action") == "direct":
rule_match = f"是({forced_rule.get('domain') or target[0]},强制直连)"
log_detail(f"[连接 #{connection_id}] 规则匹配: {rule_match}")
# 显式强制不代理优先于 PAC 的域名表和 IP 判定。
direct_target = (target[0], target[1])
route_summary = f"强制直连 {target_text}"
elif forced_rule and forced_rule.get("action") == "node":
rule_match = f"是({forced_rule.get('domain') or target[0]},强制节点)"
log_detail(f"[连接 #{connection_id}] 规则匹配: {rule_match}")
forced_node = _fixed_route_node(forced_rule)
if forced_node is None:
raise RuntimeError(
@@ -2247,22 +2322,44 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
route_jump_chain = _jump_chain(forced_node)
# 固定节点规则必须走指定节点,不允许 PAC 把目标改为直连。
direct_target = None
route_summary = (
f"强制多跳 {_chain_detail(route_jump_chain)}"
if route_jump_chain else
f"强制节点 {route_remote_host}:{route_remote_port}"
)
else:
log_detail(f"[连接 #{connection_id}] 规则匹配: 否(未命中自定义域名路由)")
direct_target = _direct_connect_target(
target[0], target[1], bypass_lan, smart)
allow_direct_fallback = (direct_target is not None and
_is_smart_china_direct(target[0], bypass_lan, smart))
if direct_target:
policy = "PAC/智能分流直连" if allow_direct_fallback else "直连策略"
route_summary = f"{policy} {direct_target[0]}:{direct_target[1]}"
else:
route_summary = (
f"多跳上游 {_chain_detail(route_jump_chain)}"
if route_jump_chain else
f"上游节点 {route_remote_host}:{route_remote_port}"
)
log_detail(f"[连接 #{connection_id}] 路由决策: {route_summary},TLS={'是' if use_tls else '否'}")
else:
log_detail(f"[连接 #{connection_id}] 无法解析目标: {first_line or '空请求'},客户端={peer_text}")
except Exception as e:
log(f"[转发器] 读取首包失败: {e}")
log_detail(f"[连接 #{connection_id}] 失败: 读取首包,目标={target_text},规则匹配={rule_match},错误={e}")
client.close()
return
remote = None
up_bytes = 0
down_bytes = 0
try:
remote, via_tunnel = _connect_route(
direct_target, allow_direct_fallback, route_remote_host, route_remote_port, ctx, use_tls,
jump_chain=route_jump_chain)
count_traffic = via_tunnel
log_detail(f"[连接 #{connection_id}] 连接成功: 目标={target_text},路径={route_summary},承载={'隧道' if via_tunnel else '直连'}")
if not via_tunnel:
if is_connect:
client.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
@@ -2278,10 +2375,12 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
)
if buf:
remote.sendall(buf)
up_bytes += len(buf)
if via_tunnel:
_traffic_add(up=len(buf))
except Exception as e:
log(f"[转发器] 连接目标失败: {e}")
log_detail(f"[连接 #{connection_id}] 连接失败: 目标={target_text},规则匹配={rule_match},路径={route_summary},错误={e}")
try:
client.close()
except Exception:
@@ -2298,6 +2397,7 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
# CONNECT 隧道承载 WebSocket / 推送等长连接,空闲属正常,不设超时;
# 明文 HTTP 连接空闲 120 秒回收
idle_timeout = None if is_connect else 120
close_reason = "对端关闭"
try:
while True:
if isinstance(remote, _LayeredTlsTransport):
@@ -2306,33 +2406,40 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
if data is None:
break
if not data:
close_reason = "上游关闭"
return
client.sendall(data)
down_bytes += len(data)
if count_traffic:
_traffic_add(down=len(data))
remote_wait = remote.wait_socket() if hasattr(remote, "wait_socket") else remote
r, _, _ = select.select([client, remote_wait], [], [], idle_timeout)
if not r:
close_reason = f"空闲超时({idle_timeout} 秒)"
break
if client in r:
data = client.recv(65536)
if not data:
close_reason = "客户端关闭"
break
if injector is not None:
data = injector(data)
if data:
remote.sendall(data)
up_bytes += len(data)
if count_traffic:
_traffic_add(up=len(data))
if remote_wait in r:
data = remote.recv(65536)
if not data:
close_reason = "上游关闭"
break
client.sendall(data)
down_bytes += len(data)
if count_traffic:
_traffic_add(down=len(data))
except Exception:
pass
except Exception as e:
close_reason = f"转发异常: {e}"
finally:
try:
client.close()
@@ -2342,6 +2449,12 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
remote.close()
except Exception:
pass
elapsed = time.monotonic() - started_at
log_detail(
f"[连接 #{connection_id}] 已关闭: 目标={target_text},规则匹配={rule_match},"
f"原因={close_reason},上行={_fmt_bytes(up_bytes)},下行={_fmt_bytes(down_bytes)},"
f"持续={elapsed:.3f} 秒"
)
def _tls_stop():
"""停止 TLS 转发器监听线程。"""
global _tls_forwarder
@@ -2478,6 +2591,10 @@ def _open_login_dialog(parent=None):
return False
def _log_services():
return LogServices(load_icon=load_icon, get_logs=_log_history)
def _logout_user_data():
"""清除官方登录与节点缓存;第三方订阅保持不变。"""
global _NODES
@@ -2504,6 +2621,7 @@ def _settings_services():
open_whitelist=lambda parent: FixedRouteDialog(_fixed_route_services(), parent).exec_(),
open_jump=lambda parent: JumpDialog(_jump_services(), parent).exec_(),
open_third_party=lambda parent: ThirdPartyDialog(_third_party_services(), parent).exec_(),
open_logs=lambda parent: LogDialog(_log_services(), parent).exec_(),
has_login=lambda: _auth_load() is not None,
open_login=_open_login_dialog,
logout=_logout_user_data,