refactor(watchdog): оптимизация и устранение дублирования кода
- watchdog: удалён дублирующий check_tachyon_cli_running(), везде используется controller.check_tachyon_cli_running() - watchdog: smart_detect_process_pending использует controller.proxy_port() вместо повторного парсинга config.json - watchdog: удалена однострочная обёртка is_dns_dead() — заменена is_dns_working() напрямую - watchdog: heal_proxy_connectivity требует streak >= 2 перед очисткой cache.db — устраняет ложные ночные уведомления при кратких просадках прокси-ноды - event_controller: push_history переведён на slice() вместо ручного цикла - event_controller: direct_ok использует тот же check_url что и proxy check — корректность при кастомном ai_proxy_health_url - event_controller: добавлен subscription_fail_streak — одиночный таймаут сети больше не триггерит Telegram - telegram: tg_request больше не создаёт временные файлы в /tmp — JSON передаётся напрямую через curl -d, устранена утечка tg_payload_*.json при kill процесса - telegram: worker() при старте удаляет оставшиеся tg_payload_*.json от предыдущих сессий - tests: исправлен шаблон mktemp в background_spawn_fds.sh — busybox не поддерживает двойное расширение (.XXXXXX.uc)
This commit is contained in:
@@ -358,7 +358,8 @@ function controller(bus, opts) {
|
||||
syslog_start_time: 0,
|
||||
logread_pipe_fd: -1,
|
||||
last_oom_time: 0,
|
||||
healthy_streak: 0
|
||||
healthy_streak: 0,
|
||||
subscription_fail_streak: 0
|
||||
};
|
||||
|
||||
let self = { EV: EV, state: state };
|
||||
@@ -368,10 +369,7 @@ function controller(bus, opts) {
|
||||
function push_history(history, entry) {
|
||||
push(history, entry);
|
||||
if (length(history) > 20) {
|
||||
let trimmed = [];
|
||||
for (let i = length(history) - 20; i < length(history); i++)
|
||||
push(trimmed, history[i]);
|
||||
return trimmed;
|
||||
return slice(history, length(history) - 20);
|
||||
}
|
||||
return history;
|
||||
}
|
||||
@@ -525,9 +523,12 @@ function controller(bus, opts) {
|
||||
|
||||
// Distinguish "proxy is broken" from "the whole uplink is down": only
|
||||
// the former is worth restarting sing-box over.
|
||||
// Use the same URL as the proxy check so the two measurements are
|
||||
// comparable: if the user changed ai_proxy_health_url, direct_ok
|
||||
// must probe the same target, not always fall back to Cloudflare.
|
||||
let direct_ok = command_success_from_args([
|
||||
"curl", "-s", "-I", "--connect-timeout", "3", "--max-time", "5",
|
||||
"https://cp.cloudflare.com/generate_204"
|
||||
check_url
|
||||
]);
|
||||
|
||||
bus.emit(EV.PROXY_DOWN, {
|
||||
@@ -765,9 +766,16 @@ function controller(bus, opts) {
|
||||
let res = command_capture(
|
||||
"curl -s -o /dev/null -w %{http_code} --connect-timeout 10 " + shell_quote(sub_url) + " 2>&1");
|
||||
let code = int(res.output);
|
||||
if (res.status == 0 && code >= 200 && code < 400) return;
|
||||
if (res.status == 0 && code >= 200 && code < 400) {
|
||||
state.subscription_fail_streak = 0;
|
||||
return;
|
||||
}
|
||||
// Two consecutive failures before alerting: a single network hiccup
|
||||
// (provider momentarily unreachable) must not trigger a Telegram message.
|
||||
state.subscription_fail_streak++;
|
||||
if (state.subscription_fail_streak < 2) return;
|
||||
|
||||
bus.emit(EV.SUBSCRIPTION_UNREACHABLE, { url: sub_url, code: code });
|
||||
bus.emit(EV.SUBSCRIPTION_UNREACHABLE, { url: sub_url, code: code, streak: state.subscription_fail_streak });
|
||||
}
|
||||
|
||||
// ── Probe: UCI config integrity ───────────────────────────────────────────
|
||||
|
||||
@@ -132,22 +132,18 @@ function get_proxy_args() {
|
||||
function tg_request(token, method, payload) {
|
||||
if (!token) return null;
|
||||
let url = "https://api.telegram.org/bot" + token + "/" + method;
|
||||
let payload_path = "/tmp/tg_payload_" + method + "_" + time() + "_" + clock()[1] + ".json";
|
||||
|
||||
let res = null;
|
||||
try {
|
||||
fs.writefile(payload_path, sprintf("%J", payload));
|
||||
let args = [ "curl", "-s", "-m", "35", "--connect-timeout", "10", "-X", "POST", "-H", "Content-Type: application/json", "-d", "@" + payload_path ];
|
||||
let proxy = get_proxy_args();
|
||||
for (let p in proxy) push(args, p);
|
||||
push(args, url);
|
||||
res = command_capture(command_from_args(args));
|
||||
} catch (e) {
|
||||
try { fs.unlink(payload_path); } catch(err) {}
|
||||
return null;
|
||||
}
|
||||
try { fs.unlink(payload_path); } catch(err) {}
|
||||
|
||||
// Pass the JSON body directly to curl's -d argument so there is no temp
|
||||
// file to leak: the old /tmp/tg_payload_*.json files were left behind
|
||||
// whenever the process was killed mid-request (e.g. during the 20-second
|
||||
// getUpdates long-poll on reboot or watchdog restart).
|
||||
let body = sprintf("%J", payload);
|
||||
let args = [ "curl", "-s", "-m", "35", "--connect-timeout", "10",
|
||||
"-X", "POST", "-H", "Content-Type: application/json",
|
||||
"-d", body ];
|
||||
let proxy = get_proxy_args();
|
||||
for (let p in proxy) push(args, p);
|
||||
push(args, url);
|
||||
let res = command_capture(command_from_args(args));
|
||||
if (!res || res.status != 0 || res.output == "") return null;
|
||||
try { return json(res.output); } catch (e) { return null; }
|
||||
}
|
||||
@@ -2645,6 +2641,18 @@ function worker() {
|
||||
];
|
||||
tg_request(cfg.bot_token, "setMyCommands", { commands: commands });
|
||||
|
||||
// Clean up leftover payload temp-files from previous runs that were
|
||||
// interrupted (e.g. killed during a 20-second getUpdates long-poll).
|
||||
let tmp = fs.opendir("/tmp");
|
||||
if (tmp) {
|
||||
let entry;
|
||||
while ((entry = tmp.read()) != null) {
|
||||
if (index(entry, "tg_payload_") == 0 && substr(entry, -5) == ".json")
|
||||
try { fs.unlink("/tmp/" + entry); } catch(e) {}
|
||||
}
|
||||
tmp.close();
|
||||
}
|
||||
|
||||
let poll_interval = int(cfg.poll_interval || "5");
|
||||
if (poll_interval < 1) poll_interval = 1;
|
||||
|
||||
|
||||
@@ -381,26 +381,6 @@ function dns_consecutive_fails() { return controller.dns_consecutive_fails(); }
|
||||
function proxy_latency_history() { return controller.proxy_latency_history(); }
|
||||
function dns_latency_history() { return controller.dns_latency_history(); }
|
||||
|
||||
function check_tachyon_cli_running() {
|
||||
let running = false;
|
||||
let proc = fs.opendir("/proc");
|
||||
if (proc) {
|
||||
let entry;
|
||||
while ((entry = proc.read()) != null) {
|
||||
if (match(entry, /^[0-9]+$/)) {
|
||||
let cmdline = fs.readfile("/proc/" + entry + "/cmdline") || "";
|
||||
if (index(cmdline, "/usr/bin/tachyon") >= 0) {
|
||||
if (index(cmdline, "start") >= 0 || index(cmdline, "restart") >= 0 || index(cmdline, "reload") >= 0 || index(cmdline, "stop") >= 0) {
|
||||
running = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
proc.close();
|
||||
}
|
||||
return running;
|
||||
}
|
||||
|
||||
// ─── Root-cause suppression ───────────────────────────────────────────────────
|
||||
// A dead WAN takes the proxy, DNS and the health check down with it. Each of
|
||||
@@ -535,7 +515,7 @@ function heal_singbox_stopped(ev) {
|
||||
let cfg = settings();
|
||||
if (cfg.recovery_bypass == "1") return;
|
||||
if (check_auto_resume_pause()) return;
|
||||
if (check_tachyon_cli_running()) return;
|
||||
if (controller.check_tachyon_cli_running()) return;
|
||||
|
||||
let list_update_pid = trim(fs.readfile("/var/run/tachyon_list_update.pid") || "");
|
||||
if (process_running(list_update_pid, "ucode")) return;
|
||||
@@ -678,7 +658,7 @@ function note_dns_recovered(ev) {
|
||||
// Guard: skip if a tachyon reload is already in progress (prevents concurrent reload_firewall races)
|
||||
function is_reload_in_progress() {
|
||||
return fs.stat("/var/run/tachyon.reload.lock") != null
|
||||
|| check_tachyon_cli_running();
|
||||
|| controller.check_tachyon_cli_running();
|
||||
}
|
||||
|
||||
// `force_level` pins the rung instead of consulting the ladder. Only
|
||||
@@ -808,6 +788,11 @@ function heal_dns_stall(ev) {
|
||||
function heal_proxy_connectivity(ev) {
|
||||
if (settings().recovery_bypass == "1") return;
|
||||
if (!ev.payload.direct_ok) return;
|
||||
// Two consecutive failures before acting: a single brief proxy-node timeout
|
||||
// (e.g. URLTest selecting a slow node for 5–10s) must not wipe cache.db and
|
||||
// restart sing-box. The heal_proxy_health subscriber handles single-failure
|
||||
// recovery via the threshold it already carries.
|
||||
if (int(ev.payload.streak) < 2) return;
|
||||
if (suppressed_by_root_cause("heal_proxy_connectivity", PRIORITY_PROXY)) return;
|
||||
|
||||
let proxy_addr = "127.0.0.1:" + as_string(ev.payload.port);
|
||||
@@ -1065,9 +1050,6 @@ function heal_empty_sections(ev) {
|
||||
let DNS_RECOVERY_STATE_FILE = "/var/run/tachyon/dns-detour-recovery.json";
|
||||
let dns_recovery_active = false;
|
||||
|
||||
function is_dns_dead() {
|
||||
return !is_dns_working();
|
||||
}
|
||||
|
||||
function read_dns_recovery_state() {
|
||||
let data = common.read_json_file(DNS_RECOVERY_STATE_FILE);
|
||||
@@ -1099,8 +1081,8 @@ function ai_heal_dns_loop() {
|
||||
let detour_section = common.option(cfg, "dns_detour_section", "");
|
||||
if (detour_section == "") return;
|
||||
|
||||
// Check if DNS is dead
|
||||
if (!is_dns_dead()) {
|
||||
// Check if DNS is working
|
||||
if (is_dns_working()) {
|
||||
// DNS works — if we were in recovery, try to restore
|
||||
let recovery = read_dns_recovery_state();
|
||||
if (recovery.phase == "detour_disabled") {
|
||||
@@ -1344,25 +1326,9 @@ function smart_detect_process_pending() {
|
||||
let sections = smart_detect_get_proxy_sections();
|
||||
if (length(sections) == 0) return;
|
||||
|
||||
let proxy_addr = "127.0.0.1:4534";
|
||||
let sb_cfg_data = fs.readfile("/etc/sing-box/config.json");
|
||||
if (sb_cfg_data) {
|
||||
try {
|
||||
let sb_cfg = json(sb_cfg_data);
|
||||
if (sb_cfg.inbounds) {
|
||||
for (let inb in sb_cfg.inbounds) {
|
||||
if (inb.type == "http" || inb.type == "mixed") {
|
||||
proxy_addr = "127.0.0.1:" + as_string(inb.listen_port || 4534);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// Falls back to the default 4534 set above. The controller logs the
|
||||
// unparseable config once per process, so this does not repeat it.
|
||||
}
|
||||
}
|
||||
// Reuse the controller's cached port value: re-parsing config.json here
|
||||
// would duplicate the work event_controller already does (and caches).
|
||||
let proxy_addr = "127.0.0.1:" + controller.proxy_port();
|
||||
|
||||
let detect_sections = [];
|
||||
let raw_list = cfg.smart_detect_sections;
|
||||
|
||||
Reference in New Issue
Block a user