Files
NeoTachyon/tachyon/files/usr/lib/singbox/urltest.uc
T
Dushnilin e6e3853f05 fix(errors): перестать глотать сбои записи состояния в пустых catch
Пустых catch в кодовой базе около полусотни, и они не одинаковые. Большинство
выражает намерение: fs.unlink бросает на ENOENT, а вызывающий говорит «пусть
этого файла не будет», и отсутствующий файл это условие уже выполняет; разбор
json в цикле пропускает мусорную строку и берёт следующую. Такие места теперь
несут пояснение, почему пустое тело здесь и есть правильный ответ.

Семь мест к этому классу не относились: там сбой означал потерю состояния, а
дальше код шёл так, будто всё получилось.

Watchdog писал через пустой catch файл лока перезапуска прокси, шкалу
GOMEMLIMIT после OOM, бэкап конфига и метку keepalive. Не лёгший лок означает,
что следующий тик перезапустит прокси заново, а счётчик rate-limit об этом не
узнает; не записанная метка keepalive заставляет init.d считать watchdog мёртвым
и перезапускать его. Все четыре теперь идут через write_state_file, который
логирует отказ. Метка пишется на каждом тике uloop, поэтому безусловная строка
залила бы syslog именно при переполненной файловой системе — там, где лог нужнее
всего; вместо этого каждый путь сообщает о первом отказе и молчит до следующего
успеха, что заодно делает видимым восстановление.

Контроллер разбирал /etc/sing-box/config.json ради портов http и tproxy и при
неудаче молча брал 4534 и 4530. Watchdog после этого опрашивает не тот порт,
вечно считает прокси нерабочим и лечит его по кругу — при том, что в логе нет
ничего, что связало бы это с конфигом. Разбор вынесен в общую функцию, которая
сообщает о непарсящемся конфиге один раз на процесс.

Telegram молча терял карту callback-токенов: токен уже вшит в клавиатуру к
моменту записи, поэтому провал отдаёт пользователю кнопку, которая на нажатие
ответит «кнопка устарела». Обновление списков молча теряло маркер отложенного
применения nft — списки обновлены на диске и до nftables не доехали никогда,
а строкой выше в лог написано, что маркер сохраняется. urltest молча выкидывал
битое регулярное выражение из конфига пользователя, отчего группа тихо
подхватывала меньше outbound-ов, чем сказано в конфиге.

tests/heal_escalation.sh проверял порядок строк по литералу fs.writefile для
лока и обновлён под новый вызов.
2026-08-08 22:32:09 +03:00

175 lines
5.1 KiB
Ucode

#!/usr/bin/env ucode
let common = require("core.common");
let as_string = common.as_string;
let array_or_empty = common.array_or_empty;
let object_or_empty = common.object_or_empty;
function contains(values, needle) {
for (let value in values) {
if (value == needle)
return true;
}
return false;
}
function regex_matches(value, pattern) {
pattern = as_string(pattern);
if (pattern == "")
return false;
try {
return match(as_string(value), regexp(pattern)) != null;
}
catch (e) {
return false;
}
}
function normalized_country_list(values) {
let result = [];
for (let value in array_or_empty(values)) {
value = uc(as_string(value));
if (length(value) == 2)
push(result, value);
}
return result;
}
function byte_at(value, index) {
return ord(value, index);
}
function regional_indicator_letter(value, index) {
if (index + 3 >= length(value))
return "";
if (byte_at(value, index) != 240 ||
byte_at(value, index + 1) != 159 ||
byte_at(value, index + 2) != 135)
return "";
let letter = byte_at(value, index + 3) - 166;
if (letter < 0 || letter > 25)
return "";
return chr(65 + letter);
}
function country_from_flag_emoji(value) {
value = as_string(value);
for (let i = 0; i + 7 < length(value); i++) {
let first = regional_indicator_letter(value, i);
if (first == "")
continue;
let second = regional_indicator_letter(value, i + 4);
if (second != "")
return first + second;
}
return "";
}
function countries_from_flag_names(names) {
names = object_or_empty(names);
let result = {};
for (let tag, name in names) {
let country = country_from_flag_emoji(name);
if (country != "")
result[tag] = country;
}
return result;
}
function regex_matching_tag_array(tags, names, regexes) {
tags = array_or_empty(tags);
names = object_or_empty(names);
regexes = array_or_empty(regexes);
let compiled = [];
for (let pattern in regexes) {
let pat_str = as_string(pattern);
if (pat_str != "") {
try {
push(compiled, regexp(pat_str));
}
catch (e) {
// The pattern comes from the user's own config. Dropping it
// silently leaves a group that quietly matches fewer outbounds
// than the config says it should — a routing change with no
// visible cause. The remaining patterns still apply.
warn("urltest: ignoring invalid regex " + pat_str + ": " + as_string(e) + "\n");
}
}
}
let result = [];
for (let tag in tags) {
let name = names[tag];
name = name == null || as_string(name) == "" ? tag : as_string(name);
for (let re in compiled) {
if (match(name, re) != null) {
push(result, tag);
break;
}
}
}
return result;
}
function filter_array(mode, tags, names, countries, name_filter, regex_tags, country_filter) {
tags = array_or_empty(tags);
names = object_or_empty(names);
countries = object_or_empty(countries);
name_filter = array_or_empty(name_filter);
regex_tags = array_or_empty(regex_tags);
country_filter = array_or_empty(country_filter);
let result = [];
for (let tag in tags) {
let name = as_string(names[tag] || tag);
let country = uc(as_string(countries[tag] || ""));
let matched = contains(name_filter, name) ||
contains(name_filter, tag) ||
(country != "" && contains(country_filter, country)) ||
contains(regex_tags, tag);
if ((mode == "include" && matched) || (mode == "exclude" && !matched))
push(result, tag);
}
return result;
}
function filter_mode(mode, tags, names, countries, include_names, include_regexes, include_countries, exclude_names, exclude_regexes, exclude_countries) {
include_countries = normalized_country_list(include_countries);
exclude_countries = normalized_country_list(exclude_countries);
let include_regex_tags = regex_matching_tag_array(tags, names, include_regexes);
let exclude_regex_tags = regex_matching_tag_array(tags, names, exclude_regexes);
if (mode == "include")
return filter_array("include", tags, names, countries, include_names, include_regex_tags, include_countries);
if (mode == "exclude")
return filter_array("exclude", tags, names, countries, exclude_names, exclude_regex_tags, exclude_countries);
if (mode == "mixed") {
let included = filter_array("include", tags, names, countries, include_names, include_regex_tags, include_countries);
return filter_array("exclude", included, names, countries, exclude_names, exclude_regex_tags, exclude_countries);
}
return array_or_empty(tags);
}
return {
normalized_country_list,
countries_from_flag_names,
regex_matching_tag_array,
filter_array,
filter_mode
};