Files
NeoTachyon/tachyon/files/usr/lib/singbox/priority.uc
T

471 lines
15 KiB
Ucode

#!/usr/bin/env ucode
let fs = require("fs");
let common = require("core.common");
let as_string = common.as_string;
let read_json_file = common.read_json_file;
let write_json = common.write_json;
let array_or_empty = common.array_or_empty;
let object_or_empty = common.object_or_empty;
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
const RUNTIME_STATE_DIR = getenv("TACHYON_RUNTIME_STATE_DIR") || "/var/run/tachyon";
const SECTION_CACHE_DIR = getenv("TACHYON_SECTION_CACHE_DIR") || RUNTIME_STATE_DIR + "/section-cache";
const PRIORITY_PID_FILE = getenv("TACHYON_PRIORITY_PID_FILE") || RUNTIME_STATE_DIR + "/priority.pid";
const PRIORITY_UC = getenv("TACHYON_PRIORITY_UC") || LIB_DIR + "/singbox/priority.uc";
const DIAGNOSTICS_UC = getenv("TACHYON_DIAGNOSTICS_UC") || LIB_DIR + "/diagnostics/runtime.uc";
let shell_quote = common.shell_quote;
let command_status = common.command_status;
let command_success_from_args = common.command_success_from_args;
let command_output_from_args = common.command_output_from_args;
let command_from_args = common.command_from_args;
let command_output = common.command_output;
function ensure_dir(path) {
return command_success_from_args([ "mkdir", "-p", path ]);
}
function remove_file(path) {
try {
fs.unlink(as_string(path));
}
catch (e) {
}
}
function file_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 log_message(message, level) {
level = as_string(level || "info");
command_success_from_args([ "logger", "-t", "tachyon", "[" + level + "] priority: " + as_string(message) ]);
}
function now_seconds() {
return int(clock()[0]);
}
function duration_to_milliseconds(value, fallback_ms) {
let rest = as_string(value);
if (rest == "")
return fallback_ms;
let total = 0.0;
let multipliers = {
ns: 0.000001,
us: 0.001,
ms: 1,
s: 1000,
m: 60000,
h: 3600000,
d: 86400000
};
while (rest != "") {
let matched = match(rest, /^([0-9]+(\.[0-9]+)?)(ns|us|ms|s|m|h|d)/);
if (!matched)
return fallback_ms;
let token = as_string(matched[0]);
total += (matched[1] * 1) * multipliers[matched[3]];
rest = substr(rest, length(token));
}
return total <= 0 ? fallback_ms : int(total + 0.5);
}
function duration_to_seconds(value, fallback_seconds) {
let ms = duration_to_milliseconds(value, fallback_seconds * 1000);
let seconds = int((ms + 999) / 1000);
return seconds > 0 ? seconds : fallback_seconds;
}
function bool_value(value, fallback) {
if (value == null || value == "")
return !!fallback;
value = lc(as_string(value));
return value == "1" || value == "true" || value == "yes" || value == "on";
}
function normalize_group(group, tag_name) {
group = object_or_empty(group);
let levels = [];
for (let level in array_or_empty(group.levels)) {
let outbounds = [];
for (let outbound in array_or_empty(level.outbounds)) {
outbound = as_string(outbound);
if (outbound != "")
push(outbounds, outbound);
}
if (length(outbounds) > 0) {
push(levels, {
id: as_string(level.id || ""),
displayName: as_string(level.displayName || level.name || ""),
order: int(level.order || 0),
outbounds
});
}
}
return {
id: as_string(group.id || ""),
tag: as_string(group.tag || tag_name),
section: as_string(group.section || ""),
displayName: as_string(group.displayName || group.name || tag_name),
health_url: as_string(group.health_url || "https://www.gstatic.com/generate_204"),
active_check_interval: as_string(group.active_check_interval || "5s"),
check_timeout: as_string(group.check_timeout || "2s"),
recovery_check_interval: as_string(group.recovery_check_interval || "15s"),
pick_fastest: bool_value(group.pick_fastest, false),
switch_to_faster_same_priority: bool_value(group.switch_to_faster_same_priority, false),
fastest_check_interval: as_string(group.fastest_check_interval || "3m"),
levels
};
}
function section_cache_files() {
let result = [];
for (let path in split(command_output_from_args([
"find",
SECTION_CACHE_DIR,
"-mindepth",
"1",
"-maxdepth",
"1",
"-type",
"f",
"-name",
"*.json"
]), "\n")) {
path = as_string(path);
if (path != "")
push(result, path);
}
return result;
}
function priority_groups_from_cache() {
let result = [];
for (let path in section_cache_files()) {
let cache = object_or_empty(read_json_file(path));
for (let tag_name, group in object_or_empty(cache.priorityGroups)) {
let normalized = normalize_group(group, tag_name);
if (normalized.tag != "" && length(normalized.levels) > 0)
push(result, normalized);
}
}
return result;
}
function module_capture(args) {
let output_path = trim(command_output_from_args([ "mktemp" ]));
if (output_path == "")
return { status: 1, output: "" };
let command = command_from_args([ "ucode", "-L", LIB_DIR, DIAGNOSTICS_UC, "clash-api" ]);
for (let arg in args)
command += " " + shell_quote(arg);
let status = command_status(command + " >" + shell_quote(output_path) + " 2>&1");
let output = as_string(fs.readfile(output_path) || "");
remove_file(output_path);
return { status, output };
}
function parse_delay_output(output) {
let value = null;
try {
value = json(output);
}
catch (e) {
return null;
}
if (type(value) != "object")
return null;
let delay = value.delay;
if (delay == null || as_string(delay) == "")
return null;
delay = int(delay, 10);
return delay >= 0 ? delay : null;
}
function clash_probe(tag_name, group) {
let timeout = as_string(duration_to_milliseconds(group.check_timeout, 2000));
let result = module_capture([ "get_proxy_latency", tag_name, timeout, group.health_url ]);
if (result.status != 0)
return { alive: false, delay: 0 };
let delay = parse_delay_output(result.output);
if (delay == null)
return { alive: false, delay: 0 };
return { alive: true, delay };
}
function fixture_probe(latencies, tag_name) {
let value = object_or_empty(latencies)[tag_name];
if (value == null || as_string(value) == "" || int(value, 10) < 0)
return { alive: false, delay: 0 };
return { alive: true, delay: int(value, 10) };
}
function choose_from_level(group, level_index, probe, skip_tag) {
let level = object_or_empty(array_or_empty(group.levels)[level_index]);
let best = null;
for (let tag_name in array_or_empty(level.outbounds)) {
if (as_string(skip_tag) != "" && tag_name == skip_tag)
continue;
let result = probe(tag_name, group);
if (!result.alive)
continue;
if (!group.pick_fastest)
return {
tag: tag_name,
levelIndex: level_index,
delay: result.delay
};
if (best == null || result.delay < best.delay)
best = {
tag: tag_name,
levelIndex: level_index,
delay: result.delay
};
}
return best;
}
function choose_from_level_range(group, start_index, end_index, probe, skip_tag) {
let levels = array_or_empty(group.levels);
if (length(levels) == 0)
return null;
start_index = int(start_index || 0);
end_index = int(end_index || 0);
if (start_index < 0)
start_index = 0;
if (end_index >= length(levels))
end_index = length(levels) - 1;
if (start_index > end_index)
return null;
for (let i = start_index; i <= end_index; i++) {
let selected = choose_from_level(group, i, probe, skip_tag);
if (selected != null)
return selected;
}
return null;
}
function choose_fastest_same_level(group, level_index, active_tag, probe) {
let level = object_or_empty(array_or_empty(group.levels)[level_index]);
let active = null;
let best = null;
for (let tag_name in array_or_empty(level.outbounds)) {
let result = probe(tag_name, group);
if (tag_name == active_tag)
active = result;
if (!result.alive)
continue;
if (best == null || result.delay < best.delay) {
best = {
tag: tag_name,
levelIndex: level_index,
delay: result.delay
};
}
}
if (best == null || best.tag == active_tag)
return null;
if (active == null || !active.alive || best.delay < active.delay)
return best;
return null;
}
function set_group_proxy(group, tag_name) {
let result = module_capture([ "set_group_proxy", group.tag, tag_name, "" ]);
return result.status == 0;
}
function switch_group(state, group, selected) {
if (selected == null || selected.tag == "")
return false;
if (state.active == selected.tag) {
state.levelIndex = selected.levelIndex;
state.activeDelay = selected.delay;
return true;
}
if (!set_group_proxy(group, selected.tag)) {
log_message("failed to switch " + group.tag + " to " + selected.tag, "warn");
return false;
}
state.active = selected.tag;
state.levelIndex = selected.levelIndex;
state.activeDelay = selected.delay;
return true;
}
function init_group_state(group) {
return {
active: "",
levelIndex: -1,
activeDelay: 0,
nextActiveCheck: now_seconds(),
nextRecoveryCheck: now_seconds() + duration_to_seconds(group.recovery_check_interval, 15),
nextFastestCheck: now_seconds() + duration_to_seconds(group.fastest_check_interval, 180)
};
}
function tick_group(state, group) {
let now = now_seconds();
if (state.active == "" && now >= state.nextActiveCheck) {
let selected = choose_from_level_range(group, 0, length(group.levels) - 1, clash_probe);
switch_group(state, group, selected);
state.nextActiveCheck = now + duration_to_seconds(group.active_check_interval, 5);
return;
}
if (state.active != "" && now >= state.nextActiveCheck) {
let active = clash_probe(state.active, group);
if (active.alive) {
state.activeDelay = active.delay;
}
else {
let selected = choose_from_level_range(
group,
state.levelIndex,
length(group.levels) - 1,
clash_probe,
state.active
);
if (switch_group(state, group, selected)) {
state.nextRecoveryCheck = now + duration_to_seconds(group.recovery_check_interval, 15);
state.nextFastestCheck = now + duration_to_seconds(group.fastest_check_interval, 180);
}
else {
state.active = "";
state.levelIndex = -1;
}
}
state.nextActiveCheck = now + duration_to_seconds(group.active_check_interval, 5);
}
if (state.active != "" && state.levelIndex > 0 && now >= state.nextRecoveryCheck) {
let selected = choose_from_level_range(group, 0, state.levelIndex - 1, clash_probe);
if (switch_group(state, group, selected))
state.nextFastestCheck = now + duration_to_seconds(group.fastest_check_interval, 180);
state.nextRecoveryCheck = now + duration_to_seconds(group.recovery_check_interval, 15);
}
if (state.active != "" && group.switch_to_faster_same_priority && now >= state.nextFastestCheck) {
let selected = choose_fastest_same_level(group, state.levelIndex, state.active, clash_probe);
if (selected != null)
switch_group(state, group, selected);
state.nextFastestCheck = now + duration_to_seconds(group.fastest_check_interval, 180);
}
}
function worker() {
let groups = priority_groups_from_cache();
if (length(groups) == 0)
return 0;
let states = {};
for (let group in groups)
states[group.tag] = init_group_state(group);
while (true) {
for (let group in groups)
tick_group(states[group.tag], group);
system("sleep 1");
}
}
function process_running(pid) {
pid = trim(as_string(pid));
return pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ]);
}
function stop_runtime() {
let pid = file_first_line(PRIORITY_PID_FILE);
if (process_running(pid))
command_success_from_args([ "kill", pid ]);
remove_file(PRIORITY_PID_FILE);
return 0;
}
function start_runtime() {
let groups = priority_groups_from_cache();
stop_runtime();
if (length(groups) == 0)
return 0;
if (!ensure_dir(RUNTIME_STATE_DIR))
return 1;
let command = command_from_args([ "ucode", "-L", LIB_DIR, PRIORITY_UC, "worker" ]) +
" >/dev/null 2>&1 1000>&- & echo $! >" + shell_quote(PRIORITY_PID_FILE);
return command_status(command);
}
function select_fixture(group_path, latency_path, start_index, end_index, skip_tag) {
let group = normalize_group(read_json_file(group_path), "fixture");
let latencies = object_or_empty(read_json_file(latency_path));
let selected = choose_from_level_range(group, start_index, end_index, function(tag_name, _group) {
return fixture_probe(latencies, tag_name);
}, skip_tag);
write_json(selected == null ? {} : selected);
}
function select_faster_fixture(group_path, latency_path, level_index, active_tag) {
let group = normalize_group(read_json_file(group_path), "fixture");
let latencies = object_or_empty(read_json_file(latency_path));
let selected = choose_fastest_same_level(group, int(level_index || 0), as_string(active_tag), function(tag_name, _group) {
return fixture_probe(latencies, tag_name);
});
write_json(selected == null ? {} : selected);
}
let mode = ARGV[0] || "";
if (mode == "start-runtime")
exit(start_runtime());
else if (mode == "stop-runtime")
exit(stop_runtime());
else if (mode == "worker")
exit(worker());
else if (mode == "select-fixture")
select_fixture(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]);
else if (mode == "select-faster-fixture")
select_faster_fixture(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
else {
warn("Usage: singbox/priority.uc <start-runtime|stop-runtime|worker|select-fixture|select-faster-fixture>\n");
exit(1);
}