fix: комплексный аудит и исправление ucode кодовой базы

Критические исправления:
- safe_proxy_restart: удаление lock-файла после фонового перезапуска
  с защитой от stale lock (>300 сек)
- write_json_file: атомарная запись через tmp+mv вместо прямой
- write_reload_state: атомарная запись через tmp+mv
- state_add_list: отклонение значений с пробелами

Исправления безопасности:
- killall logread заменён на pkill -f 'logread -f'
- SIGTERM→SIGKILL для Telegram и sing-box при остановке
- Recover OOM scale: инкремент +0.05 каждые 10 минут
- nfqueue и byedpi: trap для корректного завершения дочерних процессов
- nfqueue: валидация стратегий и откат при ошибке запуска
- nfqueue: flush mangle_output перед добавлением правил
- Исправлен парсинг exit code в 14 файлах (status & 127)

Улучшения стабильности:
- init.d: trap для start_service/stop_service
- watchdog: signal traps для legacy fallback loop
- lifecycle: логирование ошибок при остановке компонентов
- singbox/priority: SIGTERM→SIGKILL при остановке
- telegram: exponential backoff (1x→300s) при ошибках API
- telegram: process_updates возвращает boolean для отслеживания успеха
- watchdog: кэш proxy port для уменьшения чтения конфига sing-box
- singbox/generator: упрощён atomic_write_json_file
- packages.uc: переупорядочивание apk_version (лёгкие запросы первыми)
- dns/apply: проверка PID sing-box перед настройкой dnsmasq
- server/service: cleanup_orphaned_server_certs перед вызовом
This commit is contained in:
Dushnilin
2026-08-06 00:03:51 +03:00
parent cb62d7e006
commit 488bbfefd5
25 changed files with 328 additions and 78 deletions
+2
View File
@@ -3,6 +3,7 @@ fe-app-tachyon/.yarn/
fe-app-tachyon/.env
.DS_Store
shellcheck-v0.10.0/
AGENTS.md
opencode.json
sandbox/
@@ -12,3 +13,4 @@ build-out/
scratch/
tmp_*/
tmp_test*.uc
test_out.txt
+4
View File
@@ -30,8 +30,10 @@ sync_service_triggers_payload() {
}
start_service() {
trap 'exit 1' INT TERM
initd_ucode start-service "$1" "$$"
TACHYON_LAST_START_STATUS="$?"
trap - INT TERM
return "$TACHYON_LAST_START_STATUS"
}
@@ -40,7 +42,9 @@ service_started() {
}
stop_service() {
trap 'exit 1' INT TERM
initd_ucode stop-service "$$"
trap - INT TERM
}
reload_service() {
+6 -1
View File
@@ -24,7 +24,12 @@ function command_from_args(args) {
function normalize_status(status) {
status = int(status);
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function module_path(path) {
+6 -1
View File
@@ -55,7 +55,12 @@ function command_env(assignments) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success(command) {
+6 -1
View File
@@ -151,7 +151,12 @@ function command_output_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success(command) {
+28 -3
View File
@@ -64,7 +64,19 @@ function csv_to_json_array(value) {
}
function write_json_file(path, value) {
return fs.writefile(path, sprintf("%J\n", value));
path = as_string(path);
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
let result = fs.writefile(tmp_path, sprintf("%J\n", value));
if (result == null || (type(result) == "boolean" && !result)) {
try { fs.unlink(tmp_path); } catch(e) {}
return false;
}
if (!fs.rename(tmp_path, path)) {
try { fs.unlink(tmp_path); } catch(e) {}
return false;
}
return true;
}
function strip_internal_fields(value) {
@@ -147,7 +159,12 @@ function command_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success(command) {
@@ -163,7 +180,15 @@ function command_capture(command) {
if (!p) return null;
let output = as_string(p.read("all") || "");
let status = p.close();
status = status > 255 ? int(status / 256) : status;
if (status == -1)
status = 255;
else {
let signal = status & 127;
if (signal != 0)
status = 128 + signal;
else
status = (status >> 8) & 255;
}
return { status: status, output: output };
}
+5 -5
View File
@@ -139,15 +139,15 @@ function apk_version(package_name) {
if (version != "")
return version;
version = apk_info_version(package_name, command_output([ "apk", "info", "-v", package_name ]));
if (version != "")
return version;
version = apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest", package_name ]));
if (version != "")
return version;
version = apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest" ]));
if (version != "")
return version;
return apk_info_version(package_name, command_output([ "apk", "info", "-v", package_name ]));
return apk_manifest_version(package_name, command_output([ "apk", "list", "--installed", "--manifest" ]));
}
function apk_available_version(package_name) {
+4 -1
View File
@@ -155,8 +155,11 @@ function state_set(path, value) {
}
function state_add_list(path, value) {
value = as_string(value);
if (match(value, /[ \t\r\n]/) != null)
return false;
let current = state_get(path);
return state_set(path, current == "" ? value : current + " " + as_string(value));
return state_set(path, current == "" ? value : current + " " + value);
}
function state_del_list(path, value) {
+6 -1
View File
@@ -84,7 +84,12 @@ function command_from_args(args) {
function normalize_status(status) {
status = int(status);
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_status(command) {
+8
View File
@@ -244,6 +244,14 @@ function dnsmasq_configure(force) {
log("Previous Tachyon shutdown was unclean and dnsmasq is not ready; applying Tachyon DNS settings", "info");
}
if (as_string(force) != "force") {
let sb_pid = trim(fs.readfile("/var/run/sing-box.pid") || "");
if (sb_pid == "" || !run("kill -0 " + shell_quote(sb_pid) + " 2>/dev/null")) {
log("sing-box is not running; dnsmasq will forward to system DNS until sing-box starts", "warn");
return true;
}
}
log("Configuring dnsmasq to forward DNS to sing-box", "info");
dnsmasq_cleanup_legacy_instance();
dnsmasq_configure_default_instance();
@@ -62,7 +62,12 @@ function command_output_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success_from_args(args) {
@@ -290,8 +295,10 @@ function supervisor_command(port, raw_opt, child_pidfile) {
for (let word in strategy_words(raw_opt))
push(args, word);
return "ulimit -n " + shell_quote(BYEDPI_OPEN_FILES_LIMIT) + " >/dev/null 2>&1 || true; " +
command_from_args(args) + " & child=$!; echo $child > " + shell_quote(child_pidfile) + "; wait $child; rc=$?; rm -f " + shell_quote(child_pidfile) + "; exit $rc";
let child_pidfile_q = shell_quote(child_pidfile);
return "trap 'kill $child 2>/dev/null; rm -f " + child_pidfile_q + "; exit 0' TERM INT; " +
"ulimit -n " + shell_quote(BYEDPI_OPEN_FILES_LIMIT) + " >/dev/null 2>&1 || true; " +
command_from_args(args) + " & child=$!; echo $child > " + child_pidfile_q + "; wait $child; rc=$?; rm -f " + child_pidfile_q + "; exit $rc";
}
function supervisor(section, port, raw_opt, child_pidfile) {
@@ -315,7 +322,7 @@ function start_rule(section, index_value) {
let validation = validator().validate_byedpi_strategy(raw_opt);
if (!validation.valid) {
log_message("Invalid ByeDPI strategy for rule '" + name + "': " + validation.message, "fatal");
exit(1);
return false;
}
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
@@ -335,19 +342,20 @@ function start_rule(section, index_value) {
]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!";
let pid = trim(command_output("sh -c " + shell_quote(command)));
if (pid == "" || !fs.writefile(pidfile, pid + "\n")) {
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
exit(1);
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
return false;
}
command_success_from_args([ "sleep", "1" ]);
if (!runtime_pid_running(pid)) {
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
exit(1);
log_message("ciadpi failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
return false;
}
let child_pid = file_first_line(child_pidfile);
if (child_pid == "" || !runtime_pid_running(child_pid))
log_message("ciadpi supervisor started for rule '" + name + "', but ciadpi is not running yet. Check " + logfile + ".", "warn");
return true;
}
function start_runtime() {
@@ -375,9 +383,21 @@ function start_runtime() {
exit(1);
}
let started_sections = [];
let index_value = 1;
for (let section in sections) {
start_rule(section, index_value);
if (!start_rule(section, index_value)) {
log_message("Rolling back " + as_string(length(started_sections)) + " started ciadpi rules.", "warn");
for (let started in started_sections) {
let name = section_name(started);
let pidfile = BYEDPI_PID_DIR + "/" + name + ".pid";
let child_pidfile = BYEDPI_CHILD_PID_DIR + "/" + name + ".pid";
kill_pidfile_process(pidfile, "9");
kill_pidfile_process(child_pidfile, "9");
}
exit(1);
}
push(started_sections, section);
index_value++;
}
}
@@ -52,7 +52,12 @@ function command_output_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success(command) {
@@ -373,6 +378,13 @@ function validate_strategy_or_exit(cfg, section_name_value, raw_opt) {
exit(1);
}
function validate_strategy(cfg, section_name_value, raw_opt) {
let result = cfg.validator().validate_strategy(cfg.validator_kind, raw_opt, cfg.legacy_default_strategy);
if (!result.valid)
log_message("Invalid " + cfg.binary_name + " strategy for rule '" + section_name_value + "': " + result.message, "fatal");
return result.valid;
}
function supervisor_command(cfg, queue, raw_opt, child_pidfile) {
let args = [ cfg.binary, "--qnum=" + as_string(queue) ];
for (let arg in base_args(cfg))
@@ -380,7 +392,9 @@ function supervisor_command(cfg, queue, raw_opt, child_pidfile) {
for (let word in strategy_words(raw_opt))
push(args, word);
return command_from_args(args) + " & child=$!; echo $child > " + shell_quote(child_pidfile) + "; wait $child; rc=$?; rm -f " + shell_quote(child_pidfile) + "; exit $rc";
let child_pidfile_q = shell_quote(child_pidfile);
return "trap 'kill $child 2>/dev/null; rm -f " + child_pidfile_q + "; exit 0' TERM INT; " +
command_from_args(args) + " & child=$!; echo $child > " + child_pidfile_q + "; wait $child; rc=$?; rm -f " + child_pidfile_q + "; exit $rc";
}
function supervisor(cfg, section, queue, raw_opt, child_pidfile) {
@@ -423,7 +437,8 @@ function start_rule(cfg, section, index_value) {
let queue = queue_number(cfg, index_value);
let mark = route_mark_hex(cfg, index_value);
let raw_opt = expand_strategy(cfg, raw_strategy(cfg, section));
validate_strategy_or_exit(cfg, name, raw_opt);
if (!validate_strategy(cfg, name, raw_opt))
return false;
let pidfile = cfg.pid_dir + "/" + name + ".pid";
let child_pidfile = cfg.child_pid_dir + "/" + name + ".pid";
@@ -442,19 +457,20 @@ function start_rule(cfg, section, index_value) {
]) + " >>" + shell_quote(logfile) + " 2>&1 1000>&- & echo $!";
let pid = trim(command_output("sh -c " + shell_quote(command)));
if (pid == "" || fs.writefile(pidfile, pid + "\n") == null) {
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
exit(1);
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
return false;
}
command_success_from_args([ "sleep", "1" ]);
if (!runtime_pid_running(pid)) {
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ". Aborted.", "fatal");
exit(1);
log_message(cfg.binary_name + " failed to start for rule '" + name + "'. Check " + logfile + ".", "fatal");
return false;
}
let child_pid = file_first_line(child_pidfile);
if (child_pid == "" || !runtime_pid_running(child_pid))
log_message(cfg.binary_name + " supervisor started for rule '" + name + "', but " + cfg.binary_name + " is not running yet. Check " + logfile + ".", "warn");
return true;
}
function start_runtime(cfg) {
@@ -470,9 +486,21 @@ function start_runtime(cfg) {
exit(1);
}
let started_sections = [];
let index_value = 1;
for (let section in sections) {
start_rule(cfg, section, index_value);
if (!start_rule(cfg, section, index_value)) {
log_message("Rolling back " + as_string(length(started_sections)) + " started " + cfg.binary_name + " rules.", "warn");
for (let started in started_sections) {
let name = section_name(started);
let pidfile = cfg.pid_dir + "/" + name + ".pid";
let child_pidfile = cfg.child_pid_dir + "/" + name + ".pid";
kill_pidfile_process(pidfile, "9");
kill_pidfile_process(child_pidfile, "9");
}
exit(1);
}
push(started_sections, section);
index_value++;
}
}
@@ -655,6 +683,8 @@ function create_nft_rules(cfg) {
if (length(sections) == 0 || !provider_available(cfg))
return;
command_success_from_args([ "nft", "flush", "chain", "inet", NFT_TABLE_NAME, "mangle_output" ]);
command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark, "==", cfg.desync_mark, "return" ]);
command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark_postnat, "==", cfg.desync_mark_postnat, "return" ]);
+21
View File
@@ -996,10 +996,31 @@ function prepare_server_defaults(section) {
server_prepare_tls_defaults(section, protocol, security);
}
function cleanup_orphaned_server_certs() {
let cert_dir = "/etc/tachyon/server-certs";
try {
let entries = fs.readdir(cert_dir);
if (entries == null) return;
let active_names = {};
for (let section in uci_sections("server")) {
let name = safe_filename_string(section);
active_names[name + ".crt"] = true;
active_names[name + ".key"] = true;
}
for (let entry in entries) {
if (substr(entry, 0, 1) == ".") continue;
if (active_names[entry]) continue;
let path = cert_dir + "/" + entry;
try { fs.unlink(path); } catch(e) {}
}
} catch(e) {}
}
function prepare_all_server_defaults() {
SERVER_DEFAULTS_CHANGED = false;
for (let section in uci_sections("server"))
prepare_server_defaults(section);
cleanup_orphaned_server_certs();
if (SERVER_DEFAULTS_CHANGED)
uci_commit(CONFIG_NAME);
}
+8 -2
View File
@@ -49,7 +49,12 @@ function command_from_args(args) {
function normalize_status(status) {
status = int(status);
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_status(command) {
@@ -576,7 +581,8 @@ function stop_service(owner_pid) {
// Clean up orphaned logread -f processes left by watchdog instances.
// These survive tachyon stop because background subprocesses (reload_firewall,
// restart) inherit the logread pipe read-end, preventing SIGPIPE delivery.
system("killall logread 2>/dev/null; true");
// Use pkill to target only logread in follow mode, not one-shot logread calls.
system("pkill -f 'logread -f' 2>/dev/null; true");
return stop_finish(job_id, status);
}
+16 -6
View File
@@ -136,7 +136,12 @@ function command_from_args(args) {
function normalize_status(status) {
status = int(status);
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_status(command) {
@@ -852,16 +857,21 @@ function stop_main() {
let status = 0;
log_message("Stopping Tachyon", "info");
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
module_success(PRIORITY_UC, [ "stop-runtime" ]);
if (!module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]))
log_message("DNS failover stop failed (non-fatal)", "warn");
if (!module_success(PRIORITY_UC, [ "stop-runtime" ]))
log_message("Priority daemon stop failed (non-fatal)", "warn");
module_success(SUBSCRIPTION_CACHE_UC, [ "stop-deferred-bootstrap-worker" ]);
module_success(UPDATES_UC, [ "stop-list-update" ]);
remove_cron_jobs();
command_success_from_args([ "find", TMP_RULESET_FOLDER, "-mindepth", "1", "-maxdepth", "1", "-type", "f", "-delete" ]);
module_success(ZAPRET_UC, [ "stop-runtime" ]);
module_success(ZAPRET2_UC, [ "stop-runtime" ]);
module_success(BYEDPI_UC, [ "stop-runtime" ]);
if (!module_success(ZAPRET_UC, [ "stop-runtime" ]))
log_message("Zapret stop failed (non-fatal)", "warn");
if (!module_success(ZAPRET2_UC, [ "stop-runtime" ]))
log_message("Zapret2 stop failed (non-fatal)", "warn");
if (!module_success(BYEDPI_UC, [ "stop-runtime" ]))
log_message("ByeDPI stop failed (non-fatal)", "warn");
if (command_success_from_args([ "nft", "list", "table", "inet", NFT_TABLE_NAME ])) {
if (!command_success_from_args([ "nft", "delete", "table", "inet", NFT_TABLE_NAME ]))
+6 -1
View File
@@ -37,7 +37,12 @@ function command_from_args(args) {
function normalize_status(status) {
status = int(status);
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success_from_args(args) {
+11 -1
View File
@@ -337,8 +337,18 @@ function write_reload_state(path, values) {
if (!ensure_parent_dir(path))
exit(1);
if (!fs.writefile(path, reload_state_text(values)))
path = as_string(path);
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
let result = fs.writefile(tmp_path, reload_state_text(values));
if (result == null || (type(result) == "boolean" && !result)) {
try { fs.unlink(tmp_path); } catch(e) {}
exit(1);
}
if (!fs.rename(tmp_path, path)) {
try { fs.unlink(tmp_path); } catch(e) {}
exit(1);
}
}
function copy_file(source, target) {
+26 -5
View File
@@ -2095,7 +2095,7 @@ function process_updates(token, admin_ids) {
let offset = int(trim(fs.readfile(OFFSET_FILE) || "0"));
let res = tg_request(token, "getUpdates", { offset: offset, timeout: 20 });
if (!res || !res.ok || !res.result || length(res.result) == 0) return;
if (!res || !res.ok || !res.result || length(res.result) == 0) return false;
for (let upd in res.result) {
let update_id = upd.update_id;
@@ -2292,6 +2292,7 @@ function process_updates(token, admin_ids) {
}
}
fs.writefile(OFFSET_FILE, as_string(offset));
return true;
}
// ─── Entry Point ─────────────────────────────────────────────────────────────
@@ -2386,15 +2387,26 @@ function worker() {
let last_report_day = -1;
let last_update_check = 0;
let consecutive_failures = 0;
while (true) {
try {
cfg = settings();
if (cfg.enabled != "1") break;
process_updates(cfg.bot_token, cfg.admin_ids);
let res = process_updates(cfg.bot_token, cfg.admin_ids);
if (res === false) {
consecutive_failures++;
let backoff = poll_interval * (1 << min(consecutive_failures - 1, 4));
if (backoff > 300) backoff = 300;
command_success_from_args(["logger", "-t", "tachyon-telegram", "[warn] API failure " + as_string(consecutive_failures) + ", backing off " + as_string(backoff) + "s"]);
sleep(backoff * 1000);
continue;
}
consecutive_failures = 0;
let now = time();
let tm = clock(now); // [year, mon, day, hour, min, sec]
let tm = clock(now);
let daily_hour = int(cfg.daily_report_hour || "8");
if (cfg.daily_report_enabled == "1" && tm[3] == daily_hour && tm[2] != last_report_day) {
@@ -2402,11 +2414,12 @@ function worker() {
send_daily_digest(cfg.bot_token, cfg.admin_ids);
}
if (now - last_update_check > 3600) { // check every hour
if (now - last_update_check > 3600) {
check_notified_updates(cfg.bot_token, cfg.admin_ids);
last_update_check = now;
}
} catch (e) {
consecutive_failures++;
command_success_from_args(["logger", "-t", "tachyon-telegram", "[err] Worker loop error: " + as_string(e)]);
}
@@ -2418,9 +2431,17 @@ function worker() {
function stop_runtime() {
let pid = trim(fs.readfile(PID_FILE) || "");
if (pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ])) {
command_success_from_args([ "kill", pid ]);
let wait_limit = 30;
while (wait_limit > 0 && command_success_from_args([ "kill", "-0", pid ])) {
sleep(100);
wait_limit--;
}
if (command_success_from_args([ "kill", "-0", pid ])) {
command_success_from_args([ "kill", "-9", pid ]);
}
fs.unlink(PID_FILE);
}
try { fs.unlink(PID_FILE); } catch(e) {}
return 0;
}
+6 -1
View File
@@ -119,7 +119,12 @@ function command_output_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success(command) {
+45 -4
View File
@@ -146,6 +146,7 @@ function stop_runtime() {
}
remove_file("/var/run/tachyon_honeypot_listener.pid");
remove_file("/tmp/tachyon_honeypot.fifo");
remove_file(PROXY_RESTART_LOCK);
return 0;
}
@@ -279,6 +280,7 @@ try { uloop = require("uloop"); } catch (e) {}
try { ubus = require("ubus"); } catch (e) {}
let last_oom_time = 0;
let last_oom_recovery_time = 0;
let last_restart_time = 0;
let last_urltest_check = 0;
let pending_smart_domains = {};
@@ -290,6 +292,7 @@ let last_reload_time = 0;
let proxy_consecutive_fails = 0;
let dns_consecutive_fails = 0;
let ai_healthy_streak = 0;
let cached_proxy_port = null;
let proxy_latency_history = [];
let dns_latency_history = [];
let last_anomaly_check = 0;
@@ -465,12 +468,20 @@ function safe_proxy_restart(reason) {
return false;
}
if (fs.stat(PROXY_RESTART_LOCK) != null) {
log_message("Proxy restart lock exists, skipping (" + reason + ")", "warn");
let lock_content = trim(fs.readfile(PROXY_RESTART_LOCK) || "0");
let lock_age = now - int(lock_content);
if (lock_age < 300) {
log_message("Proxy restart lock exists (age " + as_string(lock_age) + "s), skipping (" + reason + ")", "warn");
return false;
}
log_message("Proxy restart lock stale (age " + as_string(lock_age) + "s), removing", "warn");
remove_file(PROXY_RESTART_LOCK);
}
try { fs.writefile(PROXY_RESTART_LOCK, as_string(now)); } catch(e) {}
proxy_restart_count++;
bg_system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 &");
cached_proxy_port = null;
let lock_path = shell_quote(PROXY_RESTART_LOCK);
bg_system("/etc/init.d/tachyon restart </dev/null >/dev/null 2>&1 & rm -f " + lock_path + " &");
return true;
}
@@ -562,6 +573,9 @@ function ai_heal_proxy_connectivity() {
let now = time();
let proxy_addr = "127.0.0.1:4534";
if (cached_proxy_port !== null) {
proxy_addr = "127.0.0.1:" + as_string(cached_proxy_port);
} else {
let sb_cfg_data = fs.readfile("/etc/sing-box/config.json");
if (sb_cfg_data) {
try {
@@ -569,13 +583,15 @@ function ai_heal_proxy_connectivity() {
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);
cached_proxy_port = inb.listen_port || 4534;
proxy_addr = "127.0.0.1:" + as_string(cached_proxy_port);
break;
}
}
}
} catch(e) {}
}
}
let proxy_ok = command_success_from_args([
"curl", "-s", "-I", "--connect-timeout", "3", "--max-time", "5",
@@ -1497,6 +1513,27 @@ function setup_honeypot_listener() {
}
}
function recover_oom_scale() {
let now = time();
if (now - last_oom_time < 1800) return;
if (last_oom_time == 0) return;
if (now - last_oom_recovery_time < 600) return;
last_oom_recovery_time = now;
let scale_path = "/etc/tachyon/mem_scale";
let scale_data = fs.readfile(scale_path);
if (scale_data == null) return;
let current_scale = double(trim(as_string(scale_data)));
if (current_scale >= 1.0 || current_scale < 0.1) {
if (current_scale >= 1.0) try { fs.unlink(scale_path); } catch(e) {}
return;
}
let new_scale = current_scale + 0.05;
if (new_scale > 1.0) new_scale = 1.0;
log_message("OOM recovery: restoring GOMEMLIMIT scale from " + sprintf("%.2f", current_scale) + " to " + sprintf("%.2f", new_scale), "info");
try { fs.writefile(scale_path, sprintf("%.2f", new_scale)); } catch(e) {}
}
function setup_syslog_listener() {
if (!uloop) return null;
syslog_start_time = time();
@@ -1504,7 +1541,8 @@ function setup_syslog_listener() {
// Without this, every restart cascades: new watchdog inherits old watchdog's
// logread pipe read-end, keeping old logread alive. Over N restarts,
// watchdog accumulates N inherited FDs → hits 1024 limit → config generator fails.
system("killall logread 2>/dev/null; true");
// Use pkill to target only logread in follow mode, not one-shot logread calls.
system("pkill -f 'logread -f' 2>/dev/null; true");
let log_pipe = fs.popen("logread -f 2>/dev/null", "r");
if (!log_pipe) return null;
// Track the FD so bg_system() can close it before spawning background processes
@@ -1601,6 +1639,7 @@ function worker() {
safe_call(ai_heal_empty_sections, "ai_heal_empty_sections");
safe_call(ai_heal_dns_loop, "ai_heal_dns_loop");
safe_call(analyze_anomalies, "analyze_anomalies");
safe_call(recover_oom_scale, "recover_oom_scale");
}
if (uloop) {
@@ -1632,6 +1671,8 @@ function worker() {
uloop.run();
} else {
log_message("uloop not available. Running Watchdog in legacy fallback loop mode.", "warn");
signal("SIGTERM", function(sig) { log_message("SIGTERM received, shutting down", "info"); stop_runtime(); exit(0); });
signal("SIGINT", function(sig) { log_message("SIGINT received, shutting down", "info"); stop_runtime(); exit(0); });
while (true) {
perform_fast_checks();
perform_normal_checks();
@@ -32,7 +32,12 @@ function command_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success_from_args(args) {
+1 -10
View File
@@ -121,18 +121,9 @@ function ensure_parent_dir(path) {
}
function atomic_write_json_file(path, value) {
let stamp = clock();
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
if (!ensure_parent_dir(path))
return false;
if (!write_json_file(tmp_path, value))
return false;
if (!fs.rename(tmp_path, path)) {
fs.unlink(tmp_path);
return false;
}
return true;
return write_json_file(path, value);
}
function fixture_section_list(type_name) {
+15 -2
View File
@@ -45,7 +45,12 @@ function command_output_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success_from_args(args) {
@@ -436,8 +441,16 @@ function process_running(pid) {
function stop_runtime() {
let pid = file_first_line(PRIORITY_PID_FILE);
if (process_running(pid))
if (process_running(pid)) {
command_success_from_args([ "kill", pid ]);
let wait_limit = 30;
while (wait_limit > 0 && process_running(pid)) {
sleep(100);
wait_limit--;
}
if (process_running(pid))
command_success_from_args([ "kill", "-9", pid ]);
}
remove_file(PRIORITY_PID_FILE);
return 0;
}
+6 -1
View File
@@ -79,7 +79,12 @@ function command_output_from_args(args) {
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function command_success_from_args(args) {
+6 -1
View File
@@ -603,7 +603,12 @@ function command_success_from_args(args) {
function command_status_from_args(args) {
let status = int(system(command_from_args(args)));
return status > 255 ? int(status / 256) : status;
if (status == -1)
return 255;
let signal = status & 127;
if (signal != 0)
return 128 + signal;
return (status >> 8) & 255;
}
function run_silent(command) {