1598 lines
53 KiB
Ucode
1598 lines
53 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let common = require("core.common");
|
|
let uci_core = require("core.uci");
|
|
|
|
let as_string = common.as_string;
|
|
|
|
function read_stdin() {
|
|
let input = fs.open("/dev/stdin", "r");
|
|
if (!input)
|
|
return "";
|
|
let data = input.read("all");
|
|
input.close();
|
|
return data == null ? "" : data;
|
|
}
|
|
|
|
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 SERVICE_INIT = getenv("TACHYON_SERVICE_INIT") || "/etc/init.d/tachyon";
|
|
const SERVICE_NAME = getenv("TACHYON_SERVICE_NAME") || "tachyon";
|
|
const STATE_UC = LIB_DIR + "/service/state.uc";
|
|
const UI_UC = LIB_DIR + "/service/ui.uc";
|
|
const STATE_DIR = getenv("TACHYON_UI_STATE_DIR") || "/var/run/tachyon/ui-state";
|
|
const PENDING_RELOAD_FILE = getenv("TACHYON_PENDING_RELOAD_FILE") || "/var/run/tachyon/reload.pending";
|
|
const SERVICE_ACTION_DIR = getenv("TACHYON_UI_SERVICE_ACTION_DIR") || STATE_DIR + "/service-actions";
|
|
const SERVICE_ACTION_LOCK_DIR = getenv("TACHYON_UI_SERVICE_ACTION_LOCK_DIR") || STATE_DIR + "/service-actions.lock";
|
|
const LATENCY_ACTION_DIR = getenv("TACHYON_UI_LATENCY_ACTION_DIR") || STATE_DIR + "/latency-actions";
|
|
const COMPONENT_ACTION_DIR = getenv("TACHYON_UI_COMPONENT_ACTION_DIR") || getenv("UPDATES_JOB_DIR") || "/var/run/tachyon/component-actions";
|
|
const SUBSCRIPTION_ACTION_DIR = getenv("TACHYON_UI_SUBSCRIPTION_ACTION_DIR") || getenv("TACHYON_SUBSCRIPTION_UPDATE_JOB_DIR") || "/var/run/tachyon/subscription-update-jobs";
|
|
const ZAPRET2_TUNE_ACTION_DIR = getenv("TACHYON_UI_ZAPRET2_TUNE_ACTION_DIR") || STATE_DIR + "/zapret2-tune-actions";
|
|
const SING_BOX_VERSION_CACHE_FILE = getenv("TACHYON_UI_SING_BOX_VERSION_CACHE_FILE") || STATE_DIR + "/sing-box-version";
|
|
const SING_BOX_VERSION_CACHE_LOCK_DIR = getenv("TACHYON_UI_SING_BOX_VERSION_CACHE_LOCK_DIR") || SING_BOX_VERSION_CACHE_FILE + ".lock";
|
|
const SING_BOX_VARIANT_STATE_FILE = getenv("TACHYON_UI_SING_BOX_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
|
const SING_BOX_BIN_PATH = getenv("TACHYON_UI_SING_BOX_BIN_PATH") || "/usr/bin/sing-box";
|
|
const SING_BOX_VERSION_PROBE_TIMEOUT_SECONDS = getenv("TACHYON_UI_SING_BOX_VERSION_PROBE_TIMEOUT_SECONDS") || "1";
|
|
const SING_BOX_VERSION_PROBE_FAILURE_TTL_SECONDS = getenv("TACHYON_UI_SING_BOX_VERSION_PROBE_FAILURE_TTL_SECONDS") || "30";
|
|
const ACTION_FINISHED_TTL_MINUTES = getenv("TACHYON_UI_ACTION_FINISHED_TTL_MINUTES") || "60";
|
|
const ACTION_ACKED_TTL_SECONDS = getenv("TACHYON_UI_ACTION_ACKED_TTL_SECONDS") || "15";
|
|
const ACTION_STALE_GRACE_SECONDS = getenv("TACHYON_UI_ACTION_STALE_GRACE_SECONDS") || "15";
|
|
const SERVICE_ACTION_TIMEOUT_SECONDS = getenv("TACHYON_UI_SERVICE_ACTION_TIMEOUT_SECONDS") || "120";
|
|
const SERVICE_ACTION_SETTLE_SECONDS = getenv("TACHYON_UI_SERVICE_ACTION_SETTLE_SECONDS") || "2";
|
|
const RUNTIME_STABLE_MIN_AGE = getenv("TACHYON_RUNTIME_STABLE_MIN_AGE") || "2";
|
|
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
|
const RT_TABLE_NAME = getenv("RT_TABLE_NAME") || "tachyon";
|
|
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x04000000";
|
|
const SB_DNS_INBOUND_ADDRESS = getenv("SB_DNS_INBOUND_ADDRESS") || "127.0.0.42";
|
|
const ZAPRET_PROVIDER_NFQWS_BIN = getenv("ZAPRET_PROVIDER_NFQWS_BIN") || "/opt/zapret/nfq/nfqws";
|
|
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || "/opt/zapret2/nfq2/nfqws2";
|
|
const BYEDPI_BIN = getenv("BYEDPI_BIN") || "/usr/bin/ciadpi";
|
|
|
|
let read_json_file = common.read_json_file;
|
|
|
|
function parse_json_or_null(value) {
|
|
try {
|
|
return json(as_string(value));
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
let write_json = common.write_json;
|
|
|
|
function json_text(value) {
|
|
return sprintf("%J", value) + "\n";
|
|
}
|
|
|
|
let shell_quote = common.shell_quote;
|
|
|
|
let command_from_args = common.command_from_args;
|
|
|
|
function command_env(assignments) {
|
|
let parts = [];
|
|
|
|
for (let name, value in assignments)
|
|
push(parts, name + "=" + shell_quote(value));
|
|
|
|
return join(" ", parts);
|
|
}
|
|
|
|
let command_output = common.command_output;
|
|
let command_output_from_args = common.command_output_from_args;
|
|
|
|
let command_status = common.command_status;
|
|
|
|
function command_success(command) {
|
|
return command_status(command + " >/dev/null 2>&1") == 0;
|
|
}
|
|
|
|
function command_success_from_args(args) {
|
|
return command_success(command_from_args(args));
|
|
}
|
|
|
|
function module_success(module_path, args) {
|
|
let command_args = [ "ucode", "-L", LIB_DIR, module_path ];
|
|
for (let arg in args)
|
|
push(command_args, arg);
|
|
return command_success_from_args(command_args);
|
|
}
|
|
|
|
function now_seconds() {
|
|
return int(clock()[0]);
|
|
}
|
|
|
|
let ensure_dir = common.ensure_dir;
|
|
|
|
function ensure_dirs() {
|
|
for (let dir in [
|
|
STATE_DIR,
|
|
SERVICE_ACTION_DIR,
|
|
LATENCY_ACTION_DIR,
|
|
COMPONENT_ACTION_DIR,
|
|
SUBSCRIPTION_ACTION_DIR,
|
|
ZAPRET2_TUNE_ACTION_DIR
|
|
])
|
|
ensure_dir(dir);
|
|
}
|
|
|
|
let write_file = common.write_file;
|
|
|
|
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;
|
|
}
|
|
|
|
let remove_file = common.remove_file;
|
|
|
|
function remove_state_file(path) {
|
|
path = as_string(path);
|
|
let base = path;
|
|
let suffix = ".json";
|
|
if (length(base) >= length(suffix) && substr(base, length(base) - length(suffix)) == suffix)
|
|
base = substr(base, 0, length(base) - length(suffix));
|
|
remove_file(path);
|
|
remove_file(base + ".out");
|
|
remove_file(base + ".out.json");
|
|
}
|
|
|
|
function arg_bool(value) {
|
|
return value === true || value == "true" || value == "1" || value == 1;
|
|
}
|
|
|
|
function arg_number(value) {
|
|
value = as_string(value);
|
|
if (value == "" || match(value, /[^0-9-]/))
|
|
return 0;
|
|
return int(value);
|
|
}
|
|
|
|
function non_negative_number(value) {
|
|
let number = arg_number(value);
|
|
return number < 0 ? 0 : number;
|
|
}
|
|
|
|
function unsigned_number(value) {
|
|
value = as_string(value);
|
|
if (value == "" || match(value, /[^0-9]/) != null)
|
|
return null;
|
|
return int(value);
|
|
}
|
|
|
|
let object_or_empty = common.object_or_empty;
|
|
|
|
function path_basename(path) {
|
|
let parts = split(as_string(path), "/");
|
|
return length(parts) > 0 ? as_string(parts[length(parts) - 1]) : "";
|
|
}
|
|
|
|
function str_remove_suffix(value, suffix) {
|
|
value = as_string(value);
|
|
suffix = as_string(suffix);
|
|
if (length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix)
|
|
return substr(value, 0, length(value) - length(suffix));
|
|
return value;
|
|
}
|
|
|
|
function job_id_from_path(path) {
|
|
return str_remove_suffix(path_basename(path), ".json");
|
|
}
|
|
|
|
function valid_action_state(value) {
|
|
return type(value) == "object" && (value.running === true || value.running === false);
|
|
}
|
|
|
|
function read_state_paths() {
|
|
let result = {
|
|
service: [],
|
|
latency: [],
|
|
component: [],
|
|
subscription: []
|
|
};
|
|
|
|
for (let line in split(read_stdin(), "\n")) {
|
|
line = trim(as_string(line));
|
|
if (line == "")
|
|
continue;
|
|
|
|
let tab = index(line, "\t");
|
|
if (tab <= 0)
|
|
continue;
|
|
|
|
let kind = substr(line, 0, tab);
|
|
let path = substr(line, tab + 1);
|
|
let value = read_json_file(path);
|
|
if (!valid_action_state(value))
|
|
continue;
|
|
|
|
value.job_id = job_id_from_path(path);
|
|
|
|
if (type(result[kind]) == "array")
|
|
push(result[kind], value);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function service_status_text(running, enabled) {
|
|
if (arg_bool(running))
|
|
return arg_bool(enabled) ? "running & enabled" : "running but disabled";
|
|
|
|
return arg_bool(enabled) ? "stopped but enabled" : "stopped & disabled";
|
|
}
|
|
|
|
function print_service_status_text(running, enabled) {
|
|
print(service_status_text(running, enabled), "\n");
|
|
}
|
|
|
|
function ui_state_json() {
|
|
let action_state = read_state_paths();
|
|
let tachyon_running = arg_number(ARGV[1]);
|
|
let tachyon_enabled = arg_number(ARGV[2]);
|
|
let tachyon_status = as_string(ARGV[3]);
|
|
let tachyon_dns_configured = arg_number(ARGV[4]);
|
|
let sing_box_running = arg_number(ARGV[5]);
|
|
let sing_box_enabled = arg_number(ARGV[6]);
|
|
let sing_box_status = as_string(ARGV[7]);
|
|
|
|
if (tachyon_status == "")
|
|
tachyon_status = service_status_text(tachyon_running, tachyon_enabled);
|
|
|
|
if (sing_box_status == "")
|
|
sing_box_status = service_status_text(sing_box_running, sing_box_enabled);
|
|
|
|
write_json({
|
|
service: {
|
|
tachyon: {
|
|
running: tachyon_running,
|
|
enabled: tachyon_enabled,
|
|
status: tachyon_status,
|
|
dns_configured: tachyon_dns_configured
|
|
},
|
|
sing_box: {
|
|
running: sing_box_running,
|
|
enabled: sing_box_enabled,
|
|
status: sing_box_status
|
|
}
|
|
},
|
|
capabilities: {
|
|
sing_box_extended: arg_number(ARGV[8]),
|
|
sing_box_tiny: arg_number(ARGV[9]),
|
|
sing_box_compressed: arg_number(ARGV[10]),
|
|
sing_box_lx: arg_number(ARGV[11]),
|
|
sing_box_tailscale: arg_number(ARGV[12]),
|
|
zapret_installed: arg_number(ARGV[13]),
|
|
zapret2_installed: arg_number(ARGV[14]),
|
|
byedpi_installed: arg_number(ARGV[15]),
|
|
server_inbounds_enabled_count: arg_number(ARGV[16])
|
|
},
|
|
actions: action_state
|
|
});
|
|
}
|
|
|
|
function action_start_response(success, job_id, message) {
|
|
write_json({
|
|
success: arg_bool(success),
|
|
job_id: as_string(job_id),
|
|
message: as_string(message)
|
|
});
|
|
}
|
|
|
|
function service_action_valid(action) {
|
|
action = as_string(action);
|
|
return action == "start" || action == "stop" || action == "restart" || action == "reload";
|
|
}
|
|
|
|
function latency_type_valid(latency_type) {
|
|
latency_type = as_string(latency_type);
|
|
return latency_type == "group" || latency_type == "proxy" || latency_type == "proxy_list";
|
|
}
|
|
|
|
function service_action_expected_running(action) {
|
|
action = as_string(action);
|
|
|
|
if (action == "start" || action == "restart" || action == "reload")
|
|
print("1\n");
|
|
else if (action == "stop")
|
|
print("0\n");
|
|
else
|
|
exit(1);
|
|
}
|
|
|
|
function running_service_action(action, source, started_at) {
|
|
write_json({
|
|
success: true,
|
|
running: true,
|
|
kind: "service",
|
|
action: as_string(action),
|
|
source: as_string(source),
|
|
message: "Service action is running",
|
|
pid: null,
|
|
started_at: arg_number(started_at),
|
|
updated_at: null,
|
|
exit_code: null
|
|
});
|
|
}
|
|
|
|
function running_latency_action(latency_type, section, tag, started_at) {
|
|
write_json({
|
|
success: true,
|
|
running: true,
|
|
kind: "latency",
|
|
latency_type: as_string(latency_type),
|
|
section: as_string(section),
|
|
tag: as_string(tag),
|
|
message: "Latency test is running",
|
|
pid: null,
|
|
started_at: arg_number(started_at),
|
|
updated_at: null,
|
|
exit_code: null
|
|
});
|
|
}
|
|
|
|
function set_running_job_pid(path, pid) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
if (value.running === true)
|
|
value.pid = as_string(pid);
|
|
write_json(value);
|
|
}
|
|
|
|
function finished_action_state(path, success, message, exit_code, updated_at) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
value.success = arg_bool(success);
|
|
value.running = false;
|
|
value.message = as_string(message);
|
|
value.exit_code = as_string(exit_code) == "" ? null : arg_number(exit_code);
|
|
value.updated_at = arg_number(updated_at);
|
|
write_json(value);
|
|
}
|
|
|
|
function stale_action_state(path, message, updated_at) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
if (value.running === true) {
|
|
value.success = false;
|
|
value.running = false;
|
|
value.message = as_string(message);
|
|
value.exit_code = null;
|
|
value.updated_at = arg_number(updated_at);
|
|
}
|
|
write_json(value);
|
|
}
|
|
|
|
function ack_action_state(path, acked_at) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
if (value.running === false)
|
|
value.acked_at = arg_number(acked_at);
|
|
write_json(value);
|
|
}
|
|
|
|
function action_ack_expired(path, now_value, ttl_value) {
|
|
let value = read_json_file(path);
|
|
if (type(value) != "object")
|
|
exit(1);
|
|
|
|
let acked_at = unsigned_number(value.acked_at);
|
|
let now = unsigned_number(now_value);
|
|
let ttl = unsigned_number(ttl_value);
|
|
|
|
if (acked_at == null || now == null || now <= 0 || ttl == null)
|
|
exit(1);
|
|
|
|
exit(now - acked_at >= ttl ? 0 : 1);
|
|
}
|
|
|
|
function json_file_field(path, key, fallback) {
|
|
let value = read_json_file(path);
|
|
if (type(value) == "object" && value[key] != null)
|
|
print(as_string(value[key]), "\n");
|
|
else
|
|
print(as_string(fallback), "\n");
|
|
}
|
|
|
|
function job_state_path(dir, job_id) {
|
|
dir = as_string(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(dir, "/", job_id, ".json\n");
|
|
}
|
|
|
|
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 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 active_service_action(dir) {
|
|
dir = as_string(dir || SERVICE_ACTION_DIR);
|
|
|
|
for (let path in fs.glob(as_string(dir) + "/*.json")) {
|
|
let value = read_json_file(path);
|
|
if (type(value) == "object" && value.running === true && as_string(value.action) != "") {
|
|
print(as_string(value.action), "\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
exit(1);
|
|
}
|
|
|
|
function active_service_action_value() {
|
|
for (let path in fs.glob(SERVICE_ACTION_DIR + "/*.json")) {
|
|
let value = read_json_file(path);
|
|
if (type(value) == "object" && value.running === true && as_string(value.action) != "")
|
|
return as_string(value.action);
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function file_executable(path) {
|
|
let stat = fs.stat(as_string(path));
|
|
return stat != null && stat.mode != null && (int(stat.mode) & 73) != 0;
|
|
}
|
|
|
|
function first_line(path) {
|
|
let data = fs.readfile(as_string(path));
|
|
if (data == null)
|
|
return "";
|
|
|
|
let newline = index(data, "\n");
|
|
return trim(newline >= 0 ? substr(data, 0, newline) : data);
|
|
}
|
|
|
|
function valid_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 job_state_path_value(dir, job_id) {
|
|
if (!valid_job_id(job_id))
|
|
return "";
|
|
return as_string(dir) + "/" + as_string(job_id) + ".json";
|
|
}
|
|
|
|
function job_id() {
|
|
let stamp = clock();
|
|
return sprintf("%d-%d", stamp[0], stamp[1]);
|
|
}
|
|
|
|
function running_service_action_value(action, source, started_at) {
|
|
return {
|
|
success: true,
|
|
running: true,
|
|
kind: "service",
|
|
action: as_string(action),
|
|
source: as_string(source),
|
|
message: "Service action is running",
|
|
pid: null,
|
|
started_at: arg_number(started_at),
|
|
updated_at: null,
|
|
exit_code: null
|
|
};
|
|
}
|
|
|
|
function latency_progress_value(completed, total, failed) {
|
|
let progress_total = non_negative_number(total);
|
|
let progress_completed = non_negative_number(completed);
|
|
if (progress_total > 0 && progress_completed > progress_total)
|
|
progress_completed = progress_total;
|
|
|
|
return {
|
|
completed: progress_completed,
|
|
total: progress_total,
|
|
failed: non_negative_number(failed)
|
|
};
|
|
}
|
|
|
|
function proxy_list_total(tag) {
|
|
let value = parse_json_or_null(tag);
|
|
if (type(value) != "array")
|
|
return 0;
|
|
|
|
let total = 0;
|
|
for (let proxy_tag in value) {
|
|
if (type(proxy_tag) == "string" && proxy_tag != "")
|
|
total++;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
function initial_latency_progress(latency_type, tag) {
|
|
if (as_string(latency_type) != "proxy_list")
|
|
return null;
|
|
|
|
return latency_progress_value(0, proxy_list_total(tag), 0);
|
|
}
|
|
|
|
function running_latency_action_value(latency_type, section, tag, started_at) {
|
|
let value = {
|
|
success: true,
|
|
running: true,
|
|
kind: "latency",
|
|
latency_type: as_string(latency_type),
|
|
section: as_string(section),
|
|
tag: as_string(tag),
|
|
message: "Latency test is running",
|
|
pid: null,
|
|
started_at: arg_number(started_at),
|
|
updated_at: null,
|
|
exit_code: null
|
|
};
|
|
|
|
let progress = initial_latency_progress(latency_type, tag);
|
|
if (type(progress) == "object")
|
|
value.progress = progress;
|
|
|
|
return value;
|
|
}
|
|
|
|
function latency_action_path_allowed(path) {
|
|
path = as_string(path);
|
|
let prefix = LATENCY_ACTION_DIR + "/";
|
|
let suffix = ".json";
|
|
return path != "" &&
|
|
substr(path, 0, length(prefix)) == prefix &&
|
|
length(path) > length(prefix) + length(suffix) &&
|
|
substr(path, length(path) - length(suffix)) == suffix;
|
|
}
|
|
|
|
function update_latency_progress_state(path, completed, total, failed) {
|
|
if (!latency_action_path_allowed(path))
|
|
return false;
|
|
|
|
let value = read_json_file(path);
|
|
if (type(value) != "object" || value.kind != "latency" || value.running !== true)
|
|
return false;
|
|
|
|
value.progress = latency_progress_value(completed, total, failed);
|
|
value.updated_at = now_seconds();
|
|
return write_state_file(path, value);
|
|
}
|
|
|
|
function update_latency_progress_state_mode(path, completed, total, failed) {
|
|
exit(update_latency_progress_state(path, completed, total, failed) ? 0 : 1);
|
|
}
|
|
|
|
function finished_action_state_value(path, success, message, exit_code, updated_at) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
value.success = arg_bool(success);
|
|
value.running = false;
|
|
value.message = as_string(message);
|
|
value.exit_code = as_string(exit_code) == "" ? null : arg_number(exit_code);
|
|
value.updated_at = arg_number(updated_at);
|
|
return value;
|
|
}
|
|
|
|
function stale_action_state_value(path, message, updated_at) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
if (value.running === true) {
|
|
value.success = false;
|
|
value.running = false;
|
|
value.message = as_string(message);
|
|
value.exit_code = null;
|
|
value.updated_at = arg_number(updated_at);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function ack_action_state_value(path, acked_at) {
|
|
let value = object_or_empty(read_json_file(path));
|
|
if (value.running === false)
|
|
value.acked_at = arg_number(acked_at);
|
|
return value;
|
|
}
|
|
|
|
function set_running_job_pid_file(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 write_finished_action_state(path, success, message, exit_code) {
|
|
return write_state_file(path, finished_action_state_value(path, success, message, exit_code, now_seconds()));
|
|
}
|
|
|
|
function write_stale_action_state(path, message) {
|
|
return write_state_file(path, stale_action_state_value(path, message, now_seconds()));
|
|
}
|
|
|
|
function pid_running(pid) {
|
|
pid = as_string(pid);
|
|
return job_pid_valid(pid) && command_success_from_args([ "kill", "-0", pid ]);
|
|
}
|
|
|
|
function current_pid() {
|
|
let stat = as_string(fs.readfile("/proc/self/stat"));
|
|
let separator = index(stat, " ");
|
|
return separator > 0 ? substr(stat, 0, separator) : "";
|
|
}
|
|
|
|
function refresh_pid_job_state(path, stale_message) {
|
|
let value = read_json_file(path);
|
|
if (type(value) != "object" || value.running !== true)
|
|
return;
|
|
|
|
let now = now_seconds();
|
|
let pid = as_string(value.pid || "");
|
|
|
|
if (job_pid_valid(pid) && pid_running(pid))
|
|
return;
|
|
|
|
let within_grace = job_started_at_within_grace(value.started_at, now, int(ACTION_STALE_GRACE_SECONDS));
|
|
if (!within_grace)
|
|
write_stale_action_state(path, stale_message);
|
|
}
|
|
|
|
function state_file_ack_expired(path) {
|
|
let value = read_json_file(path);
|
|
if (type(value) != "object")
|
|
return false;
|
|
|
|
let acked_at = unsigned_number(value.acked_at);
|
|
let now = now_seconds();
|
|
let ttl = unsigned_number(ACTION_ACKED_TTL_SECONDS);
|
|
return acked_at != null && now > 0 && ttl != null && now - acked_at >= ttl;
|
|
}
|
|
|
|
function cleanup_dir(dir) {
|
|
dir = as_string(dir);
|
|
let now = now_seconds();
|
|
let ttl_minutes = unsigned_number(ACTION_FINISHED_TTL_MINUTES);
|
|
|
|
for (let path in fs.glob(dir + "/*.json")) {
|
|
let value = read_json_file(path);
|
|
if (!valid_action_state(value)) {
|
|
remove_state_file(path);
|
|
continue;
|
|
}
|
|
|
|
if (value.running !== false)
|
|
continue;
|
|
|
|
if (state_file_ack_expired(path)) {
|
|
remove_state_file(path);
|
|
continue;
|
|
}
|
|
|
|
let stat = fs.stat(path);
|
|
if (stat != null && ttl_minutes != null && now - int(stat.mtime || 0) > ttl_minutes * 60)
|
|
remove_state_file(path);
|
|
}
|
|
}
|
|
|
|
function refresh_action_dirs() {
|
|
ensure_dirs();
|
|
cleanup_dir(SERVICE_ACTION_DIR);
|
|
cleanup_dir(LATENCY_ACTION_DIR);
|
|
cleanup_dir(COMPONENT_ACTION_DIR);
|
|
cleanup_dir(SUBSCRIPTION_ACTION_DIR);
|
|
|
|
for (let path in fs.glob(SERVICE_ACTION_DIR + "/*.json"))
|
|
refresh_pid_job_state(path, "Service action worker exited unexpectedly");
|
|
for (let path in fs.glob(LATENCY_ACTION_DIR + "/*.json"))
|
|
refresh_pid_job_state(path, "Latency test worker exited unexpectedly");
|
|
for (let path in fs.glob(COMPONENT_ACTION_DIR + "/*.json"))
|
|
refresh_pid_job_state(path, "Component action worker exited unexpectedly");
|
|
for (let path in fs.glob(SUBSCRIPTION_ACTION_DIR + "/*.json"))
|
|
refresh_pid_job_state(path, "Subscription update worker exited unexpectedly");
|
|
}
|
|
|
|
function active_service_action_default() {
|
|
refresh_action_dirs();
|
|
active_service_action(SERVICE_ACTION_DIR);
|
|
}
|
|
|
|
function action_state_from_dir(dir) {
|
|
let result = [];
|
|
|
|
for (let path in fs.glob(as_string(dir) + "/*.json")) {
|
|
let value = read_json_file(path);
|
|
if (!valid_action_state(value))
|
|
continue;
|
|
|
|
value.job_id = job_id_from_path(path);
|
|
push(result, value);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function action_state_from_dirs() {
|
|
refresh_action_dirs();
|
|
return {
|
|
service: action_state_from_dir(SERVICE_ACTION_DIR),
|
|
latency: action_state_from_dir(LATENCY_ACTION_DIR),
|
|
component: action_state_from_dir(COMPONENT_ACTION_DIR),
|
|
subscription: action_state_from_dir(SUBSCRIPTION_ACTION_DIR)
|
|
};
|
|
}
|
|
|
|
function acquire_dir_lock(lock_dir) {
|
|
lock_dir = as_string(lock_dir);
|
|
let owner_pid = current_pid();
|
|
if (!job_pid_valid(owner_pid))
|
|
return false;
|
|
|
|
if (command_success_from_args([ "mkdir", lock_dir ])) {
|
|
if (write_file(lock_dir + "/pid", owner_pid + "\n"))
|
|
return true;
|
|
release_dir_lock(lock_dir);
|
|
return false;
|
|
}
|
|
|
|
let current_owner_pid = first_line(lock_dir + "/pid");
|
|
if (pid_running(current_owner_pid))
|
|
return false;
|
|
|
|
let lock_stat = fs.stat(lock_dir);
|
|
if (current_owner_pid == "" && lock_stat != null) {
|
|
let lock_age = now_seconds() - int(lock_stat.mtime || 0);
|
|
if (lock_age >= 0 && lock_age < 5)
|
|
return false;
|
|
}
|
|
|
|
remove_file(lock_dir + "/pid");
|
|
command_success_from_args([ "rmdir", lock_dir ]);
|
|
if (!command_success_from_args([ "mkdir", lock_dir ]))
|
|
return false;
|
|
|
|
if (write_file(lock_dir + "/pid", owner_pid + "\n"))
|
|
return true;
|
|
release_dir_lock(lock_dir);
|
|
return false;
|
|
}
|
|
|
|
function release_dir_lock(lock_dir) {
|
|
remove_file(as_string(lock_dir) + "/pid");
|
|
command_success_from_args([ "rmdir", lock_dir ]);
|
|
}
|
|
|
|
function service_enabled() {
|
|
return file_executable("/etc/rc.d/S99" + SERVICE_NAME);
|
|
}
|
|
|
|
function sing_box_enabled() {
|
|
return file_executable("/etc/rc.d/S99sing-box");
|
|
}
|
|
|
|
function sing_box_running() {
|
|
return module_success(LIB_DIR + "/service/state.uc", [
|
|
"sing-box-service-stable",
|
|
RUNTIME_STABLE_MIN_AGE
|
|
]);
|
|
}
|
|
|
|
function tachyon_running() {
|
|
return module_success(LIB_DIR + "/service/state.uc", [
|
|
"tachyon-stably-running",
|
|
RT_TABLE_NAME,
|
|
NFT_TABLE_NAME,
|
|
NFT_FAKEIP_MARK,
|
|
RUNTIME_STABLE_MIN_AGE
|
|
]);
|
|
}
|
|
|
|
function dns_configured() {
|
|
return index(uci_core.get("dhcp.@dnsmasq[0].server"), SB_DNS_INBOUND_ADDRESS) >= 0;
|
|
}
|
|
|
|
function marker_is(expected) {
|
|
return first_line(SING_BOX_VARIANT_STATE_FILE) == as_string(expected);
|
|
}
|
|
|
|
function tiny_package_installed() {
|
|
if (command_success_from_args([ "sh", "-c", "command -v apk" ]))
|
|
return command_success_from_args([ "apk", "info", "-e", "sing-box-tiny" ]);
|
|
|
|
let installed = command_output_from_args([ "opkg", "list-installed" ]);
|
|
for (let line in split(installed, "\n"))
|
|
if (split(trim(as_string(line)), /[ \t]+/)[0] == "sing-box-tiny")
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function component_action_running_for(component) {
|
|
component = as_string(component);
|
|
for (let path in fs.glob(COMPONENT_ACTION_DIR + "/*.json")) {
|
|
let value = read_json_file(path);
|
|
if (type(value) != "object" || value.running !== true || as_string(value.component) != component)
|
|
continue;
|
|
// A recycled pid must not keep the job "running" forever: the process
|
|
// still has to be our component-action worker.
|
|
let pid = as_string(value.pid || "");
|
|
if (!job_pid_valid(pid))
|
|
continue;
|
|
if (index(as_string(fs.readfile("/proc/" + pid + "/cmdline")), "component-action-worker") < 0)
|
|
continue;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function sing_box_signature() {
|
|
let stat = fs.stat(SING_BOX_BIN_PATH);
|
|
if (stat == null)
|
|
return "";
|
|
|
|
return join(":", [ stat.inode, stat.size, stat.mtime, stat.ctime ]);
|
|
}
|
|
|
|
function sing_box_version_info_from_output(output) {
|
|
let first = split(as_string(output), "\n")[0] || "";
|
|
let fields = split(trim(as_string(first)), /[ \t]+/);
|
|
let version = length(fields) > 0 ? fields[length(fields) - 1] : "";
|
|
if (version == "")
|
|
return null;
|
|
|
|
let tags = "";
|
|
for (let line in split(as_string(output), "\n")) {
|
|
line = as_string(line);
|
|
if (substr(line, 0, 5) == "Tags:") {
|
|
tags = trim(substr(line, 5));
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { version, tags };
|
|
}
|
|
|
|
function sing_box_cached_version_info(signature) {
|
|
let cache = read_json_file(SING_BOX_VERSION_CACHE_FILE);
|
|
if (type(cache) == "object" && as_string(cache.signature) == signature) {
|
|
if (cache.success === true && as_string(cache.version) != "")
|
|
return { hit: true, info: { version: as_string(cache.version), tags: as_string(cache.tags) } };
|
|
|
|
let checked_at = arg_number(cache.checked_at);
|
|
let failure_ttl = arg_number(SING_BOX_VERSION_PROBE_FAILURE_TTL_SECONDS);
|
|
if (cache.success === false && checked_at > 0) {
|
|
let failure_age = now_seconds() - checked_at;
|
|
if (failure_age >= 0 && failure_age < failure_ttl)
|
|
return { hit: true, info: null };
|
|
}
|
|
}
|
|
|
|
let legacy = split(as_string(fs.readfile(SING_BOX_VERSION_CACHE_FILE)), "\n");
|
|
let legacy_version = as_string(legacy[1] || "");
|
|
if (as_string(legacy[0] || "") == signature && index(legacy_version, "extended") >= 0)
|
|
return { hit: true, info: { version: legacy_version, tags: "" } };
|
|
|
|
return null;
|
|
}
|
|
|
|
function bounded_command_output_from_args(args, timeout_seconds) {
|
|
timeout_seconds = arg_number(timeout_seconds);
|
|
if (timeout_seconds <= 0)
|
|
timeout_seconds = 1;
|
|
|
|
let pid = current_pid();
|
|
if (!job_pid_valid(pid))
|
|
return "";
|
|
|
|
let output_path = SING_BOX_VERSION_CACHE_FILE + "." + pid + ".out";
|
|
remove_file(output_path);
|
|
|
|
let script = command_from_args(args) + " >" + shell_quote(output_path) + " 2>/dev/null & child=$!; " +
|
|
"(sleep " + as_string(timeout_seconds) + "; kill -KILL \"$child\" 2>/dev/null) & watchdog=$!; " +
|
|
"wait \"$child\"; rc=$?; kill \"$watchdog\" 2>/dev/null; wait \"$watchdog\" 2>/dev/null; exit \"$rc\"";
|
|
let status = command_status(command_from_args([ "sh", "-c", script ]) + " 2>/dev/null");
|
|
let output = as_string(fs.readfile(output_path));
|
|
remove_file(output_path);
|
|
return status == 0 ? output : "";
|
|
}
|
|
|
|
function sing_box_version_info() {
|
|
if (!file_executable(SING_BOX_BIN_PATH))
|
|
return null;
|
|
|
|
let signature = sing_box_signature();
|
|
if (signature == "")
|
|
return null;
|
|
|
|
let cached = sing_box_cached_version_info(signature);
|
|
if (cached != null)
|
|
return cached.info;
|
|
|
|
ensure_dir(STATE_DIR);
|
|
if (!acquire_dir_lock(SING_BOX_VERSION_CACHE_LOCK_DIR)) {
|
|
cached = sing_box_cached_version_info(signature);
|
|
return cached != null ? cached.info : null;
|
|
}
|
|
|
|
cached = sing_box_cached_version_info(signature);
|
|
if (cached != null) {
|
|
release_dir_lock(SING_BOX_VERSION_CACHE_LOCK_DIR);
|
|
return cached.info;
|
|
}
|
|
|
|
let output = bounded_command_output_from_args([ SING_BOX_BIN_PATH, "version" ], SING_BOX_VERSION_PROBE_TIMEOUT_SECONDS);
|
|
let info = sing_box_version_info_from_output(output);
|
|
write_state_file(SING_BOX_VERSION_CACHE_FILE, {
|
|
signature,
|
|
success: info != null,
|
|
version: info != null ? info.version : "",
|
|
tags: info != null ? info.tags : "",
|
|
checked_at: now_seconds()
|
|
});
|
|
release_dir_lock(SING_BOX_VERSION_CACHE_LOCK_DIR);
|
|
return info;
|
|
}
|
|
|
|
function server_inbounds_enabled_count() {
|
|
let count = 0;
|
|
for (let section in uci_core.section_objects(CONFIG_NAME, "server")) {
|
|
section = object_or_empty(section);
|
|
let enabled = section.enabled == null ? "1" : as_string(section.enabled);
|
|
if (enabled != "0")
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function capability_flags() {
|
|
let result = {
|
|
sing_box_extended: 0,
|
|
sing_box_tiny: 0,
|
|
sing_box_compressed: 0,
|
|
sing_box_lx: 0,
|
|
sing_box_tailscale: 0,
|
|
zapret_installed: file_executable(ZAPRET_PROVIDER_NFQWS_BIN) ? 1 : 0,
|
|
zapret2_installed: file_executable(ZAPRET2_PROVIDER_NFQWS2_BIN) ? 1 : 0,
|
|
byedpi_installed: file_executable(BYEDPI_BIN) ? 1 : 0,
|
|
server_inbounds_enabled_count: 0
|
|
};
|
|
|
|
if (file_executable(SING_BOX_BIN_PATH)) {
|
|
if (marker_is("lx")) {
|
|
result.sing_box_extended = 1;
|
|
result.sing_box_lx = 1;
|
|
result.sing_box_tailscale = 1;
|
|
}
|
|
else if (marker_is("extended-compressed")) {
|
|
result.sing_box_extended = 1;
|
|
result.sing_box_compressed = 1;
|
|
result.sing_box_tailscale = 1;
|
|
}
|
|
else if (marker_is("extended")) {
|
|
result.sing_box_extended = 1;
|
|
result.sing_box_tailscale = 1;
|
|
}
|
|
else if (marker_is("tiny") || tiny_package_installed()) {
|
|
result.sing_box_tiny = 1;
|
|
}
|
|
else if (component_action_running_for("sing_box")) {
|
|
result.sing_box_tailscale = 1;
|
|
}
|
|
else {
|
|
let info = sing_box_version_info();
|
|
if (info != null && index(info.version, "-lx") >= 0) {
|
|
result.sing_box_extended = 1;
|
|
result.sing_box_lx = 1;
|
|
result.sing_box_tailscale = 1;
|
|
}
|
|
else if (info != null && index(info.version, "extended") >= 0) {
|
|
result.sing_box_extended = 1;
|
|
result.sing_box_tailscale = 1;
|
|
}
|
|
else if (info != null) {
|
|
if (match(info.tags, /(^|[,: \t])with_tailscale([, \t]|$)/) != null)
|
|
result.sing_box_tailscale = 1;
|
|
if (result.sing_box_tailscale == 0)
|
|
result.sing_box_tiny = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
result.server_inbounds_enabled_count = server_inbounds_enabled_count();
|
|
return result;
|
|
}
|
|
|
|
function ui_capabilities_json() {
|
|
write_json(capability_flags());
|
|
}
|
|
|
|
function process_memory_rss_mb(process_name) {
|
|
let output = common.command_output_from_args([ "pidof", process_name ]);
|
|
let pids = split(trim(as_string(output)), /[ \t\r\n]+/);
|
|
let total_kb = 0;
|
|
for (let p in pids) {
|
|
let pid = trim(as_string(p));
|
|
if (!pid || match(pid, /^[0-9]+$/) == null)
|
|
continue;
|
|
let cmd = fs.readfile("/proc/" + pid + "/cmdline");
|
|
if (cmd && index(cmd, "check") >= 0)
|
|
continue;
|
|
let status = fs.readfile("/proc/" + pid + "/status");
|
|
if (!status)
|
|
continue;
|
|
let m = match(status, /VmRSS:[ \t]+([0-9]+)/);
|
|
if (m)
|
|
total_kb += int(m[1]);
|
|
}
|
|
return total_kb > 0 ? int(total_kb / 1024) : 0;
|
|
}
|
|
|
|
function current_ui_state_json() {
|
|
refresh_action_dirs();
|
|
|
|
let capabilities = capability_flags();
|
|
let tachyon_is_running = tachyon_running() ? 1 : 0;
|
|
let tachyon_is_enabled = service_enabled() ? 1 : 0;
|
|
let sing_box_is_running = tachyon_is_running ? 1 : (sing_box_running() ? 1 : 0);
|
|
let sing_box_is_enabled = sing_box_enabled() ? 1 : 0;
|
|
let tachyon_status = service_status_text(tachyon_is_running, tachyon_is_enabled);
|
|
let sing_box_status = service_status_text(sing_box_is_running, sing_box_is_enabled);
|
|
let active_action = active_service_action_value();
|
|
|
|
if (active_action == "start")
|
|
tachyon_status = "starting";
|
|
else if (active_action == "stop")
|
|
tachyon_status = "stopping";
|
|
else if (active_action == "restart")
|
|
tachyon_status = "restarting";
|
|
else if (active_action == "reload")
|
|
tachyon_status = "reloading";
|
|
|
|
let sing_box_rss = sing_box_is_running ? process_memory_rss_mb("sing-box") : 0;
|
|
let zapret2_rss = file_executable(ZAPRET2_PROVIDER_NFQWS2_BIN) ? process_memory_rss_mb("nfqws2") : 0;
|
|
|
|
write_json({
|
|
service: {
|
|
tachyon: {
|
|
running: tachyon_is_running,
|
|
enabled: tachyon_is_enabled,
|
|
status: tachyon_status,
|
|
dns_configured: dns_configured() ? 1 : 0
|
|
},
|
|
sing_box: {
|
|
running: sing_box_is_running,
|
|
enabled: sing_box_is_enabled,
|
|
status: sing_box_status,
|
|
memory_rss_mb: sing_box_rss
|
|
},
|
|
zapret2: {
|
|
running: zapret2_rss > 0 ? 1 : 0,
|
|
memory_rss_mb: zapret2_rss
|
|
}
|
|
},
|
|
capabilities,
|
|
actions: action_state_from_dirs()
|
|
});
|
|
}
|
|
|
|
function service_action_expected_running_value(action) {
|
|
action = as_string(action);
|
|
if (action == "start" || action == "restart" || action == "reload")
|
|
return 1;
|
|
if (action == "stop")
|
|
return 0;
|
|
return -1;
|
|
}
|
|
|
|
function service_action_reached_expected_state(action) {
|
|
let expected = service_action_expected_running_value(action);
|
|
if (expected < 0)
|
|
return false;
|
|
return expected == 1 ? tachyon_running() : !tachyon_running();
|
|
}
|
|
|
|
function service_action_wait_for_expected_state(action, timeout, settle_seconds) {
|
|
timeout = arg_number(timeout || SERVICE_ACTION_TIMEOUT_SECONDS);
|
|
settle_seconds = arg_number(settle_seconds || SERVICE_ACTION_SETTLE_SECONDS);
|
|
let deadline = now_seconds() + timeout;
|
|
let stable_seconds = 0;
|
|
|
|
while (true) {
|
|
if (service_action_reached_expected_state(action)) {
|
|
stable_seconds++;
|
|
if (stable_seconds >= settle_seconds)
|
|
return true;
|
|
}
|
|
else {
|
|
stable_seconds = 0;
|
|
}
|
|
|
|
if (now_seconds() >= deadline)
|
|
return false;
|
|
system("sleep 1");
|
|
}
|
|
}
|
|
|
|
function begin_service_action_if_idle(action, source) {
|
|
action = as_string(action);
|
|
source = as_string(source || "ui");
|
|
if (!service_action_valid(action))
|
|
return { status: 1, job_id: "" };
|
|
|
|
ensure_dirs();
|
|
if (!acquire_dir_lock(SERVICE_ACTION_LOCK_DIR))
|
|
return { status: 2, job_id: "" };
|
|
|
|
refresh_action_dirs();
|
|
if (active_service_action_value() != "") {
|
|
release_dir_lock(SERVICE_ACTION_LOCK_DIR);
|
|
return { status: 2, job_id: "" };
|
|
}
|
|
|
|
let id = job_id();
|
|
let path = job_state_path_value(SERVICE_ACTION_DIR, id);
|
|
if (path == "" || !write_state_file(path, running_service_action_value(action, source, now_seconds()))) {
|
|
release_dir_lock(SERVICE_ACTION_LOCK_DIR);
|
|
return { status: 1, job_id: "" };
|
|
}
|
|
|
|
release_dir_lock(SERVICE_ACTION_LOCK_DIR);
|
|
return { status: 0, job_id: id };
|
|
}
|
|
|
|
function begin_service_action_mode(action, source) {
|
|
let result = begin_service_action_if_idle(action, source || "ui");
|
|
if (result.status == 0)
|
|
print(result.job_id, "\n");
|
|
exit(result.status);
|
|
}
|
|
|
|
function finish_service_action(job_id_value, success, message, exit_code) {
|
|
let path = job_state_path_value(SERVICE_ACTION_DIR, job_id_value);
|
|
if (path == "" || fs.stat(path) == null)
|
|
return false;
|
|
return write_finished_action_state(path, success, message, exit_code);
|
|
}
|
|
|
|
function finish_service_action_mode(job_id_value, success, message, exit_code) {
|
|
exit(finish_service_action(job_id_value, success, message, exit_code) ? 0 : 1);
|
|
}
|
|
|
|
function launch_worker(args) {
|
|
let command_args = [ "ucode", "-L", LIB_DIR, UI_UC ];
|
|
for (let arg in args)
|
|
push(command_args, arg);
|
|
|
|
let command = command_env({
|
|
TACHYON_CONFIG_NAME: CONFIG_NAME,
|
|
TACHYON_LIB: LIB_DIR,
|
|
TACHYON_BIN: BIN_PATH,
|
|
TACHYON_SERVICE_INIT: SERVICE_INIT,
|
|
TACHYON_SERVICE_NAME: SERVICE_NAME,
|
|
TACHYON_UI_STATE_DIR: STATE_DIR,
|
|
TACHYON_UI_SERVICE_ACTION_DIR: SERVICE_ACTION_DIR,
|
|
TACHYON_UI_SERVICE_ACTION_LOCK_DIR: SERVICE_ACTION_LOCK_DIR,
|
|
TACHYON_UI_LATENCY_ACTION_DIR: LATENCY_ACTION_DIR,
|
|
TACHYON_UI_COMPONENT_ACTION_DIR: COMPONENT_ACTION_DIR,
|
|
TACHYON_UI_SUBSCRIPTION_ACTION_DIR: SUBSCRIPTION_ACTION_DIR,
|
|
TACHYON_UI_ZAPRET2_TUNE_ACTION_DIR: ZAPRET2_TUNE_ACTION_DIR,
|
|
TACHYON_UI_SING_BOX_VERSION_CACHE_FILE: SING_BOX_VERSION_CACHE_FILE,
|
|
TACHYON_UI_SING_BOX_VERSION_CACHE_LOCK_DIR: SING_BOX_VERSION_CACHE_LOCK_DIR,
|
|
TACHYON_UI_SING_BOX_VARIANT_STATE_FILE: SING_BOX_VARIANT_STATE_FILE,
|
|
TACHYON_UI_SING_BOX_BIN_PATH: SING_BOX_BIN_PATH,
|
|
TACHYON_UI_SING_BOX_VERSION_PROBE_TIMEOUT_SECONDS: SING_BOX_VERSION_PROBE_TIMEOUT_SECONDS,
|
|
TACHYON_UI_SING_BOX_VERSION_PROBE_FAILURE_TTL_SECONDS: SING_BOX_VERSION_PROBE_FAILURE_TTL_SECONDS,
|
|
TACHYON_UI_ACTION_FINISHED_TTL_MINUTES: ACTION_FINISHED_TTL_MINUTES,
|
|
TACHYON_UI_ACTION_ACKED_TTL_SECONDS: ACTION_ACKED_TTL_SECONDS,
|
|
TACHYON_UI_ACTION_STALE_GRACE_SECONDS: ACTION_STALE_GRACE_SECONDS,
|
|
TACHYON_UI_SERVICE_ACTION_TIMEOUT_SECONDS: SERVICE_ACTION_TIMEOUT_SECONDS,
|
|
TACHYON_UI_SERVICE_ACTION_SETTLE_SECONDS: SERVICE_ACTION_SETTLE_SECONDS,
|
|
TACHYON_PENDING_RELOAD_FILE: PENDING_RELOAD_FILE,
|
|
NFT_TABLE_NAME,
|
|
RT_TABLE_NAME,
|
|
NFT_FAKEIP_MARK,
|
|
SB_DNS_INBOUND_ADDRESS,
|
|
ZAPRET_PROVIDER_NFQWS_BIN,
|
|
ZAPRET2_PROVIDER_NFQWS2_BIN,
|
|
BYEDPI_BIN
|
|
}) + " " + command_from_args(command_args);
|
|
command = common.background_command_with_pid(command);
|
|
return trim(command_output("sh -c " + shell_quote(command)));
|
|
}
|
|
|
|
function start_service_action(action, source, reason) {
|
|
let begin = begin_service_action_if_idle(action, source || "ui");
|
|
if (begin.status != 0)
|
|
return { success: false, job_id: "" };
|
|
|
|
let path = job_state_path_value(SERVICE_ACTION_DIR, begin.job_id);
|
|
if (path == "")
|
|
return { success: false, job_id: begin.job_id };
|
|
|
|
let pid = launch_worker([ "service-action-worker", path, action, begin.job_id, reason || "" ]);
|
|
if (pid == "" || !set_running_job_pid_file(path, pid)) {
|
|
if (pid != "")
|
|
command_success_from_args([ "kill", pid ]);
|
|
write_finished_action_state(path, false, "Failed to write service action worker pid", 1);
|
|
return { success: false, job_id: begin.job_id };
|
|
}
|
|
|
|
return { success: true, job_id: begin.job_id };
|
|
}
|
|
|
|
function service_action_allows_pending_reload(action) {
|
|
action = as_string(action);
|
|
return action == "start" || action == "restart" || action == "reload";
|
|
}
|
|
|
|
function consume_pending_reload() {
|
|
return command_success_from_args([
|
|
"ucode",
|
|
"-L", LIB_DIR,
|
|
STATE_UC,
|
|
"consume-pending-reload",
|
|
PENDING_RELOAD_FILE
|
|
]);
|
|
}
|
|
|
|
function mark_pending_reload(reason) {
|
|
return command_success_from_args([
|
|
"ucode",
|
|
"-L", LIB_DIR,
|
|
STATE_UC,
|
|
"mark-pending-reload",
|
|
PENDING_RELOAD_FILE,
|
|
reason
|
|
]);
|
|
}
|
|
|
|
function run_pending_reload_after_service_action(action, success) {
|
|
if (!success || !service_action_allows_pending_reload(action) || fs.stat(STATE_UC) == null)
|
|
return;
|
|
|
|
if (!consume_pending_reload())
|
|
return;
|
|
|
|
command_success_from_args([ "logger", "-t", SERVICE_NAME, "[info] Applying pending Tachyon reload" ]);
|
|
let started = start_service_action("reload", "initd", "pending");
|
|
if (!started.success)
|
|
mark_pending_reload("pending");
|
|
}
|
|
|
|
function write_finished_service_action_state(path, action, success, message, exit_code) {
|
|
let written = write_finished_action_state(path, success, message, exit_code);
|
|
if (written)
|
|
run_pending_reload_after_service_action(action, success);
|
|
return written;
|
|
}
|
|
|
|
function finish_service_action_after_command(action, job_id_value, status, spawn_waiter) {
|
|
status = arg_number(status);
|
|
if (as_string(job_id_value) == "")
|
|
return 0;
|
|
|
|
let path = job_state_path_value(SERVICE_ACTION_DIR, job_id_value);
|
|
if (path == "" || fs.stat(path) == null)
|
|
return 0;
|
|
|
|
if (status != 0) {
|
|
write_finished_service_action_state(path, action, false, "Service " + as_string(action) + " failed", status);
|
|
return 0;
|
|
}
|
|
|
|
if (!service_enabled() && !tachyon_running()) {
|
|
write_finished_service_action_state(path, action, true, "Service " + as_string(action) + " completed", 0);
|
|
return 0;
|
|
}
|
|
|
|
if (spawn_waiter) {
|
|
let pid = launch_worker([ "service-action-wait-worker", path, as_string(action), as_string(job_id_value) ]);
|
|
if (pid != "")
|
|
set_running_job_pid_file(path, pid);
|
|
return 0;
|
|
}
|
|
|
|
if (service_action_wait_for_expected_state(action, SERVICE_ACTION_TIMEOUT_SECONDS, SERVICE_ACTION_SETTLE_SECONDS))
|
|
write_finished_service_action_state(path, action, true, "Service " + as_string(action) + " completed", 0);
|
|
else
|
|
write_finished_service_action_state(path, action, false, "Service " + as_string(action) + " did not reach expected state", 1);
|
|
return 0;
|
|
}
|
|
|
|
function finish_service_action_after_command_mode(action, job_id_value, status) {
|
|
finish_service_action_after_command(action, job_id_value, status, true);
|
|
}
|
|
|
|
function update_service_action_pid_mode(job_id_value, pid) {
|
|
let path = job_state_path_value(SERVICE_ACTION_DIR, job_id_value);
|
|
exit(path != "" && set_running_job_pid_file(path, pid) ? 0 : 1);
|
|
}
|
|
|
|
function service_action_worker(path, action, job_id_value, reason) {
|
|
let args = [ SERVICE_INIT, action ];
|
|
reason = as_string(reason || "");
|
|
if (reason != "")
|
|
push(args, reason);
|
|
let command = "TACHYON_UI_ACTION_TRACKED=1 " + command_from_args(args) + " >/dev/null 2>&1";
|
|
let status = command_status(command);
|
|
finish_service_action_after_command(action, job_id_value, status, false);
|
|
}
|
|
|
|
function service_action_wait_worker(path, action, job_id_value) {
|
|
if (service_action_wait_for_expected_state(action, SERVICE_ACTION_TIMEOUT_SECONDS, SERVICE_ACTION_SETTLE_SECONDS))
|
|
write_finished_service_action_state(path, action, true, "Service " + as_string(action) + " completed", 0);
|
|
else
|
|
write_finished_service_action_state(path, action, false, "Service " + as_string(action) + " did not reach expected state", 1);
|
|
}
|
|
|
|
function service_action_async(action) {
|
|
action = as_string(action);
|
|
if (!service_action_valid(action)) {
|
|
action_start_response(false, "", "Invalid service action");
|
|
exit(1);
|
|
}
|
|
|
|
let started = start_service_action(action, "ui", "");
|
|
if (!started.success && active_service_action_value() != "") {
|
|
action_start_response(false, "", "Another service action is already running");
|
|
exit(1);
|
|
}
|
|
if (!started.success) {
|
|
action_start_response(false, "", "Failed to write service action worker pid");
|
|
exit(1);
|
|
}
|
|
|
|
action_start_response(true, started.job_id, "Service " + action + " started");
|
|
}
|
|
|
|
function service_action_status(job_id_value) {
|
|
let path = job_state_path_value(SERVICE_ACTION_DIR, job_id_value);
|
|
if (path == "") {
|
|
action_start_response(false, "", "Invalid service action job id");
|
|
exit(1);
|
|
}
|
|
if (fs.stat(path) == null) {
|
|
action_start_response(false, "", "Service action job was not found");
|
|
exit(1);
|
|
}
|
|
|
|
refresh_pid_job_state(path, "Service action worker exited unexpectedly");
|
|
print(as_string(fs.readfile(path)));
|
|
}
|
|
|
|
function latency_clash_method(latency_type) {
|
|
latency_type = as_string(latency_type);
|
|
if (latency_type == "group")
|
|
return { method: "get_group_latency", timeout: "10000" };
|
|
if (latency_type == "proxy_list")
|
|
return { method: "get_proxy_latencies", timeout: "5000" };
|
|
return { method: "get_proxy_latency", timeout: "5000" };
|
|
}
|
|
|
|
function latency_worker(path, latency_type, tag, timeout) {
|
|
let method = latency_clash_method(latency_type).method;
|
|
let status = command_status(command_from_args([ BIN_PATH, "clash_api", method, tag, timeout, path ]) + " >/dev/null 2>&1");
|
|
if (status == 0)
|
|
write_finished_action_state(path, true, "Latency test completed", status);
|
|
else
|
|
write_finished_action_state(path, false, "Latency test failed", status);
|
|
}
|
|
|
|
function latency_test_async(latency_type, section, tag, requested_timeout) {
|
|
latency_type = as_string(latency_type);
|
|
tag = as_string(tag);
|
|
if (!latency_type_valid(latency_type)) {
|
|
action_start_response(false, "", "Invalid latency test type");
|
|
exit(1);
|
|
}
|
|
if (tag == "") {
|
|
action_start_response(false, "", "Latency test tag is required");
|
|
exit(1);
|
|
}
|
|
|
|
ensure_dirs();
|
|
let id = job_id();
|
|
let path = job_state_path_value(LATENCY_ACTION_DIR, id);
|
|
if (path == "" || !write_state_file(path, running_latency_action_value(latency_type, section, tag, now_seconds()))) {
|
|
action_start_response(false, "", "Failed to write latency test state");
|
|
exit(1);
|
|
}
|
|
|
|
let plan = latency_clash_method(latency_type);
|
|
let timeout = as_string(requested_timeout) != "" ? as_string(requested_timeout) : plan.timeout;
|
|
let pid = launch_worker([ "latency-worker", path, latency_type, tag, timeout ]);
|
|
if (pid == "" || !set_running_job_pid_file(path, pid)) {
|
|
if (pid != "")
|
|
command_success_from_args([ "kill", pid ]);
|
|
action_start_response(false, "", "Failed to write latency test worker pid");
|
|
exit(1);
|
|
}
|
|
|
|
action_start_response(true, id, "Latency test started");
|
|
}
|
|
|
|
function latency_test_status(job_id_value) {
|
|
let path = job_state_path_value(LATENCY_ACTION_DIR, job_id_value);
|
|
if (path == "") {
|
|
action_start_response(false, "", "Invalid latency test job id");
|
|
exit(1);
|
|
}
|
|
if (fs.stat(path) == null) {
|
|
action_start_response(false, "", "Latency test job was not found");
|
|
exit(1);
|
|
}
|
|
|
|
refresh_pid_job_state(path, "Latency test worker exited unexpectedly");
|
|
print(as_string(fs.readfile(path)));
|
|
}
|
|
|
|
|
|
|
|
function action_dir(kind) {
|
|
kind = as_string(kind);
|
|
if (kind == "service")
|
|
return SERVICE_ACTION_DIR;
|
|
if (kind == "latency")
|
|
return LATENCY_ACTION_DIR;
|
|
if (kind == "component")
|
|
return COMPONENT_ACTION_DIR;
|
|
if (kind == "subscription")
|
|
return SUBSCRIPTION_ACTION_DIR;
|
|
if (kind == "zapret2_tune")
|
|
return ZAPRET2_TUNE_ACTION_DIR;
|
|
return "";
|
|
}
|
|
|
|
function action_ack(kind, job_id_value) {
|
|
let dir = action_dir(kind);
|
|
if (dir == "") {
|
|
action_start_response(false, "", "Invalid UI action kind");
|
|
exit(1);
|
|
}
|
|
|
|
let path = job_state_path_value(dir, job_id_value);
|
|
if (path == "") {
|
|
action_start_response(false, "", "Invalid UI action job id");
|
|
exit(1);
|
|
}
|
|
|
|
if (fs.stat(path) == null) {
|
|
action_start_response(true, job_id_value, "UI action already acknowledged");
|
|
return;
|
|
}
|
|
|
|
refresh_pid_job_state(path, "UI action worker exited unexpectedly");
|
|
let value = read_json_file(path);
|
|
if (type(value) == "object" && value.running === true) {
|
|
let pid = as_string(value.pid || "");
|
|
if (!job_pid_valid(pid) || !pid_running(pid)) {
|
|
write_stale_action_state(path, "UI action worker is no longer running");
|
|
value = read_json_file(path);
|
|
}
|
|
}
|
|
|
|
if (type(value) == "object" && value.running === true) {
|
|
action_start_response(false, job_id_value, "UI action is still running");
|
|
exit(1);
|
|
}
|
|
|
|
if (!write_state_file(path, ack_action_state_value(path, now_seconds()))) {
|
|
action_start_response(false, job_id_value, "Failed to acknowledge UI action");
|
|
exit(1);
|
|
}
|
|
|
|
action_start_response(true, job_id_value, "UI action acknowledged");
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "ui-state-json")
|
|
ui_state_json();
|
|
else if (mode == "get-ui-capabilities")
|
|
ui_capabilities_json();
|
|
else if (mode == "get-ui-state")
|
|
current_ui_state_json();
|
|
else if (mode == "service-status-text")
|
|
print_service_status_text(ARGV[1], ARGV[2]);
|
|
else if (mode == "action-start-response")
|
|
action_start_response(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "service-action-valid")
|
|
exit(service_action_valid(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "latency-type-valid")
|
|
exit(latency_type_valid(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "service-action-expected-running")
|
|
service_action_expected_running(ARGV[1]);
|
|
else if (mode == "running-service-action")
|
|
running_service_action(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "running-latency-action")
|
|
running_latency_action(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
else if (mode == "set-running-job-pid")
|
|
set_running_job_pid(ARGV[1], ARGV[2]);
|
|
else if (mode == "finished-action-state")
|
|
finished_action_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
|
|
else if (mode == "stale-action-state")
|
|
stale_action_state(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "ack-action-state")
|
|
ack_action_state(ARGV[1], ARGV[2]);
|
|
else if (mode == "action-ack-expired")
|
|
action_ack_expired(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "json-file-field")
|
|
json_file_field(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "job-state-path")
|
|
job_state_path(ARGV[1], ARGV[2]);
|
|
else if (mode == "job-refresh-plan")
|
|
job_refresh_plan(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "active-service-action")
|
|
ARGV[1] == null ? active_service_action_default() : active_service_action(ARGV[1]);
|
|
else if (mode == "component-action-running-for")
|
|
exit(component_action_running_for(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "service-action-begin-if-idle")
|
|
begin_service_action_mode(ARGV[1], ARGV[2] || "ui");
|
|
else if (mode == "service-action-update-pid")
|
|
update_service_action_pid_mode(ARGV[1], ARGV[2]);
|
|
else if (mode == "service-action-finish")
|
|
finish_service_action_mode(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
else if (mode == "latency-progress-state")
|
|
update_latency_progress_state_mode(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
else if (mode == "service-action-finish-after-command")
|
|
finish_service_action_after_command_mode(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "service-action-wait-worker")
|
|
service_action_wait_worker(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "service-action-worker")
|
|
service_action_worker(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "service-action-async")
|
|
service_action_async(ARGV[1]);
|
|
else if (mode == "service-action-status")
|
|
service_action_status(ARGV[1]);
|
|
else if (mode == "latency-worker")
|
|
latency_worker(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
else if (mode == "latency-test-async")
|
|
latency_test_async(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
else if (mode == "latency-test-status")
|
|
latency_test_status(ARGV[1]);
|
|
else if (mode == "action-ack")
|
|
action_ack(ARGV[1], ARGV[2]);
|
|
else if (mode == "cleanup-action-dir-fixture")
|
|
cleanup_dir(ARGV[1]);
|
|
else {
|
|
warn("Usage: service/ui.uc <operation> ...\n");
|
|
exit(1);
|
|
} |