migration.uc: - try/catch around migration.run() with error logging - try/catch around apply_operations() with error logging - Error checking for UCI cursor operations in apply_operations - Checked fs.writefile return values in ensure_runtime_cache_format updates.uc: - Write pending marker on lock acquisition failure for next retry
3134 lines
106 KiB
Ucode
3134 lines
106 KiB
Ucode
#!/usr/bin/env ucode
|
||
|
||
let fs = require("fs");
|
||
let helpers = require("core.helpers");
|
||
let uci_core = require("core.uci");
|
||
let connections = require("config.connections");
|
||
|
||
function core_url_module_or_null() {
|
||
try {
|
||
return require("core.url");
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
||
const BIN_PATH = getenv("TACHYON_BIN") || "/usr/bin/tachyon";
|
||
const TMP_SING_BOX_FOLDER = getenv("TMP_SING_BOX_FOLDER") || "/tmp/sing-box";
|
||
const TMP_RULESET_FOLDER = getenv("TMP_RULESET_FOLDER") || TMP_SING_BOX_FOLDER + "/rulesets";
|
||
const TMP_SUBSCRIPTION_FOLDER = getenv("TMP_SUBSCRIPTION_FOLDER") || TMP_SING_BOX_FOLDER + "/subscriptions";
|
||
const RUNTIME_STATE_DIR = getenv("TACHYON_RUNTIME_STATE_DIR") || "/var/run/tachyon";
|
||
const LIST_UPDATE_STATE_FILE = getenv("TACHYON_LIST_UPDATE_STATE_FILE") || RUNTIME_STATE_DIR + "/list-update.timestamp";
|
||
const LIST_UPDATE_PID_FILE = getenv("TACHYON_LIST_UPDATE_PID_FILE") || "/var/run/tachyon_list_update.pid";
|
||
const SUBSCRIPTION_UPDATE_STATE_DIR = getenv("TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR") || RUNTIME_STATE_DIR + "/subscription-update";
|
||
const SUBSCRIPTION_JOB_DIR = getenv("TACHYON_SUBSCRIPTION_UPDATE_JOB_DIR") || "/var/run/tachyon/subscription-update-jobs";
|
||
const SUBSCRIPTION_UPDATE_LOCK_DIR = getenv("TACHYON_SUBSCRIPTION_UPDATE_LOCK_DIR") || RUNTIME_STATE_DIR + "/subscription-update.lock";
|
||
const SUBSCRIPTION_LINKS_DIR = getenv("TACHYON_SUBSCRIPTION_LINKS_DIR") || RUNTIME_STATE_DIR + "/subscription-links";
|
||
const SUBSCRIPTION_METADATA_DIR = getenv("TACHYON_SUBSCRIPTION_METADATA_DIR") || RUNTIME_STATE_DIR + "/subscription-metadata";
|
||
const OUTBOUND_METADATA_DIR = getenv("TACHYON_OUTBOUND_METADATA_DIR") || RUNTIME_STATE_DIR + "/outbound-metadata";
|
||
const SECTION_CACHE_DIR = getenv("TACHYON_SECTION_CACHE_DIR") || RUNTIME_STATE_DIR + "/section-cache";
|
||
const RUNTIME_CACHE_FORMAT_FILE = getenv("TACHYON_RUNTIME_CACHE_FORMAT_FILE") || RUNTIME_STATE_DIR + "/cache-format";
|
||
const RUNTIME_CACHE_FORMAT = getenv("TACHYON_RUNTIME_CACHE_FORMAT") || "8";
|
||
const PERSISTENT_SUBSCRIPTION_CACHE_DIR = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR") || "/etc/tachyon/subscription-cache";
|
||
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE") || PERSISTENT_SUBSCRIPTION_CACHE_DIR + "/cache-format";
|
||
const PERSISTENT_SUBSCRIPTION_CACHE_FORMAT = getenv("TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT") || "7";
|
||
const PENDING_RELOAD_FILE = getenv("TACHYON_PENDING_RELOAD_FILE") || RUNTIME_STATE_DIR + "/reload.pending";
|
||
const RELOAD_STATE_FILE = getenv("TACHYON_RELOAD_STATE_FILE") || RUNTIME_STATE_DIR + "/reload-state";
|
||
const RELOAD_STATE_FORMAT = getenv("TACHYON_RELOAD_STATE_FORMAT") || "1";
|
||
const RULE_CONDITION_CACHE_DIR = getenv("TACHYON_RULE_CONDITION_CACHE_DIR") || RUNTIME_STATE_DIR + "/rule-condition-cache";
|
||
const RELOAD_LOCK_DIR = getenv("TACHYON_RELOAD_LOCK_DIR") || "/var/run/tachyon.reload.lock";
|
||
const SERVICE_INIT = getenv("TACHYON_SERVICE_INIT") || "/etc/init.d/tachyon";
|
||
const PRIORITY_UC = getenv("TACHYON_PRIORITY_UC") || LIB_DIR + "/singbox/priority.uc";
|
||
const DNS_FAILOVER_UC = getenv("TACHYON_DNS_FAILOVER_UC") || LIB_DIR + "/singbox/dns_failover.uc";
|
||
const COMPONENT_JOB_DIR = getenv("UPDATES_JOB_DIR") || getenv("TACHYON_UI_COMPONENT_ACTION_DIR") || "/var/run/tachyon/component-actions";
|
||
const COMPONENT_UPDATE_CHECK_CACHE_DIR = getenv("TACHYON_COMPONENT_UPDATE_CHECK_CACHE_DIR") || RUNTIME_STATE_DIR + "/component-update-checks";
|
||
const COMPONENT_UPDATE_CHECK_STATE_FILE = getenv("TACHYON_COMPONENT_UPDATE_CHECK_STATE_FILE") || RUNTIME_STATE_DIR + "/component-update-check.timestamp";
|
||
const COMPONENT_UPDATE_CHECK_LOCK_DIR = getenv("TACHYON_COMPONENT_UPDATE_CHECK_LOCK_DIR") || RUNTIME_STATE_DIR + "/component-update-check.lock";
|
||
const COMPONENT_JOB_FINISHED_TTL_MINUTES = getenv("UPDATES_JOB_FINISHED_TTL_MINUTES") || "60";
|
||
const COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES = getenv("UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES") || "60";
|
||
const COMPONENT_JOB_STALE_GRACE_SECONDS = getenv("UPDATES_JOB_STALE_GRACE_SECONDS") || getenv("TACHYON_UI_ACTION_STALE_GRACE_SECONDS") || "15";
|
||
const SUBSCRIPTION_JOB_FINISHED_TTL_MINUTES = getenv("TACHYON_SUBSCRIPTION_UPDATE_JOB_FINISHED_TTL_MINUTES") || "60";
|
||
const SUBSCRIPTION_JOB_ORPHAN_OUTPUT_TTL_MINUTES = getenv("TACHYON_SUBSCRIPTION_UPDATE_JOB_ORPHAN_OUTPUT_TTL_MINUTES") || "60";
|
||
const SUBSCRIPTION_JOB_STALE_GRACE_SECONDS = getenv("TACHYON_UI_ACTION_STALE_GRACE_SECONDS") || "15";
|
||
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||
const NFT_COMMON_SET_NAME = getenv("NFT_COMMON_SET_NAME") || "tachyon_subnets";
|
||
const NFT_COMMON6_SET_NAME = getenv("NFT_COMMON6_SET_NAME") || "tachyon_subnets6";
|
||
const NFT_IP_PORT_SET_NAME = getenv("NFT_IP_PORT_SET_NAME") || "tachyon_ip_ports";
|
||
const NFT_IP_PORT6_SET_NAME = getenv("NFT_IP_PORT6_SET_NAME") || "tachyon_ip6_ports";
|
||
const NFT_DISCORD_SET_NAME = getenv("NFT_DISCORD_SET_NAME") || "tachyon_discord_subnets";
|
||
const NFT_DISCORD6_SET_NAME = getenv("NFT_DISCORD6_SET_NAME") || "tachyon_discord_subnets6";
|
||
const NFT_INTERFACE_SET_NAME = getenv("NFT_INTERFACE_SET_NAME") || "tachyon_interfaces";
|
||
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x04000000";
|
||
const SB_SERVICE_MIXED_INBOUND_ADDRESS = getenv("SB_SERVICE_MIXED_INBOUND_ADDRESS") || "127.0.0.1";
|
||
const SB_SERVICE_MIXED_INBOUND_PORT = getenv("SB_SERVICE_MIXED_INBOUND_PORT") || "4534";
|
||
const SB_VARIANT_STATE_FILE = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
||
const GITHUB_RAW_URL = getenv("GITHUB_RAW_URL") || "https://raw.githubusercontent.com/itdoginfo/allow-domains/main";
|
||
const BUILTIN_SUBNET_URLS = {
|
||
twitter: [ getenv("SUBNETS_TWITTER") || GITHUB_RAW_URL + "/Subnets/IPv4/twitter.lst", getenv("SUBNETS_TWITTER6") || GITHUB_RAW_URL + "/Subnets/IPv6/twitter.lst" ],
|
||
meta: [ getenv("SUBNETS_META") || GITHUB_RAW_URL + "/Subnets/IPv4/meta.lst", getenv("SUBNETS_META6") || GITHUB_RAW_URL + "/Subnets/IPv6/meta.lst" ],
|
||
discord: [ getenv("SUBNETS_DISCORD") || GITHUB_RAW_URL + "/Subnets/IPv4/discord.lst", getenv("SUBNETS_DISCORD6") || GITHUB_RAW_URL + "/Subnets/IPv6/discord.lst" ],
|
||
roblox: [ getenv("SUBNETS_ROBLOX") || GITHUB_RAW_URL + "/Subnets/IPv4/roblox.lst" ],
|
||
telegram: [ getenv("SUBNETS_TELEGRAM") || GITHUB_RAW_URL + "/Subnets/IPv4/telegram.lst", getenv("SUBNETS_TELEGRAM6") || GITHUB_RAW_URL + "/Subnets/IPv6/telegram.lst" ],
|
||
cloudflare: [ getenv("SUBNETS_CLOUDFLARE") || GITHUB_RAW_URL + "/Subnets/IPv4/cloudflare.lst", getenv("SUBNETS_CLOUDFLARE6") || GITHUB_RAW_URL + "/Subnets/IPv6/cloudflare.lst" ],
|
||
hetzner: [ getenv("SUBNETS_HETZNER") || GITHUB_RAW_URL + "/Subnets/IPv4/hetzner.lst", getenv("SUBNETS_HETZNER6") || GITHUB_RAW_URL + "/Subnets/IPv6/hetzner.lst" ],
|
||
ovh: [ getenv("SUBNETS_OVH") || GITHUB_RAW_URL + "/Subnets/IPv4/ovh.lst", getenv("SUBNETS_OVH6") || GITHUB_RAW_URL + "/Subnets/IPv6/ovh.lst" ],
|
||
digitalocean: [ getenv("SUBNETS_DIGITALOCEAN") || GITHUB_RAW_URL + "/Subnets/IPv4/digitalocean.lst", getenv("SUBNETS_DIGITALOCEAN6") || GITHUB_RAW_URL + "/Subnets/IPv6/digitalocean.lst" ],
|
||
cloudfront: [ getenv("SUBNETS_CLOUDFRONT") || GITHUB_RAW_URL + "/Subnets/IPv4/cloudfront.lst", getenv("SUBNETS_CLOUDFRONT6") || GITHUB_RAW_URL + "/Subnets/IPv6/cloudfront.lst" ]
|
||
};
|
||
let rule_config = null;
|
||
let routing_rulesets_module_value = null;
|
||
let singbox_rulesets_module_value = null;
|
||
|
||
function routing_rulesets_module() {
|
||
if (routing_rulesets_module_value == null)
|
||
routing_rulesets_module_value = require("routing.rulesets");
|
||
return routing_rulesets_module_value;
|
||
}
|
||
|
||
function singbox_rulesets_module() {
|
||
if (singbox_rulesets_module_value == null)
|
||
singbox_rulesets_module_value = require("singbox.rulesets");
|
||
return singbox_rulesets_module_value;
|
||
}
|
||
|
||
function as_string(value) {
|
||
return value == null ? "" : "" + value;
|
||
}
|
||
|
||
function read_stdin() {
|
||
let data = fs.readfile("/dev/stdin");
|
||
return data == null ? "" : data;
|
||
}
|
||
|
||
function shell_quote(value) {
|
||
return "'" + replace(as_string(value), /'/g, "'\\''") + "'";
|
||
}
|
||
|
||
function command_from_args(args) {
|
||
let parts = [];
|
||
|
||
for (let arg in args)
|
||
push(parts, shell_quote(arg));
|
||
|
||
return join(" ", parts);
|
||
}
|
||
|
||
function command_env(assignments) {
|
||
let parts = [];
|
||
|
||
for (let name, value in assignments)
|
||
push(parts, name + "=" + shell_quote(value));
|
||
|
||
return join(" ", parts);
|
||
}
|
||
|
||
function command_output(command) {
|
||
let pipe = fs.popen(command, "r");
|
||
if (!pipe)
|
||
return "";
|
||
|
||
let data = pipe.read("all");
|
||
let status = pipe.close();
|
||
if (status != 0 || data == null)
|
||
return "";
|
||
|
||
return as_string(data);
|
||
}
|
||
|
||
function file_md5(path) {
|
||
path = as_string(path);
|
||
if (path == "" || fs.stat(path) == null)
|
||
return "";
|
||
|
||
let fields = split(trim(command_output(command_from_args([ "md5sum", path ]))), /[ \t\r\n]+/);
|
||
return length(fields) > 0 ? as_string(fields[0]) : "";
|
||
}
|
||
|
||
function command_output_from_args(args) {
|
||
return command_output(command_from_args(args));
|
||
}
|
||
|
||
function command_status(command) {
|
||
let status = int(system(command));
|
||
return status > 255 ? int(status / 256) : status;
|
||
}
|
||
|
||
function command_success(command) {
|
||
return command_status(command + " >/dev/null 2>&1") == 0;
|
||
}
|
||
|
||
function command_success_from_args(args) {
|
||
return system(command_from_args(args) + " >/dev/null 2>&1") == 0;
|
||
}
|
||
|
||
function now_seconds() {
|
||
return int(clock()[0]);
|
||
}
|
||
|
||
function ensure_dir(path) {
|
||
return command_success_from_args([ "mkdir", "-p", path ]);
|
||
}
|
||
|
||
function log_message(message, level) {
|
||
level = as_string(level || "info");
|
||
command_success_from_args([ "logger", "-t", "tachyon", "[" + level + "] " + as_string(message) ]);
|
||
}
|
||
|
||
function read_json_file(path) {
|
||
let data = fs.readfile(path);
|
||
if (data == null)
|
||
return null;
|
||
|
||
try {
|
||
return json(data);
|
||
}
|
||
catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function write_json(value) {
|
||
print(sprintf("%J", value), "\n");
|
||
}
|
||
|
||
function json_text(value) {
|
||
return sprintf("%J", value) + "\n";
|
||
}
|
||
|
||
function write_file(path, value) {
|
||
return fs.writefile(as_string(path), as_string(value)) != null;
|
||
}
|
||
|
||
function write_state_file(path, value) {
|
||
path = as_string(path);
|
||
let stamp = clock();
|
||
let tmp_path = sprintf("%s.%d.%d.tmp", path, stamp[0], stamp[1]);
|
||
|
||
if (!write_file(tmp_path, json_text(value))) {
|
||
fs.unlink(tmp_path);
|
||
return false;
|
||
}
|
||
if (!fs.rename(tmp_path, path)) {
|
||
fs.unlink(tmp_path);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function remove_file(path) {
|
||
try {
|
||
fs.unlink(as_string(path));
|
||
}
|
||
catch (e) {
|
||
}
|
||
}
|
||
|
||
function file_first_line(path) {
|
||
let data = fs.readfile(path);
|
||
if (data == null)
|
||
exit(1);
|
||
|
||
let newline = index(data, "\n");
|
||
print(newline >= 0 ? substr(data, 0, newline) : data, "\n");
|
||
}
|
||
|
||
function file_first_line_value(path) {
|
||
let data = fs.readfile(path);
|
||
if (data == null)
|
||
return "";
|
||
|
||
let newline = index(data, "\n");
|
||
return newline >= 0 ? substr(data, 0, newline) : data;
|
||
}
|
||
|
||
function arg_bool(value) {
|
||
value = lc(as_string(value));
|
||
return value == "true" || value == "1" || value == "yes" || value == "on";
|
||
}
|
||
|
||
function arg_number(value) {
|
||
value = as_string(value);
|
||
if (value == "" || match(value, /[^0-9-]/))
|
||
return 0;
|
||
return int(value);
|
||
}
|
||
|
||
function object_or_empty(value) {
|
||
return type(value) == "object" ? value : {};
|
||
}
|
||
|
||
function text_first_chars(value, max_chars) {
|
||
value = as_string(value);
|
||
max_chars = int(max_chars || "0", 10) || 0;
|
||
return max_chars > 0 && length(value) > max_chars ? substr(value, 0, max_chars) : value;
|
||
}
|
||
|
||
function file_last_nonblank_line_value(path, fallback, max_chars) {
|
||
let data = fs.readfile(path);
|
||
let result = "";
|
||
|
||
if (data != null) {
|
||
for (let line in split(as_string(data), "\n"))
|
||
if (match(line, /^[[:space:]]*$/) == null)
|
||
result = line;
|
||
}
|
||
|
||
if (result == "")
|
||
result = as_string(fallback);
|
||
|
||
return text_first_chars(result, max_chars);
|
||
}
|
||
|
||
function array_or_empty(value) {
|
||
return type(value) == "array" ? value : [];
|
||
}
|
||
|
||
function option(section, key, fallback) {
|
||
if (fallback == null)
|
||
fallback = "";
|
||
|
||
let value = object_or_empty(section)[key];
|
||
if (value == null)
|
||
return fallback;
|
||
if (type(value) == "array")
|
||
return join(" ", value);
|
||
return as_string(value);
|
||
}
|
||
|
||
function bool_option(section, key, fallback) {
|
||
let value = object_or_empty(section)[key];
|
||
return value == null ? !!fallback : arg_bool(value);
|
||
}
|
||
|
||
function section_name(section) {
|
||
return as_string(object_or_empty(section)[".name"]);
|
||
}
|
||
|
||
function file_exists_value(path) {
|
||
return fs.stat(as_string(path)) != null;
|
||
}
|
||
|
||
function file_nonempty(path) {
|
||
return helpers.file_is_usable(path, 0);
|
||
}
|
||
|
||
function copy_file(source, target) {
|
||
let data = fs.readfile(as_string(source));
|
||
if (data == null)
|
||
return false;
|
||
return write_file(target, data);
|
||
}
|
||
|
||
function parent_dir(path) {
|
||
path = as_string(path);
|
||
let slash = rindex(path, "/");
|
||
return slash >= 0 ? substr(path, 0, slash) : "";
|
||
}
|
||
|
||
function ensure_parent_dir(path) {
|
||
let dir = parent_dir(path);
|
||
return dir == "" || dir == "." || ensure_dir(dir);
|
||
}
|
||
|
||
function temp_path() {
|
||
return trim(command_output_from_args([ "mktemp" ]));
|
||
}
|
||
|
||
function remove_files(paths) {
|
||
for (let path in paths)
|
||
if (as_string(path) != "")
|
||
remove_file(path);
|
||
}
|
||
|
||
function owner_pid() {
|
||
let pid = trim(command_output_from_args([ "sh", "-c", "echo $PPID" ]));
|
||
return match(pid, /^[0-9]+$/) != null ? pid : "0";
|
||
}
|
||
|
||
function runtime_pid_running(pid) {
|
||
pid = as_string(pid);
|
||
return match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ]);
|
||
}
|
||
|
||
function whitespace_items(value) {
|
||
let result = [];
|
||
if (type(value) == "array") {
|
||
for (let item in value) {
|
||
item = as_string(item);
|
||
if (item != "")
|
||
push(result, item);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
for (let item in split(trim(as_string(value)), /[ \t\r\n]+/))
|
||
if (item != "")
|
||
push(result, item);
|
||
return result;
|
||
}
|
||
|
||
function list_option_values(section, key) {
|
||
return whitespace_items(object_or_empty(section)[key]);
|
||
}
|
||
|
||
function uci_sections(type_name) {
|
||
return uci_core.section_objects(CONFIG_NAME, as_string(type_name));
|
||
}
|
||
|
||
function uci_settings() {
|
||
return object_or_empty(uci_core.get_all(CONFIG_NAME, "settings"));
|
||
}
|
||
|
||
function module_command(args) {
|
||
let command_args = [ "ucode", "-L", LIB_DIR ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
return command_from_args(command_args);
|
||
}
|
||
|
||
function module_status(args) {
|
||
return command_status(module_command(args));
|
||
}
|
||
|
||
function module_success(args) {
|
||
return module_status(args) == 0;
|
||
}
|
||
|
||
function module_output(args) {
|
||
return command_output(module_command(args));
|
||
}
|
||
|
||
function nft_module_success(args) {
|
||
let command_args = [ LIB_DIR + "/nft/apply.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
return module_success(command_args);
|
||
}
|
||
|
||
function service_state_success(args) {
|
||
let command_args = [ LIB_DIR + "/service/state.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
return module_success(command_args);
|
||
}
|
||
|
||
function acquire_runtime_lock(lock_dir, wait) {
|
||
return service_state_success([
|
||
wait ? "acquire-runtime-dir-lock-wait" : "acquire-runtime-dir-lock",
|
||
lock_dir,
|
||
owner_pid(),
|
||
"300"
|
||
]);
|
||
}
|
||
|
||
function release_runtime_lock(lock_dir) {
|
||
service_state_success([ "release-runtime-dir-lock", lock_dir ]);
|
||
}
|
||
|
||
function unsigned_number(value) {
|
||
value = as_string(value);
|
||
if (value == "" || match(value, /[^0-9]/) != null)
|
||
return null;
|
||
return int(value);
|
||
}
|
||
|
||
function update_due_status(now_value, last_run_value, interval_value) {
|
||
let now = unsigned_number(now_value);
|
||
let interval = unsigned_number(interval_value);
|
||
|
||
if (now == null || interval == null || interval <= 0)
|
||
return 2;
|
||
|
||
let last_run = unsigned_number(last_run_value);
|
||
if (last_run == null)
|
||
last_run = 0;
|
||
|
||
if (last_run > 0 && now - last_run < interval)
|
||
return 1;
|
||
|
||
return 0;
|
||
}
|
||
|
||
function update_is_due(now_value, last_run_value, interval_value) {
|
||
exit(update_due_status(now_value, last_run_value, interval_value));
|
||
}
|
||
|
||
function duration_to_seconds_value(value) {
|
||
let rest = as_string(value);
|
||
if (rest == "")
|
||
return null;
|
||
|
||
let total = 0.0;
|
||
let multipliers = {
|
||
ns: 0.000000001,
|
||
us: 0.000001,
|
||
ms: 0.001,
|
||
s: 1,
|
||
m: 60,
|
||
h: 3600,
|
||
d: 86400
|
||
};
|
||
|
||
while (rest != "") {
|
||
let matched = match(rest, /^([0-9]+(\.[0-9]+)?)(ns|us|ms|s|m|h|d)/);
|
||
if (!matched)
|
||
return null;
|
||
|
||
let token = as_string(matched[0]);
|
||
let amount = matched[1] * 1;
|
||
let unit = matched[3];
|
||
total = total + amount * multipliers[unit];
|
||
rest = substr(rest, length(token));
|
||
}
|
||
|
||
if (total <= 0)
|
||
return null;
|
||
|
||
return int(total + 0.5);
|
||
}
|
||
|
||
function duration_to_seconds(value) {
|
||
let seconds = duration_to_seconds_value(value);
|
||
if (seconds == null)
|
||
exit(1);
|
||
|
||
print(seconds, "\n");
|
||
}
|
||
|
||
function due_check_cron_schedule_text(value) {
|
||
let seconds = arg_number(value);
|
||
|
||
if (seconds <= 60)
|
||
return "* * * * *";
|
||
|
||
if (seconds % 86400 == 0)
|
||
return "0 0 * * *";
|
||
|
||
if (seconds % 3600 == 0) {
|
||
let hours = seconds / 3600;
|
||
if (hours >= 1 && hours <= 23)
|
||
return hours == 1 ? "0 * * * *" : "0 */" + hours + " * * *";
|
||
}
|
||
|
||
if (seconds % 60 == 0) {
|
||
let minutes = seconds / 60;
|
||
if (minutes >= 1 && minutes <= 59)
|
||
return minutes == 1 ? "* * * * *" : "*/" + minutes + " * * * *";
|
||
}
|
||
|
||
return "* * * * *";
|
||
}
|
||
|
||
function due_check_cron_schedule(value) {
|
||
print(due_check_cron_schedule_text(value), "\n");
|
||
}
|
||
|
||
function update_cron_job(interval, command, bin, marker) {
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null)
|
||
exit(1);
|
||
|
||
print(due_check_cron_schedule_text(seconds), " ", as_string(bin), " ", as_string(command), " ", as_string(marker), "\n");
|
||
}
|
||
|
||
function subscription_update_cron_job(min_interval_seconds, bin, marker) {
|
||
let seconds = arg_number(min_interval_seconds);
|
||
if (seconds <= 0)
|
||
exit(1);
|
||
|
||
print(due_check_cron_schedule_text(seconds), " ", as_string(bin), " subscription_update_if_due ", as_string(marker), "\n");
|
||
}
|
||
|
||
function subscription_update_interval_plan() {
|
||
let min_interval = 0;
|
||
|
||
for (let line in split(read_stdin(), "\n")) {
|
||
line = as_string(line);
|
||
if (line == "")
|
||
continue;
|
||
|
||
let separator = index(line, "\t");
|
||
let section = separator >= 0 ? substr(line, 0, separator) : line;
|
||
let interval = separator >= 0 ? substr(line, separator + 1) : "";
|
||
if (interval == "")
|
||
continue;
|
||
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
print("error\t", section, "\t", interval, "\n");
|
||
continue;
|
||
}
|
||
|
||
if (min_interval == 0 || seconds < min_interval)
|
||
min_interval = seconds;
|
||
}
|
||
|
||
print("min\t", min_interval, "\n");
|
||
}
|
||
|
||
function list_has_remote_references(value) {
|
||
for (let item in split(as_string(value), /[ \t\r\n]+/)) {
|
||
if (match(item, /^https?:\/\//) != null)
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function rule_has_list_update_source(section) {
|
||
section = object_or_empty(section);
|
||
if (rule_config == null)
|
||
rule_config = require("config.rule");
|
||
|
||
if (!bool_option(section, "enabled", true))
|
||
return false;
|
||
if (option(section, "action", "") == "dns")
|
||
return list_has_remote_references(option(section, "domain_ip_lists", ""));
|
||
|
||
return (
|
||
rule_config.has_community_subnet_list(connections.community_lists_value(section)) ||
|
||
option(section, "remote_domain_lists", "") != "" ||
|
||
option(section, "remote_subnet_lists", "") != "" ||
|
||
length(connections.rule_sets_with_subnets(section)) > 0 ||
|
||
list_has_remote_references(option(section, "domain_ip_lists", ""))
|
||
);
|
||
}
|
||
|
||
function has_list_update_sources(sections, settings) {
|
||
if (settings == null)
|
||
settings = uci_settings();
|
||
if (bool_option(settings, "download_all_presets", false))
|
||
return true;
|
||
for (let section in sections)
|
||
if (rule_has_list_update_source(section))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
function settings_update_interval(settings) {
|
||
settings = object_or_empty(settings);
|
||
|
||
if (!bool_option(settings, "list_update_enabled", true))
|
||
return "";
|
||
|
||
let value = option(settings, "update_interval", "1d");
|
||
return value != "" ? value : "1d";
|
||
}
|
||
|
||
function settings_component_update_check_interval(settings) {
|
||
settings = object_or_empty(settings);
|
||
|
||
if (!bool_option(settings, "component_update_check_enabled", false))
|
||
return "";
|
||
|
||
let value = option(settings, "component_update_check_interval", "1d");
|
||
return value != "" ? value : "1d";
|
||
}
|
||
|
||
function section_subscription_update_interval(section) {
|
||
let result = "";
|
||
let result_seconds = 0;
|
||
|
||
for (let entry in connections.subscription_urls(section)) {
|
||
if (!connections.subscription_update_enabled(section, entry))
|
||
continue;
|
||
|
||
let value = connections.subscription_update_interval(section, entry);
|
||
if (value == "")
|
||
value = "1h";
|
||
let seconds = duration_to_seconds_value(value);
|
||
if (result == "" || (seconds != null && (result_seconds == 0 || seconds < result_seconds))) {
|
||
result = value;
|
||
result_seconds = seconds == null ? 0 : seconds;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function section_is_subscription_proxy(section) {
|
||
return bool_option(section, "enabled", true) &&
|
||
connections.is_connections_action(option(section, "action", "")) &&
|
||
length(connections.subscription_urls(section)) > 0;
|
||
}
|
||
|
||
function line_contains_any_marker(line, markers) {
|
||
line = as_string(line);
|
||
for (let marker in markers) {
|
||
marker = as_string(marker);
|
||
if (marker != "" && index(line, marker) >= 0)
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function filter_cron_markers_text(data, markers) {
|
||
data = as_string(data);
|
||
if (data == "")
|
||
return "";
|
||
|
||
let lines = split(data, "\n");
|
||
let has_trailing_newline = substr(data, length(data) - 1) == "\n";
|
||
let result = "";
|
||
|
||
for (let i = 0; i < length(lines); i++) {
|
||
let line = as_string(lines[i]);
|
||
if (i == length(lines) - 1 && has_trailing_newline && line == "")
|
||
continue;
|
||
if (line_contains_any_marker(line, markers))
|
||
continue;
|
||
result += line + "\n";
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function filter_cron_markers(markers) {
|
||
print(filter_cron_markers_text(read_stdin(), markers));
|
||
}
|
||
|
||
function cron_refresh_plan_rows(settings, sections, bin, list_marker, subscription_marker, component_marker) {
|
||
let status = 0;
|
||
let rows = [];
|
||
|
||
if (has_list_update_sources(sections, settings)) {
|
||
let interval = settings_update_interval(settings);
|
||
if (interval == "") {
|
||
push(rows, "list-disabled");
|
||
}
|
||
else {
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
push(rows, "list-error\t" + as_string(interval));
|
||
status = 1;
|
||
}
|
||
else {
|
||
push(rows, "list\t" + due_check_cron_schedule_text(seconds) + " " + as_string(bin) + " list_update_if_due " + as_string(list_marker));
|
||
}
|
||
}
|
||
}
|
||
|
||
let min_interval = 0;
|
||
let subscription_source_count = 0;
|
||
for (let section in sections) {
|
||
section = object_or_empty(section);
|
||
if (!section_is_subscription_proxy(section))
|
||
continue;
|
||
|
||
let interval = section_subscription_update_interval(section);
|
||
if (interval == "")
|
||
continue;
|
||
|
||
subscription_source_count++;
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
push(rows, "subscription-error\t" + section_name(section) + "\t" + as_string(interval));
|
||
continue;
|
||
}
|
||
|
||
if (min_interval == 0 || seconds < min_interval)
|
||
min_interval = seconds;
|
||
}
|
||
|
||
if (subscription_source_count > 0) {
|
||
if (min_interval <= 0)
|
||
status = 1;
|
||
else
|
||
push(rows, "subscription\t" + due_check_cron_schedule_text(min_interval) + " " + as_string(bin) + " subscription_update_if_due " + as_string(subscription_marker));
|
||
}
|
||
|
||
let component_interval = settings_component_update_check_interval(settings);
|
||
if (component_interval != "") {
|
||
let component_seconds = duration_to_seconds_value(component_interval);
|
||
if (component_seconds == null) {
|
||
push(rows, "component-error\t" + as_string(component_interval));
|
||
status = 1;
|
||
}
|
||
else {
|
||
push(rows, "component\t" + due_check_cron_schedule_text(component_seconds) + " " + as_string(bin) + " component_updates_if_due " + as_string(component_marker));
|
||
}
|
||
}
|
||
|
||
return {
|
||
status,
|
||
rows
|
||
};
|
||
}
|
||
|
||
function print_cron_refresh_plan(result) {
|
||
result = object_or_empty(result);
|
||
for (let row in result.rows)
|
||
print(row, "\n");
|
||
exit(int(result.status || 0));
|
||
}
|
||
|
||
function cron_refresh_plan(settings, sections, bin, list_marker, subscription_marker, component_marker) {
|
||
print_cron_refresh_plan(cron_refresh_plan_rows(settings, sections, bin, list_marker, subscription_marker, component_marker));
|
||
}
|
||
|
||
function cron_refresh_apply_result(settings, sections, existing_crontab, bin, list_marker, subscription_marker, component_marker) {
|
||
let plan = cron_refresh_plan_rows(settings, sections, bin, list_marker, subscription_marker, component_marker);
|
||
let filtered_crontab = filter_cron_markers_text(existing_crontab, [ list_marker, subscription_marker, component_marker ]);
|
||
let cron_jobs = "";
|
||
let logs = [ { level: "info", message: "The cron job removed" } ];
|
||
let tab = "\t";
|
||
|
||
for (let row in plan.rows) {
|
||
let line = as_string(row);
|
||
let separator = index(line, tab);
|
||
let type = separator >= 0 ? substr(line, 0, separator) : line;
|
||
let rest = separator >= 0 ? substr(line, separator + 1) : "";
|
||
|
||
if (type == "list") {
|
||
cron_jobs += rest + "\n";
|
||
push(logs, { level: "info", message: "The cron job has been created: " + rest });
|
||
}
|
||
else if (type == "list-disabled") {
|
||
push(logs, { level: "info", message: "Remote list auto-update is disabled" });
|
||
}
|
||
else if (type == "list-error") {
|
||
push(logs, { level: "error", message: "Invalid update_interval value: " + rest });
|
||
}
|
||
else if (type == "subscription") {
|
||
cron_jobs += rest + "\n";
|
||
push(logs, { level: "info", message: "The subscription cron job has been created: " + rest });
|
||
}
|
||
else if (type == "subscription-error") {
|
||
let section_separator = index(rest, tab);
|
||
let section = section_separator >= 0 ? substr(rest, 0, section_separator) : rest;
|
||
let interval = section_separator >= 0 ? substr(rest, section_separator + 1) : "";
|
||
push(logs, { level: "error", message: "Invalid subscription_update_interval value for rule '" + section + "': " + interval });
|
||
}
|
||
else if (type == "component") {
|
||
cron_jobs += rest + "\n";
|
||
push(logs, { level: "info", message: "The component update check cron job has been created: " + rest });
|
||
}
|
||
else if (type == "component-error") {
|
||
push(logs, { level: "error", message: "Invalid component_update_check_interval value: " + rest });
|
||
}
|
||
}
|
||
|
||
return {
|
||
status: int(plan.status || 0),
|
||
crontab: filtered_crontab + (int(plan.status || 0) == 0 ? cron_jobs : ""),
|
||
logs
|
||
};
|
||
}
|
||
|
||
function write_crontab_text(text) {
|
||
let tmp = trim(command_output_from_args([ "mktemp" ]));
|
||
if (tmp == "")
|
||
return false;
|
||
|
||
if (fs.writefile(tmp, as_string(text)) == null) {
|
||
fs.unlink(tmp);
|
||
return false;
|
||
}
|
||
|
||
let ok = command_success_from_args([ "crontab", tmp ]);
|
||
fs.unlink(tmp);
|
||
return ok;
|
||
}
|
||
|
||
function log_cron_apply_result(result) {
|
||
for (let item in array_or_empty(object_or_empty(result).logs))
|
||
log_message(item.message, item.level);
|
||
}
|
||
|
||
function remove_cron_jobs(list_marker, subscription_marker, component_marker) {
|
||
let crontab = command_output_from_args([ "crontab", "-l" ]);
|
||
let result = {
|
||
crontab: filter_cron_markers_text(crontab, [ list_marker, subscription_marker, component_marker ]),
|
||
logs: [ { level: "info", message: "The cron job removed" } ]
|
||
};
|
||
|
||
if (!write_crontab_text(result.crontab))
|
||
exit(1);
|
||
|
||
log_cron_apply_result(result);
|
||
}
|
||
|
||
function refresh_cron_from_sources(settings, sections, bin, list_marker, subscription_marker, component_marker) {
|
||
let result = cron_refresh_apply_result(
|
||
settings,
|
||
sections,
|
||
command_output_from_args([ "crontab", "-l" ]),
|
||
bin,
|
||
list_marker,
|
||
subscription_marker,
|
||
component_marker
|
||
);
|
||
|
||
if (!write_crontab_text(result.crontab))
|
||
exit(1);
|
||
|
||
log_cron_apply_result(result);
|
||
exit(result.status);
|
||
}
|
||
|
||
function list_update_due_status(settings, timestamp_path, now) {
|
||
let interval = settings_update_interval(settings);
|
||
if (interval == "")
|
||
exit(1);
|
||
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
print("error\t", interval, "\n");
|
||
exit(2);
|
||
}
|
||
|
||
exit(update_due_status(now, file_first_line_value(timestamp_path), seconds));
|
||
}
|
||
|
||
function subscription_update_section_due_status(section, timestamp_path, now) {
|
||
section = object_or_empty(section);
|
||
let interval = section_subscription_update_interval(section);
|
||
if (interval == "")
|
||
exit(1);
|
||
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
print("error\t", interval, "\n");
|
||
exit(2);
|
||
}
|
||
|
||
exit(update_due_status(now, file_first_line_value(timestamp_path), seconds));
|
||
}
|
||
|
||
function stdin_first_ipv4_line() {
|
||
for (let line in split(read_stdin(), "\n")) {
|
||
line = as_string(line);
|
||
if (match(line, /^[0-9]+\./) != null) {
|
||
print(line, "\n");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
function json_length(path) {
|
||
let value = read_json_file(path);
|
||
if (type(value) == "array" || type(value) == "object")
|
||
print(length(value), "\n");
|
||
else
|
||
print("0\n");
|
||
}
|
||
|
||
function job_pid(path) {
|
||
let value = read_json_file(path);
|
||
if (type(value) == "object" && value.pid != null)
|
||
print(as_string(value.pid), "\n");
|
||
}
|
||
|
||
function subscription_job_state_path(job_dir, job_id) {
|
||
job_dir = as_string(job_dir);
|
||
job_id = as_string(job_id);
|
||
|
||
if (job_id == "" || job_id == "." || job_id == ".." || match(job_id, /[^A-Za-z0-9._-]/) != null)
|
||
exit(1);
|
||
|
||
print(job_dir, "/", job_id, ".json\n");
|
||
}
|
||
|
||
function subscription_job_json_response(success, job_id, message) {
|
||
write_json({
|
||
success: arg_bool(success),
|
||
job_id: as_string(job_id),
|
||
message: as_string(message)
|
||
});
|
||
}
|
||
|
||
function subscription_running_job_state_value(section, source_index, started_at) {
|
||
return {
|
||
success: true,
|
||
running: true,
|
||
kind: "subscription",
|
||
message: "Subscription update is running",
|
||
section: as_string(section),
|
||
source_index: as_string(source_index),
|
||
pid: null,
|
||
started_at: arg_number(started_at),
|
||
updated_at: null,
|
||
exit_code: null
|
||
};
|
||
}
|
||
|
||
function subscription_running_job_state(section, source_index, started_at) {
|
||
write_json(subscription_running_job_state_value(section, source_index, started_at));
|
||
}
|
||
|
||
function job_started_at_within_grace(value, now, grace_seconds) {
|
||
let started_at = arg_number(value);
|
||
now = arg_number(now);
|
||
grace_seconds = arg_number(grace_seconds);
|
||
|
||
if (started_at <= 0 || now <= 0)
|
||
return false;
|
||
|
||
return now - started_at < grace_seconds;
|
||
}
|
||
|
||
function job_pid_valid(pid) {
|
||
pid = as_string(pid);
|
||
return pid != "" && match(pid, /^[0-9]+$/) != null;
|
||
}
|
||
|
||
function set_subscription_running_job_pid(path, pid) {
|
||
pid = as_string(pid);
|
||
if (!job_pid_valid(pid))
|
||
return false;
|
||
|
||
let value = object_or_empty(read_json_file(path));
|
||
if (value.running === true) {
|
||
value.pid = pid;
|
||
return write_state_file(path, value);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function subscription_job_refresh_plan(path, now, grace_seconds) {
|
||
let value = read_json_file(path);
|
||
if (type(value) != "object" || value.running !== true) {
|
||
print("skip\n");
|
||
return;
|
||
}
|
||
|
||
let within_grace = job_started_at_within_grace(value.started_at, now, grace_seconds);
|
||
let pid = as_string(value.pid || "");
|
||
if (!job_pid_valid(pid)) {
|
||
print(within_grace ? "skip\n" : "stale\n");
|
||
return;
|
||
}
|
||
|
||
print("pid\t", pid, "\t", within_grace ? "0" : "1", "\n");
|
||
}
|
||
|
||
function subscription_finished_job_state_value(success, message, exit_code, updated_at, section, source_index, started_at) {
|
||
return {
|
||
success: arg_bool(success),
|
||
running: false,
|
||
kind: "subscription",
|
||
message: as_string(message),
|
||
section: as_string(section),
|
||
source_index: as_string(source_index),
|
||
pid: null,
|
||
started_at: arg_number(started_at),
|
||
exit_code: arg_number(exit_code),
|
||
updated_at: arg_number(updated_at)
|
||
};
|
||
}
|
||
|
||
function subscription_finished_job_state(success, message, exit_code, updated_at, section, source_index, started_at) {
|
||
write_json(subscription_finished_job_state_value(success, message, exit_code, updated_at, section, source_index, started_at));
|
||
}
|
||
|
||
function subscription_stale_job_state_value(updated_at, section, source_index, started_at) {
|
||
return {
|
||
success: false,
|
||
running: false,
|
||
kind: "subscription",
|
||
message: "Subscription update worker exited unexpectedly",
|
||
section: as_string(section),
|
||
source_index: as_string(source_index),
|
||
pid: null,
|
||
started_at: arg_number(started_at),
|
||
exit_code: null,
|
||
updated_at: arg_number(updated_at)
|
||
};
|
||
}
|
||
|
||
function subscription_stale_job_state(updated_at, section, source_index, started_at) {
|
||
write_json(subscription_stale_job_state_value(updated_at, section, source_index, started_at));
|
||
}
|
||
|
||
function subscription_status_error(message) {
|
||
write_json({
|
||
success: false,
|
||
running: false,
|
||
message: as_string(message),
|
||
exit_code: null
|
||
});
|
||
}
|
||
|
||
function subscription_status_error_exit(message) {
|
||
subscription_status_error(message);
|
||
exit(1);
|
||
}
|
||
|
||
function valid_subscription_job_id(job_id) {
|
||
job_id = as_string(job_id);
|
||
return job_id != "" && job_id != "." && job_id != ".." && match(job_id, /[^A-Za-z0-9._-]/) == null;
|
||
}
|
||
|
||
function subscription_job_state_path_value(job_dir, job_id) {
|
||
if (!valid_subscription_job_id(job_id))
|
||
return "";
|
||
return as_string(job_dir) + "/" + as_string(job_id) + ".json";
|
||
}
|
||
|
||
function subscription_job_id() {
|
||
let stamp = clock();
|
||
return sprintf("%d-%d", stamp[0], stamp[1]);
|
||
}
|
||
|
||
function ensure_subscription_runtime_dirs() {
|
||
let command = command_env({
|
||
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
||
TACHYON_LIB: LIB_DIR,
|
||
TMP_SING_BOX_FOLDER,
|
||
TMP_RULESET_FOLDER,
|
||
TMP_SUBSCRIPTION_FOLDER,
|
||
TACHYON_RUNTIME_STATE_DIR: RUNTIME_STATE_DIR,
|
||
TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR: SUBSCRIPTION_UPDATE_STATE_DIR,
|
||
TACHYON_SUBSCRIPTION_LINKS_DIR: SUBSCRIPTION_LINKS_DIR,
|
||
TACHYON_SUBSCRIPTION_METADATA_DIR: SUBSCRIPTION_METADATA_DIR,
|
||
TACHYON_OUTBOUND_METADATA_DIR: OUTBOUND_METADATA_DIR,
|
||
TACHYON_SECTION_CACHE_DIR: SECTION_CACHE_DIR,
|
||
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT
|
||
}) + " " + command_from_args([
|
||
"ucode",
|
||
"-L", LIB_DIR,
|
||
LIB_DIR + "/subscription/cache.uc",
|
||
"ensure-runtime-dirs"
|
||
]);
|
||
|
||
return command_success(command) && ensure_dir(SUBSCRIPTION_JOB_DIR);
|
||
}
|
||
|
||
function subscription_job_output_path(job_id) {
|
||
return SUBSCRIPTION_JOB_DIR + "/" + as_string(job_id) + ".out";
|
||
}
|
||
|
||
function subscription_job_output_path_from_state(path) {
|
||
path = as_string(path);
|
||
if (length(path) >= 5 && substr(path, length(path) - 5) == ".json")
|
||
return substr(path, 0, length(path) - 5) + ".out";
|
||
return path + ".out";
|
||
}
|
||
|
||
function remove_subscription_job_state(path) {
|
||
remove_file(path);
|
||
remove_file(subscription_job_output_path_from_state(path));
|
||
}
|
||
|
||
function subscription_job_running_is(path, expected) {
|
||
let value = read_json_file(path);
|
||
let running = type(value) == "object" && value.running === true;
|
||
return running == arg_bool(expected);
|
||
}
|
||
|
||
function subscription_cleanup_jobs() {
|
||
ensure_dir(SUBSCRIPTION_JOB_DIR);
|
||
|
||
command_success_from_args([
|
||
"find",
|
||
SUBSCRIPTION_JOB_DIR,
|
||
"-type", "f",
|
||
"-name", "*.out",
|
||
"-mmin", "+" + as_string(SUBSCRIPTION_JOB_ORPHAN_OUTPUT_TTL_MINUTES),
|
||
"-delete"
|
||
]);
|
||
|
||
let old = command_output_from_args([
|
||
"find",
|
||
SUBSCRIPTION_JOB_DIR,
|
||
"-type", "f",
|
||
"-name", "*.json",
|
||
"-mmin", "+" + as_string(SUBSCRIPTION_JOB_FINISHED_TTL_MINUTES)
|
||
]);
|
||
|
||
for (let path in split(old, "\n")) {
|
||
path = trim(as_string(path));
|
||
if (path != "" && subscription_job_running_is(path, false))
|
||
remove_subscription_job_state(path);
|
||
}
|
||
}
|
||
|
||
function pid_running(pid) {
|
||
pid = as_string(pid);
|
||
return job_pid_valid(pid) && command_success_from_args([ "kill", "-0", pid ]);
|
||
}
|
||
|
||
function write_subscription_stale_job_state(path) {
|
||
let value = object_or_empty(read_json_file(path));
|
||
return write_state_file(path, subscription_stale_job_state_value(
|
||
now_seconds(),
|
||
value.section || "",
|
||
value.source_index || "",
|
||
value.started_at || 0
|
||
));
|
||
}
|
||
|
||
function refresh_subscription_running_job_state(path) {
|
||
let value = read_json_file(path);
|
||
if (type(value) != "object" || value.running !== true)
|
||
return;
|
||
|
||
let now = now_seconds();
|
||
let within_grace = job_started_at_within_grace(value.started_at, now, SUBSCRIPTION_JOB_STALE_GRACE_SECONDS);
|
||
let pid = as_string(value.pid || "");
|
||
|
||
if (!job_pid_valid(pid)) {
|
||
if (!within_grace)
|
||
write_subscription_stale_job_state(path);
|
||
return;
|
||
}
|
||
|
||
if (pid_running(pid))
|
||
return;
|
||
if (within_grace)
|
||
return;
|
||
|
||
command_success_from_args([ "sleep", "1" ]);
|
||
value = read_json_file(path);
|
||
if (type(value) != "object" || value.running !== true)
|
||
return;
|
||
if (pid_running(pid))
|
||
return;
|
||
|
||
write_subscription_stale_job_state(path);
|
||
}
|
||
|
||
function finish_subscription_job(path, exit_code, output_file) {
|
||
let value = object_or_empty(read_json_file(path));
|
||
let success = arg_number(exit_code) == 0;
|
||
let message = file_last_nonblank_line_value(
|
||
output_file,
|
||
success ? "Subscription update completed" : "Subscription update failed",
|
||
240
|
||
);
|
||
|
||
let ok = write_state_file(path, subscription_finished_job_state_value(
|
||
success,
|
||
message,
|
||
exit_code,
|
||
now_seconds(),
|
||
value.section || "",
|
||
value.source_index || "",
|
||
value.started_at || 0
|
||
));
|
||
remove_file(output_file);
|
||
return ok;
|
||
}
|
||
|
||
function subscription_worker_env() {
|
||
return {
|
||
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
||
TACHYON_LIB: LIB_DIR,
|
||
TACHYON_BIN: BIN_PATH,
|
||
TMP_SING_BOX_FOLDER,
|
||
TMP_RULESET_FOLDER,
|
||
TMP_SUBSCRIPTION_FOLDER,
|
||
TACHYON_RUNTIME_STATE_DIR: RUNTIME_STATE_DIR,
|
||
TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR: SUBSCRIPTION_UPDATE_STATE_DIR,
|
||
TACHYON_SUBSCRIPTION_UPDATE_JOB_DIR: SUBSCRIPTION_JOB_DIR,
|
||
TACHYON_SUBSCRIPTION_LINKS_DIR: SUBSCRIPTION_LINKS_DIR,
|
||
TACHYON_SUBSCRIPTION_METADATA_DIR: SUBSCRIPTION_METADATA_DIR,
|
||
TACHYON_OUTBOUND_METADATA_DIR: OUTBOUND_METADATA_DIR,
|
||
TACHYON_SECTION_CACHE_DIR: SECTION_CACHE_DIR,
|
||
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT,
|
||
TACHYON_SUBSCRIPTION_UPDATE_JOB_FINISHED_TTL_MINUTES: SUBSCRIPTION_JOB_FINISHED_TTL_MINUTES,
|
||
TACHYON_SUBSCRIPTION_UPDATE_JOB_ORPHAN_OUTPUT_TTL_MINUTES: SUBSCRIPTION_JOB_ORPHAN_OUTPUT_TTL_MINUTES,
|
||
TACHYON_UI_ACTION_STALE_GRACE_SECONDS: SUBSCRIPTION_JOB_STALE_GRACE_SECONDS
|
||
};
|
||
}
|
||
|
||
function launch_subscription_worker(args) {
|
||
let command_args = [ "ucode", "-L", LIB_DIR, LIB_DIR + "/components/updates.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
|
||
let command = command_env(subscription_worker_env()) + " " +
|
||
command_from_args(command_args) +
|
||
" >/dev/null 2>&1 1000>&- & echo $!";
|
||
return trim(command_output("sh -c " + shell_quote(command)));
|
||
}
|
||
|
||
function subscription_update_worker(state_file, output_file, section, source_index) {
|
||
let status = command_status(command_from_args([
|
||
BIN_PATH,
|
||
"subscription_update",
|
||
as_string(section),
|
||
as_string(source_index)
|
||
]) + " >" + shell_quote(output_file) + " 2>&1");
|
||
|
||
finish_subscription_job(state_file, status, output_file);
|
||
}
|
||
|
||
function subscription_update_async(section, source_index) {
|
||
if (!ensure_subscription_runtime_dirs()) {
|
||
subscription_job_json_response(false, "", "Failed to create subscription update state directory");
|
||
exit(1);
|
||
}
|
||
|
||
subscription_cleanup_jobs();
|
||
|
||
let job_id = subscription_job_id();
|
||
let state_file = subscription_job_state_path_value(SUBSCRIPTION_JOB_DIR, job_id);
|
||
if (state_file == "") {
|
||
subscription_job_json_response(false, "", "Failed to prepare subscription update job");
|
||
exit(1);
|
||
}
|
||
|
||
if (!write_state_file(state_file, subscription_running_job_state_value(section, source_index, now_seconds()))) {
|
||
subscription_job_json_response(false, "", "Failed to write subscription update state");
|
||
exit(1);
|
||
}
|
||
|
||
let output_file = subscription_job_output_path(job_id);
|
||
let pid = launch_subscription_worker([
|
||
"subscription-update-worker",
|
||
state_file,
|
||
output_file,
|
||
as_string(section),
|
||
as_string(source_index)
|
||
]);
|
||
|
||
if (pid == "" || !set_subscription_running_job_pid(state_file, pid)) {
|
||
if (pid != "")
|
||
command_success_from_args([ "kill", pid ]);
|
||
subscription_job_json_response(false, "", "Failed to write subscription update worker pid");
|
||
exit(1);
|
||
}
|
||
|
||
subscription_job_json_response(true, job_id, "Subscription update started");
|
||
}
|
||
|
||
function subscription_update_status(job_id) {
|
||
ensure_dir(SUBSCRIPTION_JOB_DIR);
|
||
subscription_cleanup_jobs();
|
||
|
||
let state_file = subscription_job_state_path_value(SUBSCRIPTION_JOB_DIR, job_id);
|
||
if (state_file == "")
|
||
subscription_status_error_exit("Invalid subscription update job id");
|
||
|
||
if (fs.stat(state_file) == null)
|
||
subscription_status_error_exit("Subscription update job was not found");
|
||
|
||
refresh_subscription_running_job_state(state_file);
|
||
print(as_string(fs.readfile(state_file)));
|
||
}
|
||
|
||
function component_job_json_response(success, job_id, message) {
|
||
write_json({
|
||
success: arg_bool(success),
|
||
job_id: as_string(job_id),
|
||
message: as_string(message)
|
||
});
|
||
}
|
||
|
||
function component_action_status_error(message) {
|
||
write_json({
|
||
success: false,
|
||
running: false,
|
||
kind: "component",
|
||
component: "unknown",
|
||
action: "status",
|
||
message: as_string(message),
|
||
current_version: "",
|
||
latest_version: "",
|
||
changed: 0,
|
||
status: "",
|
||
exit_code: null
|
||
});
|
||
}
|
||
|
||
function component_action_status_error_exit(message) {
|
||
component_action_status_error(message);
|
||
exit(1);
|
||
}
|
||
|
||
function component_running_job_state_value(component, action, started_at) {
|
||
return {
|
||
success: true,
|
||
running: true,
|
||
kind: "component",
|
||
component: as_string(component),
|
||
action: as_string(action),
|
||
message: "Component action is running",
|
||
pid: null,
|
||
started_at: arg_number(started_at),
|
||
updated_at: null,
|
||
current_version: "",
|
||
latest_version: "",
|
||
changed: 0,
|
||
status: "",
|
||
exit_code: null
|
||
};
|
||
}
|
||
|
||
function normalize_component_name(component) {
|
||
component = as_string(component);
|
||
if (component == "sing-box" || component == "singbox")
|
||
return "sing_box";
|
||
if (component == "tachyon")
|
||
return "tachyon";
|
||
return component;
|
||
}
|
||
|
||
function valid_component_name(component) {
|
||
component = normalize_component_name(component);
|
||
return component == "tachyon" || component == "sing_box" || component == "zapret" ||
|
||
component == "zapret2" || component == "byedpi";
|
||
}
|
||
|
||
function component_update_check_cache_path(component) {
|
||
component = normalize_component_name(component);
|
||
if (!valid_component_name(component))
|
||
return "";
|
||
return COMPONENT_UPDATE_CHECK_CACHE_DIR + "/" + component + ".json";
|
||
}
|
||
|
||
function component_update_check_cache_enabled() {
|
||
return settings_component_update_check_interval(uci_settings()) != "";
|
||
}
|
||
|
||
function component_update_check_result_cacheable(value) {
|
||
value = object_or_empty(value);
|
||
let status = as_string(value.status || "");
|
||
return value.success === true && as_string(value.action) == "check_update" &&
|
||
valid_component_name(value.component) &&
|
||
(status == "latest" || status == "outdated" || status == "dev");
|
||
}
|
||
|
||
function cache_component_update_check_result(value, notify_update) {
|
||
value = object_or_empty(value);
|
||
if (!component_update_check_result_cacheable(value))
|
||
return false;
|
||
|
||
let component = normalize_component_name(value.component);
|
||
let path = component_update_check_cache_path(component);
|
||
if (path == "" || !ensure_dir(COMPONENT_UPDATE_CHECK_CACHE_DIR))
|
||
return false;
|
||
|
||
let previous = object_or_empty(read_json_file(path));
|
||
let cached = {
|
||
success: true,
|
||
component,
|
||
action: "check_update",
|
||
message: as_string(value.message),
|
||
current_version: as_string(value.current_version),
|
||
latest_version: as_string(value.latest_version),
|
||
release_url: as_string(value.release_url),
|
||
changed: 0,
|
||
status: as_string(value.status),
|
||
updated_at: now_seconds()
|
||
};
|
||
if (!write_state_file(path, cached))
|
||
return false;
|
||
|
||
if (arg_bool(notify_update) && cached.status == "outdated" &&
|
||
(as_string(previous.status) != "outdated" ||
|
||
as_string(previous.latest_version) != cached.latest_version)) {
|
||
log_message("[component-update] " + component + " " + cached.latest_version, "info");
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
function update_component_check_cache_from_action(value) {
|
||
value = object_or_empty(value);
|
||
let component = normalize_component_name(value.component);
|
||
let action = as_string(value.action);
|
||
let path = component_update_check_cache_path(component);
|
||
if (path == "")
|
||
return;
|
||
|
||
if (action == "remove" && value.success === true) {
|
||
remove_file(path);
|
||
return;
|
||
}
|
||
|
||
if (!component_update_check_cache_enabled())
|
||
return;
|
||
|
||
if (action == "check_update") {
|
||
cache_component_update_check_result(value, false);
|
||
return;
|
||
}
|
||
|
||
if (value.success === true && (action == "install" || match(action, /^install_/) != null)) {
|
||
let latest_version = as_string(value.latest_version);
|
||
if (latest_version == "")
|
||
latest_version = as_string(value.current_version);
|
||
cache_component_update_check_result({
|
||
success: true,
|
||
component,
|
||
action: "check_update",
|
||
message: "Latest version is installed",
|
||
current_version: as_string(value.current_version),
|
||
latest_version,
|
||
release_url: as_string(value.release_url),
|
||
status: "latest"
|
||
}, false);
|
||
}
|
||
}
|
||
|
||
function clear_component_update_check_cache() {
|
||
for (let path in fs.glob(COMPONENT_UPDATE_CHECK_CACHE_DIR + "/*.json"))
|
||
remove_file(path);
|
||
remove_file(COMPONENT_UPDATE_CHECK_STATE_FILE);
|
||
}
|
||
|
||
function component_update_check_cache() {
|
||
let enabled = component_update_check_cache_enabled();
|
||
let results = [];
|
||
|
||
if (enabled) {
|
||
for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi" ]) {
|
||
let value = read_json_file(component_update_check_cache_path(component));
|
||
if (component_update_check_result_cacheable(value))
|
||
push(results, value);
|
||
}
|
||
}
|
||
|
||
write_json({ enabled, results });
|
||
}
|
||
|
||
function valid_component_job_id(job_id) {
|
||
job_id = as_string(job_id);
|
||
return job_id != "" && job_id != "." && job_id != ".." && match(job_id, /[^A-Za-z0-9._-]/) == null;
|
||
}
|
||
|
||
function component_job_state_path_value(job_id) {
|
||
if (!valid_component_job_id(job_id))
|
||
return "";
|
||
return COMPONENT_JOB_DIR + "/" + as_string(job_id) + ".json";
|
||
}
|
||
|
||
function component_job_id() {
|
||
let stamp = clock();
|
||
return sprintf("%d-%d", stamp[0], stamp[1]);
|
||
}
|
||
|
||
function component_job_output_path(job_id) {
|
||
return COMPONENT_JOB_DIR + "/" + as_string(job_id) + ".out";
|
||
}
|
||
|
||
function component_job_output_path_from_state(path) {
|
||
path = as_string(path);
|
||
if (length(path) >= 5 && substr(path, length(path) - 5) == ".json")
|
||
return substr(path, 0, length(path) - 5) + ".out";
|
||
return path + ".out";
|
||
}
|
||
|
||
function remove_component_job_state(path) {
|
||
let output_path = component_job_output_path_from_state(path);
|
||
remove_file(path);
|
||
remove_file(output_path);
|
||
remove_file(output_path + ".json");
|
||
}
|
||
|
||
function component_job_running_is(path, expected) {
|
||
let value = read_json_file(path);
|
||
let running = type(value) == "object" && value.running === true;
|
||
return running == arg_bool(expected);
|
||
}
|
||
|
||
function ensure_component_runtime_dirs() {
|
||
return ensure_dir(COMPONENT_JOB_DIR);
|
||
}
|
||
|
||
function write_component_stale_job_state(path) {
|
||
let value = object_or_empty(read_json_file(path));
|
||
value.success = false;
|
||
value.running = false;
|
||
value.kind = "component";
|
||
value.message = "Component action job is stale or the worker process exited unexpectedly";
|
||
value.changed = 0;
|
||
value.status = "";
|
||
value.exit_code = null;
|
||
value.updated_at = now_seconds();
|
||
return write_state_file(path, value);
|
||
}
|
||
|
||
function refresh_component_running_job_state(path) {
|
||
let value = read_json_file(path);
|
||
if (type(value) != "object" || value.running !== true)
|
||
return;
|
||
|
||
let now = now_seconds();
|
||
let within_grace = job_started_at_within_grace(value.started_at, now, COMPONENT_JOB_STALE_GRACE_SECONDS);
|
||
let pid = as_string(value.pid || "");
|
||
|
||
if (!job_pid_valid(pid)) {
|
||
if (!within_grace)
|
||
write_component_stale_job_state(path);
|
||
return;
|
||
}
|
||
|
||
if (pid_running(pid) || within_grace)
|
||
return;
|
||
|
||
command_success_from_args([ "sleep", "1" ]);
|
||
value = read_json_file(path);
|
||
if (type(value) != "object" || value.running !== true)
|
||
return;
|
||
if (pid_running(pid))
|
||
return;
|
||
|
||
write_component_stale_job_state(path);
|
||
}
|
||
|
||
function path_basename_without_suffix(path, suffix) {
|
||
let parts = split(as_string(path), "/");
|
||
let name = length(parts) > 0 ? as_string(parts[length(parts) - 1]) : "";
|
||
suffix = as_string(suffix);
|
||
if (length(name) >= length(suffix) && substr(name, length(name) - length(suffix)) == suffix)
|
||
return substr(name, 0, length(name) - length(suffix));
|
||
return name;
|
||
}
|
||
|
||
function component_cleanup_jobs() {
|
||
ensure_dir(COMPONENT_JOB_DIR);
|
||
|
||
for (let path in fs.glob(COMPONENT_JOB_DIR + "/*.json"))
|
||
refresh_component_running_job_state(path);
|
||
|
||
for (let output_path in fs.glob(COMPONENT_JOB_DIR + "/*.out")) {
|
||
let state_file = component_job_state_path_value(path_basename_without_suffix(output_path, ".out"));
|
||
if (state_file == "" || fs.stat(state_file) == null || !component_job_running_is(state_file, true)) {
|
||
remove_file(output_path);
|
||
remove_file(output_path + ".json");
|
||
}
|
||
}
|
||
|
||
command_success_from_args([
|
||
"find",
|
||
COMPONENT_JOB_DIR,
|
||
"-type", "f",
|
||
"-name", "*.out",
|
||
"-mmin", "+" + as_string(COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES),
|
||
"-delete"
|
||
]);
|
||
command_success_from_args([
|
||
"find",
|
||
COMPONENT_JOB_DIR,
|
||
"-type", "f",
|
||
"-name", "*.out.json",
|
||
"-mmin", "+" + as_string(COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES),
|
||
"-delete"
|
||
]);
|
||
command_success_from_args([
|
||
"find",
|
||
COMPONENT_JOB_DIR,
|
||
"-type", "f",
|
||
"-name", "*.json.*",
|
||
"-mmin", "+10",
|
||
"-delete"
|
||
]);
|
||
|
||
let old = command_output_from_args([
|
||
"find",
|
||
COMPONENT_JOB_DIR,
|
||
"-type", "f",
|
||
"-name", "*.json",
|
||
"-mmin", "+" + as_string(COMPONENT_JOB_FINISHED_TTL_MINUTES)
|
||
]);
|
||
|
||
for (let path in split(old, "\n")) {
|
||
path = trim(as_string(path));
|
||
if (path != "" && component_job_running_is(path, false))
|
||
remove_component_job_state(path);
|
||
}
|
||
}
|
||
|
||
function set_component_running_job_pid(path, pid) {
|
||
pid = as_string(pid);
|
||
if (!job_pid_valid(pid))
|
||
return false;
|
||
|
||
let value = object_or_empty(read_json_file(path));
|
||
if (value.running === true) {
|
||
value.pid = pid;
|
||
return write_state_file(path, value);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function output_json_object(path) {
|
||
let data = as_string(fs.readfile(path) || "");
|
||
|
||
try {
|
||
let value = json(data);
|
||
if (type(value) == "object")
|
||
return value;
|
||
}
|
||
catch (e) {
|
||
}
|
||
|
||
let result = null;
|
||
for (let line in split(data, "\n")) {
|
||
line = trim(as_string(line));
|
||
let start = index(line, "{");
|
||
if (start < 0)
|
||
continue;
|
||
try {
|
||
let value = json(substr(line, start));
|
||
if (type(value) == "object")
|
||
result = value;
|
||
}
|
||
catch (e) {
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function component_fallback_job_state(component, action, message, exit_code, updated_at) {
|
||
return {
|
||
success: false,
|
||
running: false,
|
||
kind: "component",
|
||
component: as_string(component),
|
||
action: as_string(action),
|
||
message: as_string(message),
|
||
current_version: "",
|
||
latest_version: "",
|
||
changed: 0,
|
||
status: "",
|
||
exit_code: arg_number(exit_code),
|
||
updated_at: arg_number(updated_at)
|
||
};
|
||
}
|
||
|
||
function finish_component_job(path, component, action, exit_code, output_file) {
|
||
let updated_at = now_seconds();
|
||
let value = output_json_object(output_file);
|
||
let ok;
|
||
|
||
if (type(value) == "object") {
|
||
value.running = false;
|
||
value.kind = "component";
|
||
value.exit_code = arg_number(exit_code);
|
||
value.updated_at = updated_at;
|
||
update_component_check_cache_from_action(value);
|
||
ok = write_state_file(path, value);
|
||
}
|
||
else {
|
||
let raw_output = file_last_nonblank_line_value(output_file, "Failed to execute", 240);
|
||
ok = write_state_file(path, component_fallback_job_state(component, action, raw_output, exit_code, updated_at));
|
||
}
|
||
|
||
remove_file(output_file);
|
||
remove_file(output_file + ".json");
|
||
return ok;
|
||
}
|
||
|
||
function component_worker_env() {
|
||
return {
|
||
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
||
TACHYON_LIB: LIB_DIR,
|
||
TACHYON_BIN: BIN_PATH,
|
||
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
||
UPDATES_JOB_DIR: COMPONENT_JOB_DIR,
|
||
UPDATES_JOB_FINISHED_TTL_MINUTES: COMPONENT_JOB_FINISHED_TTL_MINUTES,
|
||
UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES: COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES,
|
||
UPDATES_JOB_STALE_GRACE_SECONDS: COMPONENT_JOB_STALE_GRACE_SECONDS,
|
||
TACHYON_UI_COMPONENT_ACTION_TRACKED: "1"
|
||
};
|
||
}
|
||
|
||
function launch_component_worker(args) {
|
||
let command_args = [ "ucode", "-L", LIB_DIR, LIB_DIR + "/components/updates.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
|
||
let command = command_env(component_worker_env()) + " " +
|
||
command_from_args(command_args) +
|
||
" >/dev/null 2>&1 1000>&- & echo $!";
|
||
return trim(command_output("sh -c " + shell_quote(command)));
|
||
}
|
||
|
||
function component_action_worker(state_file, output_file, component, action) {
|
||
component = normalize_component_name(component);
|
||
let command = command_env(component_worker_env()) + " " +
|
||
command_from_args([
|
||
"ucode",
|
||
"-L", LIB_DIR,
|
||
LIB_DIR + "/components/action.uc",
|
||
"component-action",
|
||
as_string(component),
|
||
as_string(action)
|
||
]) + " >" + shell_quote(output_file) + " 2>&1";
|
||
let status = command_status(command);
|
||
|
||
finish_component_job(state_file, component, action, status, output_file);
|
||
}
|
||
|
||
function component_action_async(component, action) {
|
||
component = normalize_component_name(component);
|
||
if (!ensure_component_runtime_dirs()) {
|
||
component_job_json_response(false, "", "Failed to create component action state directory");
|
||
exit(1);
|
||
}
|
||
|
||
component_cleanup_jobs();
|
||
|
||
let job_id = component_job_id();
|
||
let state_file = component_job_state_path_value(job_id);
|
||
if (state_file == "") {
|
||
component_job_json_response(false, "", "Failed to prepare component action job");
|
||
exit(1);
|
||
}
|
||
|
||
if (!write_state_file(state_file, component_running_job_state_value(component, action, now_seconds()))) {
|
||
component_job_json_response(false, "", "Failed to write component action state");
|
||
exit(1);
|
||
}
|
||
|
||
let output_file = component_job_output_path(job_id);
|
||
let pid = launch_component_worker([
|
||
"component-action-worker",
|
||
state_file,
|
||
output_file,
|
||
as_string(component),
|
||
as_string(action)
|
||
]);
|
||
|
||
if (pid == "" || !set_component_running_job_pid(state_file, pid)) {
|
||
if (pid != "")
|
||
command_success_from_args([ "kill", pid ]);
|
||
component_job_json_response(false, "", "Failed to write component action worker pid");
|
||
exit(1);
|
||
}
|
||
|
||
component_job_json_response(true, job_id, "Component action started");
|
||
}
|
||
|
||
function component_action_status(job_id) {
|
||
ensure_dir(COMPONENT_JOB_DIR);
|
||
component_cleanup_jobs();
|
||
|
||
let state_file = component_job_state_path_value(job_id);
|
||
if (state_file == "")
|
||
component_action_status_error_exit("Invalid component action job id");
|
||
|
||
if (fs.stat(state_file) == null)
|
||
component_action_status_error_exit("Component action job was not found");
|
||
|
||
refresh_component_running_job_state(state_file);
|
||
print(as_string(fs.readfile(state_file)));
|
||
}
|
||
|
||
function automatic_component_check_names() {
|
||
let result = [ "tachyon" ];
|
||
|
||
if (fs.stat("/usr/bin/sing-box") != null)
|
||
push(result, "sing_box");
|
||
if (module_success([ LIB_DIR + "/providers/zapret/runtime.uc", "installed" ]))
|
||
push(result, "zapret");
|
||
if (module_success([ LIB_DIR + "/providers/zapret2/runtime.uc", "installed" ]))
|
||
push(result, "zapret2");
|
||
if (module_success([ LIB_DIR + "/providers/byedpi/runtime.uc", "installed" ]))
|
||
push(result, "byedpi");
|
||
|
||
return result;
|
||
}
|
||
|
||
function run_automatic_component_update_check(component) {
|
||
let output_file = temp_path();
|
||
if (output_file == "")
|
||
return false;
|
||
|
||
let command = command_env(component_worker_env()) + " " +
|
||
command_from_args([
|
||
"ucode",
|
||
"-L", LIB_DIR,
|
||
LIB_DIR + "/components/action.uc",
|
||
"component-action",
|
||
as_string(component),
|
||
"check_update"
|
||
]) + " >" + shell_quote(output_file) + " 2>&1";
|
||
let status = command_status(command);
|
||
let value = output_json_object(output_file);
|
||
remove_file(output_file);
|
||
|
||
if (status != 0 || !component_update_check_result_cacheable(value))
|
||
return false;
|
||
|
||
return cache_component_update_check_result(value, true);
|
||
}
|
||
|
||
function component_updates_if_due() {
|
||
let interval = settings_component_update_check_interval(uci_settings());
|
||
if (interval == "")
|
||
exit(0);
|
||
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
log_message("Invalid component_update_check_interval value: " + interval, "error");
|
||
exit(1);
|
||
}
|
||
|
||
let due = update_due_status(
|
||
now_seconds(),
|
||
file_first_line_value(COMPONENT_UPDATE_CHECK_STATE_FILE),
|
||
seconds
|
||
);
|
||
if (due == 1)
|
||
exit(0);
|
||
if (due != 0)
|
||
exit(1);
|
||
|
||
if (!acquire_runtime_lock(COMPONENT_UPDATE_CHECK_LOCK_DIR, false))
|
||
exit(0);
|
||
|
||
due = update_due_status(
|
||
now_seconds(),
|
||
file_first_line_value(COMPONENT_UPDATE_CHECK_STATE_FILE),
|
||
seconds
|
||
);
|
||
if (due != 0) {
|
||
release_runtime_lock(COMPONENT_UPDATE_CHECK_LOCK_DIR);
|
||
exit(due == 1 ? 0 : 1);
|
||
}
|
||
|
||
ensure_dir(COMPONENT_UPDATE_CHECK_CACHE_DIR);
|
||
for (let component in automatic_component_check_names())
|
||
run_automatic_component_update_check(component);
|
||
|
||
if (!write_file(COMPONENT_UPDATE_CHECK_STATE_FILE, as_string(now_seconds()) + "\n")) {
|
||
release_runtime_lock(COMPONENT_UPDATE_CHECK_LOCK_DIR);
|
||
log_message("Failed to write component update check timestamp", "error");
|
||
exit(1);
|
||
}
|
||
release_runtime_lock(COMPONENT_UPDATE_CHECK_LOCK_DIR);
|
||
}
|
||
|
||
function download_via_proxy_option_for_purpose(purpose) {
|
||
purpose = as_string(purpose || "lists");
|
||
if (purpose == "lists")
|
||
return "download_lists_via_proxy";
|
||
if (purpose == "components")
|
||
return "download_components_via_proxy";
|
||
return "";
|
||
}
|
||
|
||
function download_via_proxy_section_option_for_purpose(purpose) {
|
||
purpose = as_string(purpose || "lists");
|
||
if (purpose == "lists")
|
||
return "download_lists_via_proxy_section";
|
||
if (purpose == "components")
|
||
return "download_components_via_proxy_section";
|
||
return "";
|
||
}
|
||
|
||
function download_via_proxy_section(settings, purpose) {
|
||
let enabled_option = download_via_proxy_option_for_purpose(purpose);
|
||
if (enabled_option == "" || !bool_option(settings, enabled_option, false))
|
||
return "";
|
||
|
||
let section_option = download_via_proxy_section_option_for_purpose(purpose);
|
||
let configured = section_option != "" ? option(settings, section_option, "") : "";
|
||
if (configured != "")
|
||
return configured;
|
||
|
||
return option(settings, "download_lists_via_proxy_section", "");
|
||
}
|
||
|
||
function service_proxy_port_for_purpose(purpose) {
|
||
return int(SB_SERVICE_MIXED_INBOUND_PORT) + (as_string(purpose || "lists") == "components" ? 1 : 0);
|
||
}
|
||
|
||
function service_proxy_address(settings, purpose) {
|
||
return download_via_proxy_section(settings, purpose) != "" ?
|
||
SB_SERVICE_MIXED_INBOUND_ADDRESS + ":" + service_proxy_port_for_purpose(purpose) : "";
|
||
}
|
||
|
||
function download_to_file(url, filepath, proxy_address) {
|
||
let core_url_mod = core_url_module_or_null();
|
||
let candidates = core_url_mod && type(core_url_mod.download_candidates) == "function" ? core_url_mod.download_candidates(url) : [ url ];
|
||
|
||
for (let candidate in candidates) {
|
||
let attempt = 1;
|
||
while (attempt <= 2) {
|
||
let command = command_from_args([ "wget", "-q", "-T", "10", "-t", "2", "-O", filepath, candidate ]);
|
||
if (as_string(proxy_address) != "")
|
||
command = "http_proxy=" + shell_quote("http://" + as_string(proxy_address)) +
|
||
" https_proxy=" + shell_quote("http://" + as_string(proxy_address)) + " " + command;
|
||
|
||
if (command_success(command) && file_nonempty(filepath)) {
|
||
if (candidate != url)
|
||
log_message("Successfully downloaded " + as_string(url) + " via mirror " + candidate, "info");
|
||
return true;
|
||
}
|
||
|
||
attempt++;
|
||
}
|
||
}
|
||
|
||
if (file_nonempty(filepath)) {
|
||
log_message("Failed to download " + as_string(url) + " (tried direct and ghproxy mirrors); keeping existing cached file on disk", "warn");
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function convert_crlf_to_lf(path) {
|
||
let data = fs.readfile(as_string(path));
|
||
if (data == null || index(data, "\r") < 0)
|
||
return;
|
||
|
||
log_message("Converting CRLF line endings to LF in " + as_string(path), "debug");
|
||
write_file(path, replace(data, /\r/g, ""));
|
||
}
|
||
|
||
function ruleset_module_success(args) {
|
||
let command_args = [ LIB_DIR + "/routing/rulesets.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
return module_success(command_args);
|
||
}
|
||
|
||
function domain_ip_list_ruleset_path(section) {
|
||
return TMP_RULESET_FOLDER + "/" + routing_rulesets_module().ruleset_tag(section_name(section), "lists", "") + ".json";
|
||
}
|
||
|
||
function remote_ruleset_path(section, kind) {
|
||
return TMP_RULESET_FOLDER + "/" + routing_rulesets_module().ruleset_tag(section_name(section), "remote", kind) + ".json";
|
||
}
|
||
|
||
function reset_domain_ip_list_ruleset(section) {
|
||
let path = domain_ip_list_ruleset_path(section);
|
||
ensure_dir(TMP_RULESET_FOLDER);
|
||
remove_file(path);
|
||
return ruleset_module_success([ "create-source", path ]);
|
||
}
|
||
|
||
function ensure_ruleset_source(path) {
|
||
ensure_dir(TMP_RULESET_FOLDER);
|
||
if (file_exists_value(path))
|
||
return true;
|
||
return ruleset_module_success([ "create-source", path ]);
|
||
}
|
||
|
||
function cleanup_empty_ruleset(path) {
|
||
if (routing_rulesets_module().has_rules(path))
|
||
return true;
|
||
remove_file(path);
|
||
return false;
|
||
}
|
||
|
||
function add_plain_subnet_file_to_nft_for_section(section, filepath) {
|
||
if (!file_nonempty(filepath))
|
||
return true;
|
||
|
||
return nft_module_success([
|
||
"nft-add-subnet-file-for-uci-section",
|
||
section_name(section),
|
||
filepath,
|
||
NFT_TABLE_NAME,
|
||
NFT_COMMON_SET_NAME,
|
||
NFT_IP_PORT_SET_NAME,
|
||
"5000",
|
||
NFT_COMMON6_SET_NAME,
|
||
NFT_IP_PORT6_SET_NAME
|
||
]);
|
||
}
|
||
|
||
function add_json_ruleset_subnets_to_nft_for_section(section, json_file, label) {
|
||
let unscoped_tmpfile = temp_path();
|
||
let scoped_tmpfile = temp_path();
|
||
if (unscoped_tmpfile == "" || scoped_tmpfile == "") {
|
||
remove_files([ unscoped_tmpfile, scoped_tmpfile ]);
|
||
return false;
|
||
}
|
||
|
||
let ok = nft_module_success([
|
||
"nft-add-json-ruleset-subnets-for-uci-section",
|
||
section_name(section),
|
||
json_file,
|
||
label,
|
||
NFT_TABLE_NAME,
|
||
NFT_COMMON_SET_NAME,
|
||
NFT_IP_PORT_SET_NAME,
|
||
unscoped_tmpfile,
|
||
scoped_tmpfile,
|
||
"5000",
|
||
NFT_COMMON6_SET_NAME,
|
||
NFT_IP_PORT6_SET_NAME
|
||
]);
|
||
remove_files([ unscoped_tmpfile, scoped_tmpfile ]);
|
||
return ok;
|
||
}
|
||
|
||
function import_domain_ip_list_file_into_rulesets(filepath, section) {
|
||
if (!file_exists_value(filepath))
|
||
return true;
|
||
|
||
let domains_tmpfile = temp_path();
|
||
let subnets_tmpfile = temp_path();
|
||
if (domains_tmpfile == "" || subnets_tmpfile == "") {
|
||
remove_files([ domains_tmpfile, subnets_tmpfile ]);
|
||
return false;
|
||
}
|
||
|
||
let ruleset_filepath = domain_ip_list_ruleset_path(section);
|
||
let ok = nft_module_success([ "split-domain-subnet-file", filepath, domains_tmpfile, subnets_tmpfile ]);
|
||
let domains_only = option(section, "action", "") == "dns";
|
||
if (ok)
|
||
ok = ruleset_module_success([ "import-plain-list", domains_tmpfile, ruleset_filepath, "domain_suffix", "domains", "5000" ]);
|
||
if (ok && !domains_only)
|
||
ok = ruleset_module_success([ "import-plain-list", subnets_tmpfile, ruleset_filepath, "ip_cidr", "subnets", "5000" ]);
|
||
// NOTE: nft apply for domain/ip list subnets is intentionally deferred to Phase 2
|
||
// (apply_domain_ip_list_subnets_to_nft_from_rule) so it runs under RELOAD_LOCK_DIR.
|
||
|
||
remove_files([ domains_tmpfile, subnets_tmpfile ]);
|
||
return ok;
|
||
}
|
||
|
||
// Применяет суbnets из уже сохранённого ruleset-файла domain_ip_list к nft.
|
||
// Вызывается в Phase 2 list_update() — под RELOAD_LOCK_DIR.
|
||
function apply_domain_ip_list_subnets_to_nft_from_rule(section) {
|
||
if (!bool_option(section, "enabled", true))
|
||
return true;
|
||
if (option(section, "action", "") == "dns")
|
||
return true;
|
||
|
||
let ruleset_path = domain_ip_list_ruleset_path(section);
|
||
if (!file_exists_value(ruleset_path))
|
||
return true;
|
||
|
||
return add_json_ruleset_subnets_to_nft_for_section(section, ruleset_path, "domain-ip-list");
|
||
}
|
||
|
||
function import_domain_ip_list_reference_into_rulesets(reference, section, settings) {
|
||
reference = as_string(reference);
|
||
if (match(reference, /^https?:\/\//) == null)
|
||
return import_domain_ip_list_file_into_rulesets(reference, section);
|
||
|
||
let tmpfile = temp_path();
|
||
if (tmpfile == "")
|
||
return false;
|
||
|
||
let ok = true;
|
||
if (download_to_file(reference, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile)) {
|
||
convert_crlf_to_lf(tmpfile);
|
||
ok = import_domain_ip_list_file_into_rulesets(tmpfile, section);
|
||
}
|
||
else {
|
||
log_message("Failed to download remote domain/IP list " + reference + "; skipping it until the next successful update", "warn");
|
||
}
|
||
|
||
remove_file(tmpfile);
|
||
return ok;
|
||
}
|
||
|
||
function rebuild_domain_ip_lists_from_rule(section, settings) {
|
||
if (!bool_option(section, "enabled", true))
|
||
return true;
|
||
|
||
let references = list_option_values(section, "domain_ip_lists");
|
||
if (length(references) == 0)
|
||
return true;
|
||
|
||
if (!reset_domain_ip_list_ruleset(section))
|
||
return false;
|
||
|
||
let ok = true;
|
||
for (let reference in references)
|
||
if (!import_domain_ip_list_reference_into_rulesets(reference, section, settings))
|
||
ok = false;
|
||
|
||
cleanup_empty_ruleset(domain_ip_list_ruleset_path(section));
|
||
return ok;
|
||
}
|
||
|
||
function import_community_srs_file(service, settings) {
|
||
service = as_string(service);
|
||
if (!singbox_rulesets_module().is_community(service))
|
||
return true;
|
||
|
||
let url = singbox_rulesets_module().community_url(service);
|
||
let cached_file = TMP_RULESET_FOLDER + "/community-" + service + ".srs";
|
||
let persistent_file = "/etc/tachyon/rulesets/community-" + service + ".srs";
|
||
let tmpfile = temp_path();
|
||
if (tmpfile == "")
|
||
return false;
|
||
|
||
let ok = true;
|
||
if (download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile)) {
|
||
ensure_dir(TMP_RULESET_FOLDER);
|
||
copy_file(tmpfile, cached_file);
|
||
ensure_dir("/etc/tachyon/rulesets");
|
||
copy_file(tmpfile, persistent_file);
|
||
log_message("Successfully cached preset ruleset " + service, "info");
|
||
}
|
||
else if (file_exists_value(persistent_file)) {
|
||
ensure_dir(TMP_RULESET_FOLDER);
|
||
copy_file(persistent_file, cached_file);
|
||
}
|
||
else {
|
||
log_message("Failed to download preset ruleset " + service + "; skipping it until next update", "warn");
|
||
ok = false;
|
||
}
|
||
|
||
remove_file(tmpfile);
|
||
return ok;
|
||
}
|
||
|
||
function import_all_preset_lists(settings) {
|
||
log_message("Pre-downloading all preset lists and databases", "info");
|
||
let ok = true;
|
||
|
||
for (let service, urls in BUILTIN_SUBNET_URLS) {
|
||
if (type(urls) != "array")
|
||
continue;
|
||
|
||
let cached_file = TMP_RULESET_FOLDER + "/community-subnets-" + as_string(service) + ".lst";
|
||
let persistent_file = "/etc/tachyon/rulesets/community-subnets-" + as_string(service) + ".lst";
|
||
let combined_lines = [];
|
||
|
||
for (let url in urls) {
|
||
let tmpfile = temp_path();
|
||
if (tmpfile == "") {
|
||
ok = false;
|
||
continue;
|
||
}
|
||
|
||
let downloaded = download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile);
|
||
if (!downloaded) {
|
||
remove_file(tmpfile);
|
||
if (file_exists_value(persistent_file)) {
|
||
tmpfile = persistent_file;
|
||
} else {
|
||
log_message("Failed to download built-in preset subnet list " + as_string(service) + "; skipping it until next update", "warn");
|
||
continue;
|
||
}
|
||
}
|
||
|
||
let lines = split(as_string(fs.readfile(tmpfile)), "\n");
|
||
for (let l in lines) {
|
||
l = trim(replace(as_string(l), /\r/g, ""));
|
||
if (l != "" && substr(l, 0, 1) != "#")
|
||
push(combined_lines, l);
|
||
}
|
||
|
||
if (downloaded)
|
||
remove_file(tmpfile);
|
||
}
|
||
|
||
if (length(combined_lines) > 0) {
|
||
ensure_dir(TMP_RULESET_FOLDER);
|
||
write_file(cached_file, join("\n", combined_lines) + "\n");
|
||
ensure_dir("/etc/tachyon/rulesets");
|
||
write_file(persistent_file, join("\n", combined_lines) + "\n");
|
||
}
|
||
}
|
||
|
||
let community_services = keys(singbox_rulesets_module().COMMUNITY_SERVICES);
|
||
for (let service in community_services) {
|
||
if (!import_community_srs_file(service, settings))
|
||
ok = false;
|
||
}
|
||
|
||
return ok;
|
||
}
|
||
|
||
function import_builtin_subnets_from_rule(section, settings) {
|
||
if (!bool_option(section, "enabled", true))
|
||
return true;
|
||
if (option(section, "action", "") == "dns")
|
||
return true;
|
||
|
||
let ok = true;
|
||
for (let service in connections.community_lists(section)) {
|
||
if (!singbox_rulesets_module().is_community(service))
|
||
continue;
|
||
|
||
import_community_srs_file(service, settings);
|
||
|
||
let urls = BUILTIN_SUBNET_URLS[as_string(service)];
|
||
if (type(urls) != "array")
|
||
continue;
|
||
|
||
let cached_file = TMP_RULESET_FOLDER + "/community-subnets-" + as_string(service) + ".lst";
|
||
let persistent_file = "/etc/tachyon/rulesets/community-subnets-" + as_string(service) + ".lst";
|
||
let combined_lines = [];
|
||
for (let url in urls) {
|
||
let tmpfile = temp_path();
|
||
if (tmpfile == "") {
|
||
ok = false;
|
||
continue;
|
||
}
|
||
|
||
let downloaded = download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile);
|
||
if (!downloaded) {
|
||
remove_file(tmpfile);
|
||
if (file_exists_value(persistent_file)) {
|
||
tmpfile = persistent_file;
|
||
} else {
|
||
log_message("Failed to download built-in " + as_string(service) + " subnet list; skipping it until next update", "warn");
|
||
continue;
|
||
}
|
||
}
|
||
|
||
let lines = split(as_string(fs.readfile(tmpfile)), "\n");
|
||
for (let l in lines) {
|
||
l = trim(replace(as_string(l), /\r/g, ""));
|
||
if (l != "" && substr(l, 0, 1) != "#")
|
||
push(combined_lines, l);
|
||
}
|
||
|
||
nft_module_success([
|
||
"nft-add-community-subnet-file-for-uci-section",
|
||
section_name(section),
|
||
service,
|
||
tmpfile,
|
||
NFT_TABLE_NAME,
|
||
NFT_COMMON_SET_NAME,
|
||
NFT_IP_PORT_SET_NAME,
|
||
NFT_INTERFACE_SET_NAME,
|
||
NFT_DISCORD_SET_NAME,
|
||
NFT_FAKEIP_MARK,
|
||
"5000",
|
||
NFT_COMMON6_SET_NAME,
|
||
NFT_IP_PORT6_SET_NAME,
|
||
NFT_DISCORD6_SET_NAME
|
||
]);
|
||
|
||
if (downloaded)
|
||
remove_file(tmpfile);
|
||
}
|
||
|
||
if (length(combined_lines) > 0) {
|
||
ensure_dir(TMP_RULESET_FOLDER);
|
||
write_file(cached_file, join("\n", combined_lines) + "\n");
|
||
ensure_dir("/etc/tachyon/rulesets");
|
||
write_file(persistent_file, join("\n", combined_lines) + "\n");
|
||
system("/etc/init.d/tachyon reload &");
|
||
}
|
||
}
|
||
|
||
return ok;
|
||
}
|
||
|
||
function import_custom_ruleset_subnets_from_local(path, format, section, label) {
|
||
if (!file_exists_value(path)) {
|
||
log_message("Local rule set file " + as_string(path) + " not found", "error");
|
||
return false;
|
||
}
|
||
|
||
let json_tmpfile = temp_path();
|
||
if (json_tmpfile == "")
|
||
return false;
|
||
|
||
let ok = true;
|
||
if (as_string(format) == "binary") {
|
||
if (!command_success_from_args([ "sing-box", "rule-set", "decompile", path, "-o", json_tmpfile ])) {
|
||
log_message("Failed to decompile rule set " + as_string(path), "error");
|
||
ok = false;
|
||
}
|
||
}
|
||
else if (!copy_file(path, json_tmpfile)) {
|
||
log_message("Failed to copy source rule set file " + as_string(path), "error");
|
||
ok = false;
|
||
}
|
||
|
||
if (ok && !add_json_ruleset_subnets_to_nft_for_section(section, json_tmpfile, label))
|
||
ok = false;
|
||
|
||
remove_file(json_tmpfile);
|
||
return ok;
|
||
}
|
||
|
||
function import_custom_ruleset_subnets_from_remote(url, format, section, label, settings) {
|
||
let remote_tmpfile = temp_path();
|
||
let json_tmpfile = temp_path();
|
||
if (remote_tmpfile == "" || json_tmpfile == "") {
|
||
remove_files([ remote_tmpfile, json_tmpfile ]);
|
||
return false;
|
||
}
|
||
|
||
if (!download_to_file(url, remote_tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(remote_tmpfile)) {
|
||
log_message("Failed to download remote rule set " + as_string(url) + "; skipping it until the next successful update", "warn");
|
||
remove_files([ remote_tmpfile, json_tmpfile ]);
|
||
return true;
|
||
}
|
||
|
||
let ok = true;
|
||
if (as_string(format) == "binary") {
|
||
if (!command_success_from_args([ "sing-box", "rule-set", "decompile", remote_tmpfile, "-o", json_tmpfile ])) {
|
||
log_message("Failed to decompile rule set " + as_string(url), "error");
|
||
ok = false;
|
||
}
|
||
}
|
||
else if (!copy_file(remote_tmpfile, json_tmpfile)) {
|
||
log_message("Failed to copy downloaded source rule set " + as_string(url), "error");
|
||
ok = false;
|
||
}
|
||
|
||
if (ok && !add_json_ruleset_subnets_to_nft_for_section(section, json_tmpfile, label))
|
||
ok = false;
|
||
|
||
remove_files([ remote_tmpfile, json_tmpfile ]);
|
||
return ok;
|
||
}
|
||
|
||
function import_rule_sets_with_subnets_from_rule(section, settings) {
|
||
if (!bool_option(section, "enabled", true))
|
||
return true;
|
||
if (option(section, "action", "") == "dns")
|
||
return true;
|
||
|
||
let references = connections.rule_sets_with_subnets(section);
|
||
if (length(references) == 0)
|
||
return true;
|
||
|
||
log_message("Importing subnets from rule sets with subnets for '" + section_name(section) + "' section", "info");
|
||
let ok = true;
|
||
|
||
for (let reference in references) {
|
||
reference = as_string(reference);
|
||
log_message("Importing subnets from rule set reference for '" + section_name(section) + "': " + reference, "info");
|
||
|
||
let extension = singbox_rulesets_module().file_extension(reference);
|
||
if (match(reference, /^\/.*\.srs$/) != null) {
|
||
if (!import_custom_ruleset_subnets_from_local(reference, "binary", section, "Rule set " + reference))
|
||
ok = false;
|
||
}
|
||
else if (match(reference, /^\/.*\.json$/) != null) {
|
||
if (!import_custom_ruleset_subnets_from_local(reference, "source", section, "Rule set " + reference))
|
||
ok = false;
|
||
}
|
||
else if (match(reference, /^https?:\/\//) != null) {
|
||
let format = extension == "json" ? "source" : (extension == "srs" ? "binary" : singbox_rulesets_module().remote_format(reference));
|
||
if (!import_custom_ruleset_subnets_from_remote(reference, format, section, "Rule set " + reference, settings))
|
||
ok = false;
|
||
}
|
||
else {
|
||
log_message("Unsupported rule set reference for subnet import: " + reference, "error");
|
||
ok = false;
|
||
}
|
||
}
|
||
|
||
return ok;
|
||
}
|
||
|
||
function import_domains_from_remote_plain_file(url, section, settings) {
|
||
let tmpfile = temp_path();
|
||
if (tmpfile == "")
|
||
return false;
|
||
|
||
if (!download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(tmpfile)) {
|
||
log_message("Failed to download remote domain list " + as_string(url) + "; skipping it until the next successful update", "warn");
|
||
remove_file(tmpfile);
|
||
return true;
|
||
}
|
||
|
||
convert_crlf_to_lf(tmpfile);
|
||
let ruleset_path = remote_ruleset_path(section, "domains");
|
||
let ok = ensure_ruleset_source(ruleset_path) &&
|
||
ruleset_module_success([ "import-plain-list", tmpfile, ruleset_path, "domain_suffix", "domains", "5000" ]);
|
||
remove_file(tmpfile);
|
||
return ok;
|
||
}
|
||
|
||
function import_domains_from_remote_domain_lists(section, settings) {
|
||
if (!bool_option(section, "enabled", true))
|
||
return true;
|
||
|
||
let references = list_option_values(section, "remote_domain_lists");
|
||
if (length(references) == 0)
|
||
return true;
|
||
|
||
log_message("Importing domains from remote domain lists for '" + section_name(section) + "' section", "info");
|
||
let ok = true;
|
||
for (let url in references) {
|
||
log_message("Importing domains from URL: " + as_string(url), "info");
|
||
let extension = singbox_rulesets_module().file_extension(url);
|
||
log_message("Detected file extension: '" + extension + "'", "debug");
|
||
if (extension == "json" || extension == "srs") {
|
||
log_message("No update needed - sing-box manages updates automatically.", "info");
|
||
continue;
|
||
}
|
||
log_message("Import domains from a remote plain-text list", "info");
|
||
if (!import_domains_from_remote_plain_file(url, section, settings))
|
||
ok = false;
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
function import_subnets_from_remote_json_file(url, section, settings) {
|
||
let json_tmpfile = temp_path();
|
||
if (json_tmpfile == "")
|
||
return false;
|
||
|
||
if (!download_to_file(url, json_tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(json_tmpfile)) {
|
||
log_message("Failed to download remote JSON subnet list " + as_string(url) + "; skipping it until the next successful update", "warn");
|
||
remove_file(json_tmpfile);
|
||
return true;
|
||
}
|
||
|
||
let ok = add_json_ruleset_subnets_to_nft_for_section(section, json_tmpfile, "Remote JSON rule set " + as_string(url));
|
||
if (!ok)
|
||
log_message("Failed to add subnets from remote JSON list " + as_string(url) + " to nftables", "error");
|
||
remove_file(json_tmpfile);
|
||
return ok;
|
||
}
|
||
|
||
function import_subnets_from_remote_srs_file(url, section, settings) {
|
||
let binary_tmpfile = temp_path();
|
||
let json_tmpfile = temp_path();
|
||
if (binary_tmpfile == "" || json_tmpfile == "") {
|
||
remove_files([ binary_tmpfile, json_tmpfile ]);
|
||
return false;
|
||
}
|
||
|
||
if (!download_to_file(url, binary_tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(binary_tmpfile)) {
|
||
log_message("Failed to download remote SRS subnet list " + as_string(url) + "; skipping it until the next successful update", "warn");
|
||
remove_files([ binary_tmpfile, json_tmpfile ]);
|
||
return true;
|
||
}
|
||
|
||
let ok = command_success_from_args([ "sing-box", "rule-set", "decompile", binary_tmpfile, "-o", json_tmpfile ]);
|
||
if (!ok)
|
||
log_message("Failed to decompile binary rule set file", "error");
|
||
if (ok && !add_json_ruleset_subnets_to_nft_for_section(section, json_tmpfile, "Remote SRS rule set " + as_string(url))) {
|
||
log_message("Failed to add subnets from remote SRS list " + as_string(url) + " to nftables", "error");
|
||
ok = false;
|
||
}
|
||
|
||
remove_files([ binary_tmpfile, json_tmpfile ]);
|
||
return ok;
|
||
}
|
||
|
||
function import_subnets_from_remote_plain_file(url, section, settings) {
|
||
let tmpfile = temp_path();
|
||
if (tmpfile == "")
|
||
return false;
|
||
|
||
if (!download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(tmpfile)) {
|
||
log_message("Failed to download remote plain subnet list " + as_string(url) + "; skipping it until the next successful update", "warn");
|
||
remove_file(tmpfile);
|
||
return true;
|
||
}
|
||
|
||
convert_crlf_to_lf(tmpfile);
|
||
let ruleset_path = remote_ruleset_path(section, "subnets");
|
||
let ok = ensure_ruleset_source(ruleset_path) &&
|
||
ruleset_module_success([ "import-plain-list", tmpfile, ruleset_path, "ip_cidr", "subnets", "5000" ]);
|
||
if (ok)
|
||
ok = add_plain_subnet_file_to_nft_for_section(section, tmpfile);
|
||
remove_file(tmpfile);
|
||
return ok;
|
||
}
|
||
|
||
function import_subnets_from_remote_subnet_lists(section, settings) {
|
||
if (!bool_option(section, "enabled", true))
|
||
return true;
|
||
|
||
let references = list_option_values(section, "remote_subnet_lists");
|
||
if (length(references) == 0)
|
||
return true;
|
||
|
||
log_message("Importing subnets from remote subnet lists for '" + section_name(section) + "' section", "info");
|
||
let ok = true;
|
||
for (let url in references) {
|
||
log_message("Importing subnets from URL: " + as_string(url), "info");
|
||
let extension = singbox_rulesets_module().file_extension(url);
|
||
log_message("Detected file extension: '" + extension + "'", "debug");
|
||
if (extension == "json") {
|
||
log_message("Import subnets from a remote JSON list", "info");
|
||
if (!import_subnets_from_remote_json_file(url, section, settings))
|
||
ok = false;
|
||
}
|
||
else if (extension == "srs") {
|
||
log_message("Import subnets from a remote SRS list", "info");
|
||
if (!import_subnets_from_remote_srs_file(url, section, settings))
|
||
ok = false;
|
||
}
|
||
else {
|
||
log_message("Import subnets from a remote plain-text list", "info");
|
||
if (!import_subnets_from_remote_plain_file(url, section, settings))
|
||
ok = false;
|
||
}
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
function list_update_pid_begin() {
|
||
let existing_pid = trim(as_string(fs.readfile(LIST_UPDATE_PID_FILE) || ""));
|
||
let current_pid = owner_pid();
|
||
if (existing_pid != "" && existing_pid != current_pid && runtime_pid_running(existing_pid)) {
|
||
log_message("Another lists update is already running, skipping", "info");
|
||
return false;
|
||
}
|
||
|
||
ensure_parent_dir(LIST_UPDATE_PID_FILE);
|
||
write_file(LIST_UPDATE_PID_FILE, current_pid + "\n");
|
||
return true;
|
||
}
|
||
|
||
function list_update_pid_end() {
|
||
remove_file(LIST_UPDATE_PID_FILE);
|
||
}
|
||
|
||
function dns_probe_passed(proxy_address) {
|
||
if (as_string(proxy_address) != "") {
|
||
log_message("DNS check skipped because list downloads use service proxy", "info");
|
||
return true;
|
||
}
|
||
|
||
let attempt = 1;
|
||
while (attempt <= 10) {
|
||
let output = command_output_from_args([ "dig", "+short", "openwrt.org", "A", "+timeout=3", "+tries=1" ]);
|
||
for (let line in split(output, "\n")) {
|
||
if (match(as_string(line), /^[0-9]+\./) != null) {
|
||
log_message("DNS check passed", "info");
|
||
return true;
|
||
}
|
||
}
|
||
|
||
log_message("DNS is unavailable [" + attempt + "/10]", "info");
|
||
command_success_from_args([ "sleep", "3" ]);
|
||
attempt++;
|
||
}
|
||
|
||
log_message("DNS check failed after 10 attempts; skipping remote lists update until the next attempt", "info");
|
||
return false;
|
||
}
|
||
|
||
function github_probe(proxy_address) {
|
||
let attempt = 1;
|
||
let timeout = 5;
|
||
while (attempt <= 10) {
|
||
let args = [ "curl", "-s", "-m", "" + timeout ];
|
||
if (as_string(proxy_address) != "") {
|
||
push(args, "-x");
|
||
push(args, "http://" + as_string(proxy_address));
|
||
}
|
||
push(args, "https://github.com");
|
||
|
||
if (command_success_from_args(args)) {
|
||
log_message(as_string(proxy_address) != "" ? "GitHub connection check passed (via proxy)" : "GitHub connection check passed", "info");
|
||
return true;
|
||
}
|
||
|
||
log_message("GitHub is unavailable [" + attempt + "/10] (max-timeout=" + timeout + ")", "info");
|
||
if (timeout < 10)
|
||
timeout++;
|
||
command_success_from_args([ "sleep", "3" ]);
|
||
attempt++;
|
||
}
|
||
|
||
log_message("GitHub connection check failed after 10 attempts; trying configured list downloads individually", "info");
|
||
return false;
|
||
}
|
||
|
||
function write_list_update_timestamp(timestamp) {
|
||
ensure_dir(RUNTIME_STATE_DIR);
|
||
write_file(LIST_UPDATE_STATE_FILE, as_string(timestamp) + "\n");
|
||
}
|
||
|
||
function list_update() {
|
||
log_message("Starting lists update", "info");
|
||
if (!list_update_pid_begin())
|
||
exit(0);
|
||
|
||
let settings = uci_settings();
|
||
let proxy_address = service_proxy_address(settings, "lists");
|
||
if (!dns_probe_passed(proxy_address)) {
|
||
list_update_pid_end();
|
||
exit(0);
|
||
}
|
||
github_probe(proxy_address);
|
||
|
||
log_message("Downloading and processing lists", "info");
|
||
let sections = uci_sections("section");
|
||
let ok = true;
|
||
|
||
// Фаза 1: скачивание файлов (без блокировки, не трогает nft)
|
||
if (bool_option(settings, "download_all_presets", false)) {
|
||
if (!import_all_preset_lists(settings))
|
||
ok = false;
|
||
}
|
||
for (let section in sections)
|
||
if (!rebuild_domain_ip_lists_from_rule(section, settings))
|
||
ok = false;
|
||
for (let section in sections)
|
||
if (!import_domains_from_remote_domain_lists(section, settings))
|
||
ok = false;
|
||
|
||
// Фаза 2: применение к nft (захватываем лок reload, чтобы не конфликтовать
|
||
// с reload-firewall, который пересоздаёт таблицы и сеты nftables)
|
||
log_message("Waiting for reload lock to apply nft rules", "debug");
|
||
if (!acquire_runtime_lock(RELOAD_LOCK_DIR, true)) {
|
||
log_message("Could not acquire reload lock for nft apply; saving pending marker for next attempt", "warn");
|
||
try { fs.writefile("/tmp/tachyon_list_update_pending_nft", as_string(now_seconds())); } catch(e) {}
|
||
list_update_pid_end();
|
||
exit(1);
|
||
}
|
||
|
||
for (let section in sections)
|
||
if (!import_builtin_subnets_from_rule(section, settings))
|
||
ok = false;
|
||
for (let section in sections)
|
||
if (!import_subnets_from_remote_subnet_lists(section, settings))
|
||
ok = false;
|
||
for (let section in sections)
|
||
if (!import_rule_sets_with_subnets_from_rule(section, settings))
|
||
ok = false;
|
||
// Fix race condition #2: subnets из domain/ip lists тоже применяем под локом
|
||
for (let section in sections)
|
||
if (!apply_domain_ip_list_subnets_to_nft_from_rule(section))
|
||
ok = false;
|
||
|
||
release_runtime_lock(RELOAD_LOCK_DIR);
|
||
|
||
if (ok) {
|
||
write_list_update_timestamp(now_seconds());
|
||
log_message("Lists update completed successfully", "info");
|
||
}
|
||
else {
|
||
log_message("Lists update failed", "info");
|
||
}
|
||
|
||
list_update_pid_end();
|
||
exit(ok ? 0 : 1);
|
||
}
|
||
|
||
function list_update_if_due() {
|
||
let interval = settings_update_interval(uci_settings());
|
||
if (interval == "")
|
||
exit(1);
|
||
|
||
let seconds = duration_to_seconds_value(interval);
|
||
if (seconds == null) {
|
||
log_message("Invalid update_interval value: " + interval, "error");
|
||
exit(1);
|
||
}
|
||
|
||
let status = update_due_status(now_seconds(), file_first_line_value(LIST_UPDATE_STATE_FILE), seconds);
|
||
if (status == 0)
|
||
list_update();
|
||
if (status == 1)
|
||
exit(0);
|
||
|
||
exit(1);
|
||
}
|
||
|
||
function stop_list_update() {
|
||
let pid = trim(as_string(fs.readfile(LIST_UPDATE_PID_FILE) || ""));
|
||
if (pid != "" && runtime_pid_running(pid)) {
|
||
command_success_from_args([ "kill", pid ]);
|
||
log_message("Stopped list_update", "info");
|
||
}
|
||
remove_file(LIST_UPDATE_PID_FILE);
|
||
}
|
||
|
||
function subscription_cache_env() {
|
||
return {
|
||
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
||
TACHYON_LIB: LIB_DIR,
|
||
TMP_SING_BOX_FOLDER,
|
||
TMP_RULESET_FOLDER,
|
||
TMP_SUBSCRIPTION_FOLDER,
|
||
TACHYON_RUNTIME_STATE_DIR: RUNTIME_STATE_DIR,
|
||
TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR: SUBSCRIPTION_UPDATE_STATE_DIR,
|
||
TACHYON_SUBSCRIPTION_LINKS_DIR: SUBSCRIPTION_LINKS_DIR,
|
||
TACHYON_SUBSCRIPTION_METADATA_DIR: SUBSCRIPTION_METADATA_DIR,
|
||
TACHYON_OUTBOUND_METADATA_DIR: OUTBOUND_METADATA_DIR,
|
||
TACHYON_SECTION_CACHE_DIR: SECTION_CACHE_DIR,
|
||
TACHYON_RUNTIME_CACHE_FORMAT_FILE: RUNTIME_CACHE_FORMAT_FILE,
|
||
TACHYON_RUNTIME_CACHE_FORMAT: RUNTIME_CACHE_FORMAT,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR: PERSISTENT_SUBSCRIPTION_CACHE_DIR,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE,
|
||
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT: PERSISTENT_SUBSCRIPTION_CACHE_FORMAT,
|
||
TACHYON_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
|
||
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
||
SB_SERVICE_MIXED_INBOUND_ADDRESS,
|
||
SB_SERVICE_MIXED_INBOUND_PORT,
|
||
SB_VARIANT_STATE_FILE
|
||
};
|
||
}
|
||
|
||
function module_env_capture(env, args) {
|
||
let output_path = temp_path();
|
||
if (output_path == "")
|
||
return { status: 1, output: "" };
|
||
|
||
let status = command_status(command_env(env) + " " + module_command(args) + " >" + shell_quote(output_path) + " 2>&1");
|
||
let output = as_string(fs.readfile(output_path) || "");
|
||
remove_file(output_path);
|
||
return { status, output };
|
||
}
|
||
|
||
function subscription_cache_capture(args) {
|
||
let command_args = [ LIB_DIR + "/subscription/cache.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
return module_env_capture(subscription_cache_env(), command_args);
|
||
}
|
||
|
||
function subscription_cache_success(args) {
|
||
let result = subscription_cache_capture(args);
|
||
if (result.output != "")
|
||
for (let line in split(result.output, "\n"))
|
||
if (trim(as_string(line)) != "")
|
||
log_message("subscription cache: " + line, "debug");
|
||
return result.status == 0;
|
||
}
|
||
|
||
function log_file_lines_from_text(text, level, prefix) {
|
||
for (let line in split(as_string(text), "\n"))
|
||
if (trim(as_string(line)) != "")
|
||
log_message(as_string(prefix) + as_string(line), level);
|
||
}
|
||
|
||
function singbox_runtime_success(args) {
|
||
let command_args = [ LIB_DIR + "/singbox/runtime.uc" ];
|
||
for (let arg in args)
|
||
push(command_args, arg);
|
||
|
||
let result = module_env_capture(subscription_cache_env(), command_args);
|
||
if (result.output != "")
|
||
log_file_lines_from_text(result.output, "debug", "sing-box runtime: ");
|
||
return result.status == 0;
|
||
}
|
||
|
||
function write_current_reload_state_clean() {
|
||
return service_state_success([
|
||
"write-current-reload-state-clean",
|
||
RELOAD_STATE_FILE,
|
||
RELOAD_STATE_FORMAT,
|
||
RULE_CONDITION_CACHE_DIR
|
||
]);
|
||
}
|
||
|
||
function mark_pending_reload(reason) {
|
||
service_state_success([ "mark-pending-reload", PENDING_RELOAD_FILE, reason ]);
|
||
}
|
||
|
||
function run_pending_reload_if_requested() {
|
||
service_state_success([ "run-pending-reload-if-requested", PENDING_RELOAD_FILE, SERVICE_INIT ]);
|
||
}
|
||
|
||
function subscription_update_common_locked(force, target_section, target_source_index) {
|
||
let result = subscription_cache_capture([
|
||
"update-request",
|
||
force ? "1" : "0",
|
||
as_string(target_section),
|
||
as_string(target_source_index)
|
||
]);
|
||
if (result.status != 0) {
|
||
log_file_lines_from_text(result.output, "error", "subscription update: ");
|
||
return false;
|
||
}
|
||
|
||
let fields = split(trim(result.output), /[ \t\r\n]+/);
|
||
let updated = arg_number(fields[0] || "0");
|
||
let failed = arg_number(fields[1] || "0");
|
||
let unchanged = arg_number(fields[2] || "0");
|
||
let superseded = arg_number(fields[3] || "0");
|
||
|
||
if (updated == 0) {
|
||
if (superseded > 0) {
|
||
log_message("Subscription update was superseded by newer configuration", "info");
|
||
return true;
|
||
}
|
||
if (failed > 0) {
|
||
log_message("Subscription update finished with errors; keeping the last working cache", "info");
|
||
return false;
|
||
}
|
||
if (unchanged > 0)
|
||
log_message("Subscription update completed: no changes detected", "info");
|
||
else
|
||
log_message("No subscription rules are due for update", "info");
|
||
return true;
|
||
}
|
||
|
||
log_message("Reloading sing-box to apply updated subscriptions", "info");
|
||
if (!module_success([ LIB_DIR + "/server/service.uc", "prepare-all-defaults" ]))
|
||
return false;
|
||
if (!module_success([ LIB_DIR + "/config/validator.uc", "validate-runtime" ])) {
|
||
log_message("Runtime config validation failed. Aborted.", "fatal");
|
||
return false;
|
||
}
|
||
|
||
if (!singbox_runtime_success([ "configure-service" ]))
|
||
return false;
|
||
let sing_box_config_path = option(uci_settings(), "config_path", "");
|
||
let sing_box_config_hash_before = file_md5(sing_box_config_path);
|
||
let sing_box_pid_before = trim(module_output([ LIB_DIR + "/service/state.uc", "sing-box-service-runtime-pid" ]));
|
||
module_success([ DNS_FAILOVER_UC, "stop-runtime" ]);
|
||
if (!singbox_runtime_success([ "init-config", "0", "1", "1" ])) {
|
||
module_success([ DNS_FAILOVER_UC, "start-runtime" ]);
|
||
log_message("Failed to rebuild sing-box after subscription update", "error");
|
||
return false;
|
||
}
|
||
module_success([ PRIORITY_UC, "stop-runtime" ]);
|
||
if (!service_state_success([
|
||
"reload-sing-box-runtime",
|
||
sing_box_pid_before,
|
||
sing_box_config_hash_before,
|
||
file_md5(sing_box_config_path)
|
||
])) {
|
||
module_success([ PRIORITY_UC, "start-runtime" ]);
|
||
module_success([ DNS_FAILOVER_UC, "start-runtime" ]);
|
||
return false;
|
||
}
|
||
if (!module_success([ PRIORITY_UC, "start-runtime" ])) {
|
||
log_message("Failed to restart Priority runtime after subscription update", "error");
|
||
return false;
|
||
}
|
||
if (!module_success([ DNS_FAILOVER_UC, "start-runtime" ])) {
|
||
log_message("Failed to restart DNS failover runtime after subscription update", "error");
|
||
return false;
|
||
}
|
||
if (!write_current_reload_state_clean())
|
||
return false;
|
||
|
||
if (failed > 0)
|
||
log_message("Subscription update applied for changed rules; failed rules kept their previous cache", "info");
|
||
else
|
||
log_message("Subscription update completed", "info");
|
||
return true;
|
||
}
|
||
|
||
function subscription_update_common(force, target_section, target_source_index) {
|
||
if (!subscription_cache_success([ "ensure-runtime-dirs" ]))
|
||
exit(1);
|
||
|
||
force = !!force;
|
||
if (!acquire_runtime_lock(SUBSCRIPTION_UPDATE_LOCK_DIR, force)) {
|
||
log_message("Subscription update is already running", "info");
|
||
if (force)
|
||
mark_pending_reload("subscription_update_busy");
|
||
return force ? 1 : 0;
|
||
}
|
||
|
||
if (!acquire_runtime_lock(RELOAD_LOCK_DIR, force)) {
|
||
release_runtime_lock(SUBSCRIPTION_UPDATE_LOCK_DIR);
|
||
log_message("Tachyon reload is already running; skipping subscription update", "info");
|
||
if (force)
|
||
mark_pending_reload("reload_busy");
|
||
return force ? 1 : 0;
|
||
}
|
||
|
||
let ok = subscription_update_common_locked(force, target_section, target_source_index);
|
||
release_runtime_lock(RELOAD_LOCK_DIR);
|
||
release_runtime_lock(SUBSCRIPTION_UPDATE_LOCK_DIR);
|
||
run_pending_reload_if_requested();
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
function subscription_update_if_due() {
|
||
log_message("Starting due subscription update", "info");
|
||
exit(subscription_update_common(false, "", ""));
|
||
}
|
||
|
||
function subscription_update(target_section, target_source_index) {
|
||
if (as_string(target_section) != "")
|
||
log_message("Starting subscription update for rule '" + as_string(target_section) + "'", "info");
|
||
else
|
||
log_message("Starting subscription update", "info");
|
||
exit(subscription_update_common(true, target_section, target_source_index));
|
||
}
|
||
|
||
function fixture_section_list(data, type_name) {
|
||
type_name = as_string(type_name || "section");
|
||
let value = object_or_empty(data)[type_name];
|
||
if (type(value) == "array")
|
||
return value;
|
||
if (type(value) == "object")
|
||
return [ value ];
|
||
|
||
let plural = object_or_empty(data)[type_name + "s"];
|
||
return type(plural) == "array" ? plural : [];
|
||
}
|
||
|
||
function fixture_cron_refresh_plan(path, bin, list_marker, subscription_marker, component_marker) {
|
||
let data = object_or_empty(read_json_file(path));
|
||
connections.set_item_sections_from_data(data);
|
||
cron_refresh_plan(
|
||
object_or_empty(data.settings),
|
||
fixture_section_list(data),
|
||
bin,
|
||
list_marker,
|
||
subscription_marker,
|
||
component_marker
|
||
);
|
||
}
|
||
|
||
function fixture_cron_refresh_apply(path, existing_crontab_path, bin, list_marker, subscription_marker, component_marker) {
|
||
let data = object_or_empty(read_json_file(path));
|
||
connections.set_item_sections_from_data(data);
|
||
let result = cron_refresh_apply_result(
|
||
object_or_empty(data.settings),
|
||
fixture_section_list(data),
|
||
fs.readfile(as_string(existing_crontab_path)) || "",
|
||
bin,
|
||
list_marker,
|
||
subscription_marker,
|
||
component_marker
|
||
);
|
||
|
||
write_json({
|
||
crontab: result.crontab,
|
||
logs: result.logs
|
||
});
|
||
exit(result.status);
|
||
}
|
||
|
||
function fixture_section_by_name(data, target_name) {
|
||
target_name = as_string(target_name);
|
||
for (let section in fixture_section_list(data))
|
||
if (section_name(section) == target_name)
|
||
return section;
|
||
return {};
|
||
}
|
||
|
||
function uci_cron_refresh_plan(bin, list_marker, subscription_marker, component_marker) {
|
||
cron_refresh_plan(
|
||
uci_settings(),
|
||
uci_sections("section"),
|
||
bin,
|
||
list_marker,
|
||
subscription_marker,
|
||
component_marker
|
||
);
|
||
}
|
||
|
||
function uci_refresh_cron(bin, list_marker, subscription_marker, component_marker) {
|
||
let settings = uci_settings();
|
||
if (settings_component_update_check_interval(settings) == "")
|
||
clear_component_update_check_cache();
|
||
refresh_cron_from_sources(
|
||
settings,
|
||
uci_sections("section"),
|
||
bin,
|
||
list_marker,
|
||
subscription_marker,
|
||
component_marker
|
||
);
|
||
}
|
||
|
||
function uci_list_update_due_status(timestamp_path, now) {
|
||
list_update_due_status(uci_settings(), timestamp_path, now);
|
||
}
|
||
|
||
function uci_subscription_update_section_due_status(section_name, timestamp_path, now) {
|
||
subscription_update_section_due_status(object_or_empty(uci_core.get_all(CONFIG_NAME, section_name)), timestamp_path, now);
|
||
}
|
||
|
||
function fixture_list_update_due_status(path, timestamp_path, now) {
|
||
let data = object_or_empty(read_json_file(path));
|
||
connections.set_item_sections_from_data(data);
|
||
list_update_due_status(object_or_empty(data.settings), timestamp_path, now);
|
||
}
|
||
|
||
function fixture_subscription_update_section_due_status(path, section_name_value, timestamp_path, now) {
|
||
let data = object_or_empty(read_json_file(path));
|
||
connections.set_item_sections_from_data(data);
|
||
subscription_update_section_due_status(fixture_section_by_name(data, section_name_value), timestamp_path, now);
|
||
}
|
||
|
||
function print_builtin_subnet_urls(service) {
|
||
for (let url in array_or_empty(BUILTIN_SUBNET_URLS[as_string(service)]))
|
||
print(url, "\n");
|
||
}
|
||
|
||
let mode = ARGV[0] || "";
|
||
|
||
if (mode == "json-length")
|
||
json_length(ARGV[1]);
|
||
else if (mode == "update-is-due")
|
||
update_is_due(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "duration-to-seconds")
|
||
duration_to_seconds(ARGV[1]);
|
||
else if (mode == "builtin-subnet-urls")
|
||
print_builtin_subnet_urls(ARGV[1]);
|
||
else if (mode == "due-check-cron-schedule")
|
||
due_check_cron_schedule(ARGV[1]);
|
||
else if (mode == "list-update-cron-job")
|
||
update_cron_job(ARGV[1], "list_update_if_due", ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-update-cron-job")
|
||
subscription_update_cron_job(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-update-interval-plan")
|
||
subscription_update_interval_plan();
|
||
else if (mode == "cron-refresh-plan")
|
||
uci_cron_refresh_plan(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||
else if (mode == "cron-refresh-plan-fixture")
|
||
fixture_cron_refresh_plan(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
|
||
else if (mode == "refresh-cron-from-uci")
|
||
uci_refresh_cron(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||
else if (mode == "refresh-cron-fixture")
|
||
fixture_cron_refresh_apply(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6]);
|
||
else if (mode == "remove-cron-jobs")
|
||
remove_cron_jobs(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "list-update")
|
||
list_update();
|
||
else if (mode == "list-update-if-due")
|
||
list_update_if_due();
|
||
else if (mode == "stop-list-update")
|
||
stop_list_update();
|
||
else if (mode == "list-update-due-status")
|
||
uci_list_update_due_status(ARGV[1], ARGV[2]);
|
||
else if (mode == "list-update-due-status-fixture")
|
||
fixture_list_update_due_status(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-update-section-due-status")
|
||
uci_subscription_update_section_due_status(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-update-section-due-status-fixture")
|
||
fixture_subscription_update_section_due_status(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||
else if (mode == "file-first-line")
|
||
file_first_line(ARGV[1]);
|
||
else if (mode == "stdin-first-ipv4-line")
|
||
stdin_first_ipv4_line();
|
||
else if (mode == "filter-cron-markers")
|
||
filter_cron_markers([ARGV[1], ARGV[2]]);
|
||
else if (mode == "job-pid")
|
||
job_pid(ARGV[1]);
|
||
else if (mode == "subscription-job-state-path")
|
||
subscription_job_state_path(ARGV[1], ARGV[2]);
|
||
else if (mode == "subscription-job-json-response")
|
||
subscription_job_json_response(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-running-job-state")
|
||
subscription_running_job_state(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-job-refresh-plan")
|
||
subscription_job_refresh_plan(ARGV[1], ARGV[2], ARGV[3]);
|
||
else if (mode == "subscription-finished-job-state")
|
||
subscription_finished_job_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7]);
|
||
else if (mode == "subscription-stale-job-state")
|
||
subscription_stale_job_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||
else if (mode == "subscription-status-error")
|
||
subscription_status_error(ARGV[1]);
|
||
else if (mode == "subscription-cleanup-jobs")
|
||
subscription_cleanup_jobs();
|
||
else if (mode == "subscription-update-worker")
|
||
subscription_update_worker(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||
else if (mode == "subscription-update")
|
||
subscription_update(ARGV[1], ARGV[2]);
|
||
else if (mode == "subscription-update-if-due")
|
||
subscription_update_if_due();
|
||
else if (mode == "subscription-update-async")
|
||
subscription_update_async(ARGV[1], ARGV[2]);
|
||
else if (mode == "subscription-update-status")
|
||
subscription_update_status(ARGV[1]);
|
||
else if (mode == "component-action-worker")
|
||
component_action_worker(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||
else if (mode == "component-action-async")
|
||
component_action_async(ARGV[1], ARGV[2]);
|
||
else if (mode == "component-action-status")
|
||
component_action_status(ARGV[1]);
|
||
else if (mode == "component-updates-if-due")
|
||
component_updates_if_due();
|
||
else if (mode == "component-update-check-cache")
|
||
component_update_check_cache();
|
||
else {
|
||
warn("Usage: components/updates.uc <operation> ...\n");
|
||
exit(1);
|
||
} |