mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-22 02:47:14 +00:00
fix(mtproto): reap orphaned mtg, fix SysLog viewer, mtg log visibility, export remark (#5105) (#5107)
* fix(logs): render journalctl output in the SysLog viewer The log viewer's parseLogLine only understood the app-log format (2006/01/02 15:04:05 LEVEL - body). With SysLog ticked the backend returns journalctl lines (Mon DD HH:MM:SS host ident[pid]: LEVEL - body), so the parser mistook the journal time for the level and dropped the body, leaving only timestamps. Detect and strip the journald prefix, keep the journal timestamp as the stamp, then parse the real level and body from the remainder. * feat(mtproto): surface mtg output and add status reporting mtg's stdout/stderr was captured by a writer that kept only the last line and showed it nowhere, so the reason a proxy could not reach Telegram was invisible. Stream mtg output line-by-line into the x-ui log, tagged per inbound, so it appears in the panel log viewer and journald. Also fix mangled log lines: logger.Info uses fmt.Sprint, which drops the space between adjacent string operands, producing output like 'inbound3on0.0.0.0:8443'. Switch the affected mtproto calls to the formatted (*f) variants. Add show_mtproto_status to x-ui.sh so 'x-ui status' reports each mtproto inbound's mtg process state and bind address. * fix(logs): parse all journalctl message shapes in SysLog viewer Real journalctl output mixes four message shapes after the 'Mon DD HH:MM:SS host ident[pid]:' prefix: go-logging 'LEVEL - msg' (x-ui/xray), Go std-log with an embedded date (net/http, runtime), telego's '[timestamp] LEVEL msg', and systemd lines. The viewer only understood the first, so std-log and telego lines — which never contain ' - ' — collapsed to a bare timestamp (e.g. the 8s telego 409 spam). Extract the parser into a pure, testable module and teach it the other shapes: strip the redundant Go std-log date, lift the level out of telego brackets, and always keep the message body. Add a unit test covering each shape with real captured lines. * fix(mtproto): reap orphaned mtg sidecars so a stale one can't break new clients On Linux x-ui does not kill its mtg children when it dies (no kill-on-exit, unlike the Windows job object). After a crash, OOM, kill -9, or update, a stale mtg keeps holding the inbound port with an OLD secret, so new clients fail the FakeTLS handshake and get silently domain-fronted to the fakeTLS domain instead of proxied to Telegram (a few MB of traffic, never connects). Sweep orphans at startup: on the first reconcile, before x-ui starts any of its own mtg, scan /proc and SIGKILL any process whose executable is our mtg-<goos>-<goarch> binary. x-ui is the sole owner of mtg, so anything alive then is an orphan. Runs once per process (swept guard), survives the binary-deleted-during-update case via /proc/<pid>/cmdline, and is a no-op on Windows (job object) and other platforms. Also clear stray mtg in update.sh/install.sh after stopping x-ui, anchored to the 'mtg-linux-<arch> run ' invocation so the pattern can't match unrelated command lines (e.g. x-ui.sh's own 'grep mtg-linux'). * fix(logs): drop dead body initializer flagged by eslint no-useless-assignment * fix(mtproto): drop remark fragment from tg://proxy export link The mtproto export link appended the inbound remark as a URL fragment (tg://proxy?server=...&port=...&secret=...#remark). Telegram Desktop rejects a proxy deep link with a trailing fragment as 'This proxy link is invalid', breaking one-click import, and a remark is meaningless for proxy links across clients. Stop adding it in both the panel link (genMtprotoLink) and the subscription service. Fixes #5105. * fix(x-ui.sh): remove unused check_mtproto_status helper show_mtproto_status does its own process check, so check_mtproto_status was dead code. Drop it (per Copilot review on #5107).
This commit is contained in:
@@ -5,6 +5,7 @@ import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
|
||||
import { HttpUtil, FileManager, PromiseUtil } from '@/utils';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { parseLogLine } from './logParse';
|
||||
import './LogModal.css';
|
||||
|
||||
interface LogModalProps {
|
||||
@@ -12,50 +13,6 @@ interface LogModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface ParsedLog {
|
||||
date: string;
|
||||
time: string;
|
||||
stamp: string;
|
||||
levelText: string;
|
||||
levelClass: string;
|
||||
service: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const LEVELS = ['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR'];
|
||||
const LEVEL_CLASSES = ['level-debug', 'level-info', 'level-notice', 'level-warning', 'level-error'];
|
||||
|
||||
function parseLogLine(line: string): ParsedLog {
|
||||
const [head, ...rest] = (line || '').split(' - ');
|
||||
const message = rest.join(' - ');
|
||||
const parts = head.split(' ');
|
||||
|
||||
let date = '';
|
||||
let time = '';
|
||||
let levelText: string;
|
||||
if (parts.length >= 3) {
|
||||
[date, time, levelText] = parts;
|
||||
} else {
|
||||
levelText = head;
|
||||
}
|
||||
|
||||
const li = LEVELS.indexOf(levelText);
|
||||
const levelClass = li >= 0 ? LEVEL_CLASSES[li] : 'level-unknown';
|
||||
|
||||
let service = '';
|
||||
let body = message || '';
|
||||
if (body.startsWith('XRAY:')) {
|
||||
service = 'XRAY:';
|
||||
body = body.slice('XRAY:'.length).trimStart();
|
||||
} else if (body) {
|
||||
service = 'X-UI:';
|
||||
}
|
||||
|
||||
const stamp = [date, time].filter(Boolean).join(' ');
|
||||
|
||||
return { date, time, stamp, levelText, levelClass, service, body };
|
||||
}
|
||||
|
||||
export default function LogModal({ open, onClose }: LogModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Parser for the panel log viewer. Logs reach the UI in two shapes:
|
||||
//
|
||||
// - App log (SysLog off): the in-memory buffer, formatted as
|
||||
// "2006/01/02 15:04:05 LEVEL - message"
|
||||
// - SysLog (journalctl -o short): every entry is prefixed with
|
||||
// "Mon DD HH:MM:SS host ident[pid]: " before the real message, and the
|
||||
// message itself is one of several shapes depending on which subsystem
|
||||
// emitted it:
|
||||
// "INFO - mtproto: ..." go-logging (x-ui + xray)
|
||||
// "2026/06/08 19:22:22 http: ..." Go std log (net/http, runtime)
|
||||
// "[Mon Jun 8 23:56:52 UTC 2026] ERROR ..." telego bot
|
||||
// "Stopping x-ui.service - ..." systemd
|
||||
//
|
||||
// parseLogLine normalises all of these into a stamp + level + service + body so
|
||||
// the viewer renders a readable line instead of a bare timestamp.
|
||||
|
||||
export interface ParsedLog {
|
||||
date: string;
|
||||
time: string;
|
||||
stamp: string;
|
||||
levelText: string;
|
||||
levelClass: string;
|
||||
service: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export const LEVELS = ['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR'];
|
||||
export const LEVEL_CLASSES = [
|
||||
'level-debug',
|
||||
'level-info',
|
||||
'level-notice',
|
||||
'level-warning',
|
||||
'level-error',
|
||||
];
|
||||
|
||||
// "Mon DD HH:MM:SS host ident[pid]: <message>" — captures the journal date,
|
||||
// time, and the message that follows the syslog identifier.
|
||||
const SYSLOG_PREFIX = /^([A-Za-z]{3}\s+\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+\S+\s+\S+?:\s+(.*)$/;
|
||||
// Redundant Go std-log date prefix ("2006/01/02 15:04:05 ") to strip — the
|
||||
// journal already carries the timestamp.
|
||||
const GO_LOG_DATE = /^\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/;
|
||||
// telego's own line prefix: "[Mon Jan _2 15:04:05 MST 2006] LEVEL rest".
|
||||
const TELEGO = /^\[[^\]]+\]\s+([A-Z]+)\s+(.*)$/;
|
||||
|
||||
// splitLevelDash pulls a leading "LEVEL - " off a message, returning the level
|
||||
// and the remainder. Returns null when the message does not start with a level.
|
||||
function splitLevelDash(message: string): { level: string; rest: string } | null {
|
||||
const dash = message.indexOf(' - ');
|
||||
if (dash < 0) return null;
|
||||
const level = message.slice(0, dash).trim();
|
||||
if (LEVELS.indexOf(level) < 0) return null;
|
||||
return { level, rest: message.slice(dash + 3) };
|
||||
}
|
||||
|
||||
export function parseLogLine(line: string): ParsedLog {
|
||||
const raw = (line || '').trim();
|
||||
|
||||
let date = '';
|
||||
let time = '';
|
||||
let levelText = '';
|
||||
let body: string;
|
||||
|
||||
const sys = raw.match(SYSLOG_PREFIX);
|
||||
if (sys) {
|
||||
date = sys[1];
|
||||
time = sys[2];
|
||||
let message = sys[3];
|
||||
|
||||
const ld = splitLevelDash(message);
|
||||
if (ld) {
|
||||
// go-logging: "LEVEL - message"
|
||||
levelText = ld.level;
|
||||
body = ld.rest;
|
||||
} else {
|
||||
// Strip the redundant Go std-log date, then try to lift a level out of a
|
||||
// telego "[timestamp] LEVEL ..." line; otherwise keep the message as-is.
|
||||
message = message.replace(GO_LOG_DATE, '');
|
||||
const tg = message.match(TELEGO);
|
||||
if (tg && LEVELS.indexOf(tg[1]) >= 0) {
|
||||
levelText = tg[1];
|
||||
body = tg[2];
|
||||
} else {
|
||||
body = message;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// App-log format: "2006/01/02 15:04:05 LEVEL - body"
|
||||
const [head, ...rest] = raw.split(' - ');
|
||||
const message = rest.join(' - ');
|
||||
const parts = head.split(' ');
|
||||
if (parts.length >= 3) {
|
||||
[date, time, levelText] = parts;
|
||||
} else {
|
||||
levelText = head;
|
||||
}
|
||||
body = message || '';
|
||||
}
|
||||
|
||||
const li = LEVELS.indexOf(levelText);
|
||||
const levelClass = li >= 0 ? LEVEL_CLASSES[li] : 'level-unknown';
|
||||
|
||||
let service = '';
|
||||
if (body.startsWith('XRAY:')) {
|
||||
service = 'XRAY:';
|
||||
body = body.slice('XRAY:'.length).trimStart();
|
||||
} else if (body) {
|
||||
service = 'X-UI:';
|
||||
}
|
||||
|
||||
const stamp = [date, time].filter(Boolean).join(' ');
|
||||
|
||||
return { date, time, stamp, levelText, levelClass, service, body };
|
||||
}
|
||||
Reference in New Issue
Block a user