diff --git a/.gitignore b/.gitignore index 76957664..5d77a449 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ fe-app-tachyon/.yarn/ fe-app-tachyon/.env .DS_Store shellcheck-v0.10.0/ +AGENTS.md opencode.json sandbox/ @@ -11,4 +12,5 @@ dist/ build-out/ scratch/ tmp_*/ -tmp_test*.uc \ No newline at end of file +tmp_test*.uc +test_out.txt \ No newline at end of file diff --git a/tachyon/files/etc/init.d/tachyon b/tachyon/files/etc/init.d/tachyon index c62d0825..d9b5e946 100644 --- a/tachyon/files/etc/init.d/tachyon +++ b/tachyon/files/etc/init.d/tachyon @@ -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() { diff --git a/tachyon/files/usr/bin/tachyon b/tachyon/files/usr/bin/tachyon index a5ce7beb..1b1a6fc9 100755 --- a/tachyon/files/usr/bin/tachyon +++ b/tachyon/files/usr/bin/tachyon @@ -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) { diff --git a/tachyon/files/usr/lib/components/action.uc b/tachyon/files/usr/lib/components/action.uc index 8a7068db..3f5a24b0 100644 --- a/tachyon/files/usr/lib/components/action.uc +++ b/tachyon/files/usr/lib/components/action.uc @@ -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) { diff --git a/tachyon/files/usr/lib/components/updates.uc b/tachyon/files/usr/lib/components/updates.uc index e13ed754..bb1dc8b4 100644 --- a/tachyon/files/usr/lib/components/updates.uc +++ b/tachyon/files/usr/lib/components/updates.uc @@ -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) { diff --git a/tachyon/files/usr/lib/core/common.uc b/tachyon/files/usr/lib/core/common.uc index ad0eb028..c110289d 100644 --- a/tachyon/files/usr/lib/core/common.uc +++ b/tachyon/files/usr/lib/core/common.uc @@ -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 }; } diff --git a/tachyon/files/usr/lib/core/packages.uc b/tachyon/files/usr/lib/core/packages.uc index e15bd591..d25e2d66 100644 --- a/tachyon/files/usr/lib/core/packages.uc +++ b/tachyon/files/usr/lib/core/packages.uc @@ -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) { diff --git a/tachyon/files/usr/lib/core/uci.uc b/tachyon/files/usr/lib/core/uci.uc index f7780e7e..0390440b 100644 --- a/tachyon/files/usr/lib/core/uci.uc +++ b/tachyon/files/usr/lib/core/uci.uc @@ -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) { diff --git a/tachyon/files/usr/lib/diagnostics/runtime.uc b/tachyon/files/usr/lib/diagnostics/runtime.uc index 3c7173e3..34dfe2c0 100644 --- a/tachyon/files/usr/lib/diagnostics/runtime.uc +++ b/tachyon/files/usr/lib/diagnostics/runtime.uc @@ -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) { diff --git a/tachyon/files/usr/lib/dns/apply.uc b/tachyon/files/usr/lib/dns/apply.uc index 7335b4ff..8f14fa44 100644 --- a/tachyon/files/usr/lib/dns/apply.uc +++ b/tachyon/files/usr/lib/dns/apply.uc @@ -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(); diff --git a/tachyon/files/usr/lib/providers/byedpi/runtime.uc b/tachyon/files/usr/lib/providers/byedpi/runtime.uc index 4b2ad776..329c98ec 100644 --- a/tachyon/files/usr/lib/providers/byedpi/runtime.uc +++ b/tachyon/files/usr/lib/providers/byedpi/runtime.uc @@ -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++; } } diff --git a/tachyon/files/usr/lib/providers/nfqueue/runtime.uc b/tachyon/files/usr/lib/providers/nfqueue/runtime.uc index 91f012f9..100e3054 100644 --- a/tachyon/files/usr/lib/providers/nfqueue/runtime.uc +++ b/tachyon/files/usr/lib/providers/nfqueue/runtime.uc @@ -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" ]); diff --git a/tachyon/files/usr/lib/server/service.uc b/tachyon/files/usr/lib/server/service.uc index 4f859e42..e09c4805 100644 --- a/tachyon/files/usr/lib/server/service.uc +++ b/tachyon/files/usr/lib/server/service.uc @@ -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); } diff --git a/tachyon/files/usr/lib/service/initd.uc b/tachyon/files/usr/lib/service/initd.uc index 081e473c..2eff7ebc 100644 --- a/tachyon/files/usr/lib/service/initd.uc +++ b/tachyon/files/usr/lib/service/initd.uc @@ -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); } diff --git a/tachyon/files/usr/lib/service/lifecycle.uc b/tachyon/files/usr/lib/service/lifecycle.uc index 3a21b668..a21c6dc4 100644 --- a/tachyon/files/usr/lib/service/lifecycle.uc +++ b/tachyon/files/usr/lib/service/lifecycle.uc @@ -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 ])) diff --git a/tachyon/files/usr/lib/service/package.uc b/tachyon/files/usr/lib/service/package.uc index 991d2a1c..7d8c00ec 100644 --- a/tachyon/files/usr/lib/service/package.uc +++ b/tachyon/files/usr/lib/service/package.uc @@ -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) { diff --git a/tachyon/files/usr/lib/service/state.uc b/tachyon/files/usr/lib/service/state.uc index 309ac7f7..f35cef95 100644 --- a/tachyon/files/usr/lib/service/state.uc +++ b/tachyon/files/usr/lib/service/state.uc @@ -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) { diff --git a/tachyon/files/usr/lib/service/telegram.uc b/tachyon/files/usr/lib/service/telegram.uc index 3395deb0..9844deaa 100644 --- a/tachyon/files/usr/lib/service/telegram.uc +++ b/tachyon/files/usr/lib/service/telegram.uc @@ -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", "-9", 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; } diff --git a/tachyon/files/usr/lib/service/ui.uc b/tachyon/files/usr/lib/service/ui.uc index de1bf868..136833ee 100644 --- a/tachyon/files/usr/lib/service/ui.uc +++ b/tachyon/files/usr/lib/service/ui.uc @@ -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) { diff --git a/tachyon/files/usr/lib/service/watchdog.uc b/tachyon/files/usr/lib/service/watchdog.uc index afba2a44..6ee1d46f 100644 --- a/tachyon/files/usr/lib/service/watchdog.uc +++ b/tachyon/files/usr/lib/service/watchdog.uc @@ -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"); - return false; + 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 2>&1 &"); + cached_proxy_port = null; + let lock_path = shell_quote(PROXY_RESTART_LOCK); + bg_system("/etc/init.d/tachyon restart /dev/null 2>&1 & rm -f " + lock_path + " &"); return true; } @@ -562,19 +573,24 @@ function ai_heal_proxy_connectivity() { let now = time(); 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; + 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 { + 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") { + cached_proxy_port = inb.listen_port || 4534; + proxy_addr = "127.0.0.1:" + as_string(cached_proxy_port); + break; + } } } - } - } catch(e) {} + } catch(e) {} + } } let proxy_ok = command_success_from_args([ @@ -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(); diff --git a/tachyon/files/usr/lib/singbox/dns_failover.uc b/tachyon/files/usr/lib/singbox/dns_failover.uc index b5a97499..c250aaf6 100644 --- a/tachyon/files/usr/lib/singbox/dns_failover.uc +++ b/tachyon/files/usr/lib/singbox/dns_failover.uc @@ -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) { diff --git a/tachyon/files/usr/lib/singbox/generator.uc b/tachyon/files/usr/lib/singbox/generator.uc index 1fa12716..38e14e0a 100644 --- a/tachyon/files/usr/lib/singbox/generator.uc +++ b/tachyon/files/usr/lib/singbox/generator.uc @@ -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) { diff --git a/tachyon/files/usr/lib/singbox/priority.uc b/tachyon/files/usr/lib/singbox/priority.uc index d21c16c2..66b234df 100644 --- a/tachyon/files/usr/lib/singbox/priority.uc +++ b/tachyon/files/usr/lib/singbox/priority.uc @@ -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; } diff --git a/tachyon/files/usr/lib/singbox/runtime.uc b/tachyon/files/usr/lib/singbox/runtime.uc index 2adcc054..e6449ed2 100644 --- a/tachyon/files/usr/lib/singbox/runtime.uc +++ b/tachyon/files/usr/lib/singbox/runtime.uc @@ -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) { diff --git a/tachyon/files/usr/lib/subscription/cache.uc b/tachyon/files/usr/lib/subscription/cache.uc index 5976b100..d9605cc3 100644 --- a/tachyon/files/usr/lib/subscription/cache.uc +++ b/tachyon/files/usr/lib/subscription/cache.uc @@ -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) {