feat(updates): 1.3.10 - Автоматическое обновление компонентов (сразу при выходе и по расписанию)

- Backend: реализован запуск автоматического обновления компонентов в фоновом режиме (trigger_component_auto_update, component_auto_update_apply)
- Cron: поддержка окна обслуживания (schedule mode) с регистрацией задачи в crontab по заданному времени (HH:MM)
- UCI: новые опции component_auto_update_enabled, component_auto_update_mode, component_auto_update_time, component_auto_update_targets
- Web UI: элементы управления во вкладке настроек «Обновления» (чекбокс включения, переключатель режима, время и мультиселект разрешенных компонентов)
- Тесты: добавлен интеграционный тест component_auto_update.sh, обновлены cron-тесты updates_due.sh, все 118 тестов успешно пройдены в Docker
- Локализация: добавлены русские переводы для всех новых элементов интерфейса
This commit is contained in:
Dushnilin
2026-09-01 10:58:46 +03:00
parent f553352e25
commit 84cdf9d2f8
16 changed files with 1317 additions and 536 deletions
+16
View File
@@ -1233,6 +1233,22 @@ const dictionary = {
"Proxy route to Telegram API works": "Маршрут через прокси к Telegram API работает",
"sing-box is running": "sing-box запущен",
"Test message sent to chat": "Тестовое сообщение отправлено в чат",
// Component Auto-Update
"Automatic component updates": "Автоматическое обновление компонентов",
"Automatically download and install new versions of components": "Автоматически загружать и устанавливать новые версии компонентов",
"Auto-update mode": "Режим автообновления",
"Choose whether to install updates immediately when detected or at a scheduled time (e.g. maintenance window)": "Выберите, устанавливать ли обновления сразу при обнаружении или по расписанию (например, в ночное окно обслуживания)",
"Immediately upon release": "Сразу при выходе новой версии",
"By schedule": "По расписанию",
"Scheduled auto-update time": "Время обновления по расписанию",
"Time of day to run scheduled component updates in HH:MM (24h) format, e.g. 04:00": "Время суток для запуска планового обновления компонентов в формате ЧЧ:ММ (24ч), например 04:00",
"Enter a valid time in HH:MM format (00:00 to 23:59)": "Введите корректное время в формате ЧЧ:ММ (от 00:00 до 23:59)",
"Eligible components for auto-update": "Разрешенные компоненты для автообновления",
"Select which components are permitted to update automatically": "Выберите, какие компоненты разрешено обновлять автоматически",
"All components": "Все компоненты",
"Tachyon Core": "Ядро Tachyon",
"LuCI Web UI": "Веб-интерфейс LuCI",
};
function escapePoString(str) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+56 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-01 08:41+0300\n"
"PO-Revision-Date: 2026-09-01 08:41+0300\n"
"POT-Creation-Date: 2026-09-01 10:48+0300\n"
"PO-Revision-Date: 2026-09-01 10:48+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -314,6 +314,9 @@ msgstr "Все"
msgid "All checks passed — Telegram bot should work correctly"
msgstr "Все проверки пройдены — бот должен работать корректно"
msgid "All components"
msgstr "Все компоненты"
msgid "All devices"
msgstr "Все устройства"
@@ -494,9 +497,15 @@ msgstr "Автолечение сетевого стека"
msgid "Auto-update Hosts Lists"
msgstr "Автообновление списков Hosts"
msgid "Auto-update mode"
msgstr "Режим автообновления"
msgid "Automatic component update checks"
msgstr "Автоматическая проверка обновлений компонентов"
msgid "Automatic component updates"
msgstr "Автоматическое обновление компонентов"
msgid "Automatic User-Agent selection"
msgstr "Автоматический выбор User-Agent"
@@ -506,6 +515,9 @@ msgstr "Автоматически добавлять текст к имени
msgid "Automatically check installed components for new versions"
msgstr "Автоматически проверять установленные компоненты на наличие новых версий"
msgid "Automatically download and install new versions of components"
msgstr "Автоматически загружать и устанавливать новые версии компонентов"
msgid "Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings."
msgstr "Автоматически создать аккаунт Cloudflare WARP и настроить все параметры AmneziaWG."
@@ -626,6 +638,12 @@ msgstr "Встроенные наборы правил"
msgid "By flag emoji from name"
msgstr "По эмодзи флага из имени"
msgid "By schedule"
msgstr "По расписанию"
msgid "ByeDPI"
msgstr ""
msgid "ByeDPI (ciadpi)"
msgstr "ByeDPI (ciadpi)"
@@ -776,6 +794,9 @@ msgstr "Выберите визуальную иконку для профиля
msgid "Choose Hosts File..."
msgstr "Выбрать файл Hosts..."
msgid "Choose whether to install updates immediately when detected or at a scheduled time (e.g. maintenance window)"
msgstr "Выберите, устанавливать ли обновления сразу при обнаружении или по расписанию (например, в ночное окно обслуживания)"
msgid "Choose whether to restrict the entire internet or only specific Tachyon routing sections."
msgstr "Выберите, блокировать ли весь доступ в интернет или только определенные секции Tachyon."
@@ -1337,6 +1358,9 @@ msgstr "Каждая запись — \"proto\", \"proto:port\" или \"proto:p
msgid "Edit"
msgstr "Изменить"
msgid "Eligible components for auto-update"
msgstr "Разрешенные компоненты для автообновления"
msgid "Enable"
msgstr "Включить"
@@ -1454,6 +1478,9 @@ msgstr "Введите домен или IP для проверки (напр. e
msgid "Enter a level name"
msgstr "Введите имя уровня"
msgid "Enter a valid time in HH:MM format (00:00 to 23:59)"
msgstr "Введите корректное время в формате ЧЧ:ММ (от 00:00 до 23:59)"
msgid "Enter a whole number of minutes between 0 and 1440"
msgstr "Введите целое число минут от 0 до 1440"
@@ -1961,6 +1988,9 @@ msgstr "Таймаут простоя"
msgid "If one health check fails with an error, continue running other checks instead of stopping."
msgstr "Если одна проверка завершается ошибкой, продолжать другие проверки, а не останавливаться."
msgid "Immediately upon release"
msgstr "Сразу при выходе новой версии"
msgid "Import"
msgstr "Импорт"
@@ -2495,6 +2525,9 @@ msgstr "Локальный адрес"
msgid "Log Level"
msgstr "Уровень логирования"
msgid "LuCI Web UI"
msgstr "Веб-интерфейс LuCI"
msgid "lx"
msgstr "lx"
@@ -3506,6 +3539,9 @@ msgstr "Расписание"
msgid "Schedule Rule"
msgstr "Правило расписания"
msgid "Scheduled auto-update time"
msgstr "Время обновления по расписанию"
msgid "Score"
msgstr "Баллы"
@@ -3635,6 +3671,9 @@ msgstr "Выберите WAN-интерфейсы для отслеживани
msgid "Select whether to disable the target during the schedule or enable it ONLY during the schedule."
msgstr "Выберите, отключать ли цель в период расписания или включать ТОЛЬКО в период расписания."
msgid "Select which components are permitted to update automatically"
msgstr "Выберите, какие компоненты разрешено обновлять автоматически"
msgid "Selected"
msgstr "Выбран"
@@ -3800,6 +3839,9 @@ msgstr "Не удалось показать конфигурацию sing-box"
msgid "Show subscription metadata for this source on the dashboard"
msgstr "Показывать метаданные подписки для этого источника на дашборде"
msgid "sing-box"
msgstr ""
msgid "Sing-box autostart disabled"
msgstr "Автостарт sing-box отключен"
@@ -4070,6 +4112,9 @@ msgstr "Системная информация"
msgid "Table exist"
msgstr "Таблица существует"
msgid "Tachyon Core"
msgstr "Ядро Tachyon"
msgid "Tachyon Error"
msgstr "Ошибка Tachyon"
@@ -4295,6 +4340,9 @@ msgstr "Время кэширования DNS-записей в секундах
msgid "Time is required (e.g. 22:00)"
msgstr "Укажите время (например, 22:00)"
msgid "Time of day to run scheduled component updates in HH:MM (24h) format, e.g. 04:00"
msgstr "Время суток для запуска планового обновления компонентов в формате ЧЧ:ММ (24ч), например 04:00"
msgid "Time skew tolerance"
msgstr "Допуск отклонения времени"
@@ -4775,6 +4823,9 @@ msgstr "Да"
msgid "You can select Output Network Interface, by default autodetect"
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
msgid "Zapret"
msgstr ""
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
@@ -4802,6 +4853,9 @@ msgstr "Zapret v1 (nfqws)"
msgid "Zapret v2 (nfqws2)"
msgstr "Zapret v2 (nfqws2)"
msgid "Zapret2"
msgstr ""
msgid "Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies."
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "fe-app-tachyon",
"version": "1.3.9",
"version": "1.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fe-app-tachyon",
"version": "1.3.9",
"version": "1.3.10",
"license": "MIT",
"devDependencies": {
"@babel/parser": "7.28.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "fe-app-tachyon",
"version": "1.3.9",
"version": "1.3.10",
"license": "MIT",
"type": "module",
"scripts": {
@@ -1437,6 +1437,75 @@ function createSettingsContent(section, capabilities) {
return _("Use sing-box duration format like 1d, 12h or 30m");
};
o = section.taboption(
"updates",
form.Flag,
"component_auto_update_enabled",
_("Automatic component updates"),
_("Automatically download and install new versions of components"),
);
o.depends("component_update_check_enabled", "1");
o.default = "0";
o.rmempty = false;
o = section.taboption(
"updates",
form.ListValue,
"component_auto_update_mode",
_("Auto-update mode"),
_(
"Choose whether to install updates immediately when detected or at a scheduled time (e.g. maintenance window)",
),
);
o.depends("component_auto_update_enabled", "1");
o.value("immediate", _("Immediately upon release"));
o.value("schedule", _("By schedule"));
o.default = "immediate";
o.rmempty = false;
o = section.taboption(
"updates",
form.Value,
"component_auto_update_time",
_("Scheduled auto-update time"),
_(
"Time of day to run scheduled component updates in HH:MM (24h) format, e.g. 04:00",
),
);
o.depends({
component_auto_update_enabled: "1",
component_auto_update_mode: "schedule",
});
o.placeholder = "04:00";
o.default = "04:00";
o.rmempty = false;
o.validate = function (_section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/.test(normalized)) {
return true;
}
return _("Enter a valid time in HH:MM format (00:00 to 23:59)");
};
o = section.taboption(
"updates",
form.MultiValue,
"component_auto_update_targets",
_("Eligible components for auto-update"),
_("Select which components are permitted to update automatically"),
);
o.depends("component_auto_update_enabled", "1");
o.value("all", _("All components"));
o.value("tachyon", _("Tachyon Core"));
o.value("luci_app_tachyon", _("LuCI Web UI"));
o.value("sing_box", _("sing-box"));
o.value("zapret", _("Zapret"));
o.value("zapret2", _("Zapret2"));
o.value("byedpi", _("ByeDPI"));
o.value("tailscale", _("Tailscale"));
o.default = ["all"];
o.rmempty = false;
o = section.taboption(
"updates",
form.Value,
+56 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-01 08:41+0300\n"
"PO-Revision-Date: 2026-09-01 08:41+0300\n"
"POT-Creation-Date: 2026-09-01 10:48+0300\n"
"PO-Revision-Date: 2026-09-01 10:48+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -314,6 +314,9 @@ msgstr "Все"
msgid "All checks passed — Telegram bot should work correctly"
msgstr "Все проверки пройдены — бот должен работать корректно"
msgid "All components"
msgstr "Все компоненты"
msgid "All devices"
msgstr "Все устройства"
@@ -494,9 +497,15 @@ msgstr "Автолечение сетевого стека"
msgid "Auto-update Hosts Lists"
msgstr "Автообновление списков Hosts"
msgid "Auto-update mode"
msgstr "Режим автообновления"
msgid "Automatic component update checks"
msgstr "Автоматическая проверка обновлений компонентов"
msgid "Automatic component updates"
msgstr "Автоматическое обновление компонентов"
msgid "Automatic User-Agent selection"
msgstr "Автоматический выбор User-Agent"
@@ -506,6 +515,9 @@ msgstr "Автоматически добавлять текст к имени
msgid "Automatically check installed components for new versions"
msgstr "Автоматически проверять установленные компоненты на наличие новых версий"
msgid "Automatically download and install new versions of components"
msgstr "Автоматически загружать и устанавливать новые версии компонентов"
msgid "Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings."
msgstr "Автоматически создать аккаунт Cloudflare WARP и настроить все параметры AmneziaWG."
@@ -626,6 +638,12 @@ msgstr "Встроенные наборы правил"
msgid "By flag emoji from name"
msgstr "По эмодзи флага из имени"
msgid "By schedule"
msgstr "По расписанию"
msgid "ByeDPI"
msgstr ""
msgid "ByeDPI (ciadpi)"
msgstr "ByeDPI (ciadpi)"
@@ -776,6 +794,9 @@ msgstr "Выберите визуальную иконку для профиля
msgid "Choose Hosts File..."
msgstr "Выбрать файл Hosts..."
msgid "Choose whether to install updates immediately when detected or at a scheduled time (e.g. maintenance window)"
msgstr "Выберите, устанавливать ли обновления сразу при обнаружении или по расписанию (например, в ночное окно обслуживания)"
msgid "Choose whether to restrict the entire internet or only specific Tachyon routing sections."
msgstr "Выберите, блокировать ли весь доступ в интернет или только определенные секции Tachyon."
@@ -1337,6 +1358,9 @@ msgstr "Каждая запись — \"proto\", \"proto:port\" или \"proto:p
msgid "Edit"
msgstr "Изменить"
msgid "Eligible components for auto-update"
msgstr "Разрешенные компоненты для автообновления"
msgid "Enable"
msgstr "Включить"
@@ -1454,6 +1478,9 @@ msgstr "Введите домен или IP для проверки (напр. e
msgid "Enter a level name"
msgstr "Введите имя уровня"
msgid "Enter a valid time in HH:MM format (00:00 to 23:59)"
msgstr "Введите корректное время в формате ЧЧ:ММ (от 00:00 до 23:59)"
msgid "Enter a whole number of minutes between 0 and 1440"
msgstr "Введите целое число минут от 0 до 1440"
@@ -1961,6 +1988,9 @@ msgstr "Таймаут простоя"
msgid "If one health check fails with an error, continue running other checks instead of stopping."
msgstr "Если одна проверка завершается ошибкой, продолжать другие проверки, а не останавливаться."
msgid "Immediately upon release"
msgstr "Сразу при выходе новой версии"
msgid "Import"
msgstr "Импорт"
@@ -2495,6 +2525,9 @@ msgstr "Локальный адрес"
msgid "Log Level"
msgstr "Уровень логирования"
msgid "LuCI Web UI"
msgstr "Веб-интерфейс LuCI"
msgid "lx"
msgstr "lx"
@@ -3506,6 +3539,9 @@ msgstr "Расписание"
msgid "Schedule Rule"
msgstr "Правило расписания"
msgid "Scheduled auto-update time"
msgstr "Время обновления по расписанию"
msgid "Score"
msgstr "Баллы"
@@ -3635,6 +3671,9 @@ msgstr "Выберите WAN-интерфейсы для отслеживани
msgid "Select whether to disable the target during the schedule or enable it ONLY during the schedule."
msgstr "Выберите, отключать ли цель в период расписания или включать ТОЛЬКО в период расписания."
msgid "Select which components are permitted to update automatically"
msgstr "Выберите, какие компоненты разрешено обновлять автоматически"
msgid "Selected"
msgstr "Выбран"
@@ -3800,6 +3839,9 @@ msgstr "Не удалось показать конфигурацию sing-box"
msgid "Show subscription metadata for this source on the dashboard"
msgstr "Показывать метаданные подписки для этого источника на дашборде"
msgid "sing-box"
msgstr ""
msgid "Sing-box autostart disabled"
msgstr "Автостарт sing-box отключен"
@@ -4070,6 +4112,9 @@ msgstr "Системная информация"
msgid "Table exist"
msgstr "Таблица существует"
msgid "Tachyon Core"
msgstr "Ядро Tachyon"
msgid "Tachyon Error"
msgstr "Ошибка Tachyon"
@@ -4295,6 +4340,9 @@ msgstr "Время кэширования DNS-записей в секундах
msgid "Time is required (e.g. 22:00)"
msgstr "Укажите время (например, 22:00)"
msgid "Time of day to run scheduled component updates in HH:MM (24h) format, e.g. 04:00"
msgstr "Время суток для запуска планового обновления компонентов в формате ЧЧ:ММ (24ч), например 04:00"
msgid "Time skew tolerance"
msgstr "Допуск отклонения времени"
@@ -4775,6 +4823,9 @@ msgstr "Да"
msgid "You can select Output Network Interface, by default autodetect"
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
msgid "Zapret"
msgstr ""
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
@@ -4802,6 +4853,9 @@ msgstr "Zapret v1 (nfqws)"
msgid "Zapret v2 (nfqws2)"
msgstr "Zapret v2 (nfqws2)"
msgid "Zapret2"
msgstr ""
msgid "Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies."
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
File diff suppressed because it is too large Load Diff
+4
View File
@@ -28,6 +28,10 @@ config settings 'settings'
option download_all_presets '0'
option component_update_check_enabled '1'
option component_update_check_interval '1d'
option component_auto_update_enabled '0'
option component_auto_update_mode 'immediate'
option component_auto_update_time '04:00'
list component_auto_update_targets 'all'
option latency_test_url 'https://www.gstatic.com/generate_204'
option download_lists_via_proxy '0'
option download_components_via_proxy '0'
+1
View File
@@ -229,6 +229,7 @@ function command_spec(command) {
component_action_status: [ "components/updates.uc", "component-action-status", 1 ],
component_action_log: [ "components/updates.uc", "component-action-log", 2 ],
component_updates_if_due: [ "components/updates.uc", "component-updates-if-due", 0 ],
component_auto_update_apply: [ "components/updates.uc", "component-auto-update-apply", 0 ],
component_update_check_cache: [ "components/updates.uc", "component-update-check-cache", 0 ],
hosts_list_update: [ "components/hosts.uc", "list-update", 1 ],
hosts_list_status: [ "components/hosts.uc", "list-status", 0 ],
+152 -32
View File
@@ -109,6 +109,7 @@ let object_or_empty = common.object_or_empty;
let array_or_empty = common.array_or_empty;
let option = common.option;
let bool_option = common.bool_option;
let list_option = common.list_option;
function read_stdin() {
let data = fs.readfile("/dev/stdin");
@@ -568,6 +569,56 @@ function settings_component_update_check_interval(settings) {
return value != "" ? value : "1d";
}
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" || component == "tailscale";
}
function settings_component_auto_update_enabled(settings) {
settings = object_or_empty(settings);
return bool_option(settings, "component_auto_update_enabled", false);
}
function settings_component_auto_update_mode(settings) {
settings = object_or_empty(settings);
let mode = option(settings, "component_auto_update_mode", "immediate");
return (mode == "schedule" || mode == "scheduled") ? "schedule" : "immediate";
}
function settings_component_auto_update_time(settings) {
settings = object_or_empty(settings);
let time = trim(as_string(option(settings, "component_auto_update_time", "04:00")));
return match(time, /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/) != null ? time : "04:00";
}
function settings_component_auto_update_targets(settings) {
settings = object_or_empty(settings);
let raw = list_option(settings, "component_auto_update_targets");
if (length(raw) == 0 || (length(raw) == 1 && raw[0] == "all"))
return [ "all" ];
return raw;
}
function component_is_auto_update_target(settings, component) {
let targets = settings_component_auto_update_targets(settings);
component = normalize_component_name(component);
for (let target in targets) {
if (target == "all" || normalize_component_name(target) == component)
return true;
}
return false;
}
function section_subscription_update_interval(section) {
let result = "";
let result_seconds = 0;
@@ -693,6 +744,16 @@ function cron_refresh_plan_rows(settings, sections, bin, list_marker, subscripti
}
}
if (settings_component_auto_update_enabled(settings) &&
settings_component_auto_update_mode(settings) == "schedule") {
let time_str = settings_component_auto_update_time(settings);
let parts = split(time_str, ":");
let hour = int(parts[0] || "4");
let minute = int(parts[1] || "0");
let cron_time = sprintf("%d %d * * *", minute, hour);
push(rows, "component-auto-update\t" + cron_time + " " + as_string(bin) + " component_auto_update_apply " + as_string(component_marker));
}
return {
status,
rows
@@ -720,7 +781,7 @@ function strip_unmarked_tachyon_cron_lines(data, bin, markers) {
return "";
let escaped_bin = replace(as_string(bin), /([^0-9A-Za-z_])/g, "\\$1");
let pattern = "^[^#\\n]*" + escaped_bin + "[ \\t]+(list_update|subscription_update|component_updates_if_due)([ \\t]|$)";
let pattern = "^[^#\\n]*" + escaped_bin + "[ \\t]+(list_update|subscription_update|component_updates_if_due|component_auto_update_apply)([ \\t]|$)";
let lines = split(data, "\n");
let has_trailing_newline = substr(data, length(data) - 1) == "\n";
@@ -777,6 +838,10 @@ function cron_refresh_apply_result(settings, sections, existing_crontab, bin, li
cron_jobs += rest + "\n";
push(logs, { level: "info", message: "The component update check cron job has been created: " + rest });
}
else if (type == "component-auto-update") {
cron_jobs += rest + "\n";
push(logs, { level: "info", message: "The component auto-update cron job has been created: " + rest });
}
else if (type == "component-error") {
push(logs, { level: "error", message: "Invalid component_update_check_interval value: " + rest });
}
@@ -1358,21 +1423,6 @@ function component_running_job_state_value(component, action, started_at) {
};
}
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" || component == "tailscale";
}
function component_update_check_cache_path(component) {
component = normalize_component_name(component);
if (!valid_component_name(component))
@@ -1824,28 +1874,22 @@ function component_action_worker(state_file, output_file, component, action) {
finish_component_job(state_file, component, action, status, output_file);
}
function component_action_async(component, action) {
function component_action_async_job(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);
}
if (!ensure_component_runtime_dirs())
return { success: false, job_id: "", message: "Failed to create component action state directory" };
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 (state_file == "")
return { success: false, job_id: "", message: "Failed to prepare component action job" };
let running_state = component_running_job_state_value(component, action, now_seconds());
running_state.log_path = component_job_log_path(job_id);
if (!write_state_file(state_file, running_state)) {
component_job_json_response(false, "", "Failed to write component action state");
exit(1);
}
if (!write_state_file(state_file, running_state))
return { success: false, job_id: "", message: "Failed to write component action state" };
let output_file = component_job_output_path(job_id);
let pid = launch_component_worker([
@@ -1859,11 +1903,29 @@ function component_action_async(component, 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);
return { success: false, job_id: "", message: "Failed to write component action worker pid" };
}
component_job_json_response(true, job_id, "Component action started");
return { success: true, job_id, message: "Component action started" };
}
function component_action_async(component, action) {
let res = component_action_async_job(component, action);
component_job_json_response(res.success, res.job_id, res.message);
if (!res.success)
exit(1);
}
function component_has_running_job() {
if (!ensure_component_runtime_dirs())
return false;
component_cleanup_jobs();
for (let path in fs.glob(COMPONENT_JOB_DIR + "/*.json")) {
let val = object_or_empty(read_json_file(path));
if (val.running === true)
return true;
}
return false;
}
function component_action_status(job_id) {
@@ -1938,6 +2000,46 @@ function run_automatic_component_update_check(component) {
return cache_component_update_check_result(value, true);
}
function trigger_component_auto_update(component, target_version) {
component = normalize_component_name(component);
if (component_has_running_job()) {
log_message("[auto-update] Skipping auto-update for " + component + ": another component update is already running", "warn");
return false;
}
log_message("[auto-update] Initiating automatic update for " + component + (target_version != "" ? " to version " + target_version : ""), "info");
let res = component_action_async_job(component, "update");
if (!res.success) {
log_message("[auto-update] Failed to start auto-update for " + component + ": " + res.message, "error");
return false;
}
return true;
}
function component_auto_update_apply() {
let settings = uci_settings();
if (!settings_component_auto_update_enabled(settings)) {
log_message("[auto-update] Automatic component updates are disabled", "info");
exit(0);
}
let checked_components = automatic_component_check_names();
for (let component in checked_components) {
if (!component_is_auto_update_target(settings, component))
continue;
run_automatic_component_update_check(component);
let cached = object_or_empty(read_json_file(component_update_check_cache_path(component)));
if (cached.status == "outdated" || cached.status == "outdated_same_release") {
if (trigger_component_auto_update(component, cached.latest_version)) {
break;
}
}
}
exit(0);
}
function component_updates_if_due() {
let interval = settings_component_update_check_interval(uci_settings());
if (interval == "")
@@ -1976,6 +2078,22 @@ function component_updates_if_due() {
for (let component in automatic_component_check_names())
run_automatic_component_update_check(component);
let settings = uci_settings();
if (settings_component_auto_update_enabled(settings) &&
settings_component_auto_update_mode(settings) == "immediate") {
for (let component in automatic_component_check_names()) {
if (!component_is_auto_update_target(settings, component))
continue;
let cached = object_or_empty(read_json_file(component_update_check_cache_path(component)));
if (cached.status == "outdated" || cached.status == "outdated_same_release") {
if (trigger_component_auto_update(component, cached.latest_version)) {
break;
}
}
}
}
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");
@@ -3285,6 +3403,8 @@ else if (mode == "component-action-log")
component_action_log(ARGV[1], ARGV[2]);
else if (mode == "component-updates-if-due")
component_updates_if_due();
else if (mode == "component-auto-update-apply")
component_auto_update_apply();
else if (mode == "component-update-check-cache")
component_update_check_cache();
else {
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
UPDATES_UC="$TACHYON_LIB/components/updates.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
fake_lib="$WORK_DIR/lib"
cache_dir="$WORK_DIR/cache"
job_dir="$WORK_DIR/jobs"
timestamp_file="$WORK_DIR/component-update-check.timestamp"
mkdir -p \
"$fake_lib/core" \
"$fake_lib/config" \
"$fake_lib/components" \
"$fake_lib/service" \
"$fake_lib/providers/zapret" \
"$fake_lib/providers/zapret2" \
"$fake_lib/providers/byedpi" \
"$job_dir" \
"$cache_dir"
cat >"$fake_lib/core/uci.uc" <<'UCODE'
function get_all(_config, section) {
if (section == "settings") {
return {
component_update_check_enabled: getenv("TEST_COMPONENT_UPDATE_CHECK_ENABLED") || "1",
component_update_check_interval: getenv("TEST_COMPONENT_UPDATE_CHECK_INTERVAL") || "1d",
component_auto_update_enabled: getenv("TEST_COMPONENT_AUTO_UPDATE_ENABLED") || "0",
component_auto_update_mode: getenv("TEST_COMPONENT_AUTO_UPDATE_MODE") || "immediate",
component_auto_update_time: getenv("TEST_COMPONENT_AUTO_UPDATE_TIME") || "04:00",
component_auto_update_targets: getenv("TEST_COMPONENT_AUTO_UPDATE_TARGETS") ? split(getenv("TEST_COMPONENT_AUTO_UPDATE_TARGETS"), " ") : [ "all" ]
};
}
return {};
}
function section_objects(_config, _type) {
return [];
}
return {
get_all,
section_objects
};
UCODE
cat >"$fake_lib/config/connections.uc" <<'UCODE'
return {};
UCODE
cat >"$fake_lib/components/action.uc" <<'UCODE'
#!/usr/bin/env ucode
let component = ARGV[1] || "tachyon";
let action = ARGV[2] || "check_update";
if (action == "check_update") {
let result = {
success: true,
component,
action,
message: "Update is available",
current_version: "1.0.0",
latest_version: "1.1.0",
release_url: "https://example.com/release",
changed: 0,
status: "outdated"
};
print(sprintf("%J\n", result));
exit(0);
}
if (action == "update") {
let result = {
success: true,
component,
action,
message: "Updated successfully",
current_version: "1.1.0",
latest_version: "1.1.0",
changed: 1,
status: "latest"
};
print(sprintf("%J\n", result));
exit(0);
}
UCODE
cat >"$fake_lib/providers/zapret/runtime.uc" <<'UCODE'
#!/usr/bin/env ucode
exit(1);
UCODE
cp "$fake_lib/providers/zapret/runtime.uc" "$fake_lib/providers/zapret2/runtime.uc"
cp "$fake_lib/providers/zapret/runtime.uc" "$fake_lib/providers/byedpi/runtime.uc"
cat >"$fake_lib/service/state.uc" <<'UCODE'
#!/usr/bin/env ucode
exit(0);
UCODE
updates_ucode() {
TEST_COMPONENT_UPDATE_CHECK_ENABLED="${TEST_COMPONENT_UPDATE_CHECK_ENABLED:-1}" \
TEST_COMPONENT_UPDATE_CHECK_INTERVAL="${TEST_COMPONENT_UPDATE_CHECK_INTERVAL:-1d}" \
TEST_COMPONENT_AUTO_UPDATE_ENABLED="${TEST_COMPONENT_AUTO_UPDATE_ENABLED:-0}" \
TEST_COMPONENT_AUTO_UPDATE_MODE="${TEST_COMPONENT_AUTO_UPDATE_MODE:-immediate}" \
TEST_COMPONENT_AUTO_UPDATE_TIME="${TEST_COMPONENT_AUTO_UPDATE_TIME:-04:00}" \
TEST_COMPONENT_AUTO_UPDATE_TARGETS="${TEST_COMPONENT_AUTO_UPDATE_TARGETS:-all}" \
TACHYON_LIB="$fake_lib" \
TACHYON_BIN="/usr/bin/tachyon" \
TACHYON_SERVICE_INIT="/etc/init.d/tachyon" \
TACHYON_COMPONENT_UPDATE_CHECK_CACHE_DIR="$cache_dir" \
TACHYON_COMPONENT_UPDATE_CHECK_STATE_FILE="$timestamp_file" \
UPDATES_JOB_DIR="$job_dir" \
ucode -L "$fake_lib" -L "$TACHYON_LIB" "$UPDATES_UC" "$@"
}
# 1. Test immediate auto-update trigger on check if outdated
TEST_COMPONENT_AUTO_UPDATE_ENABLED=1 \
TEST_COMPONENT_AUTO_UPDATE_MODE=immediate \
updates_ucode component-updates-if-due
# Verify update check cache was populated
[ -f "$cache_dir/tachyon.json" ] || fail "component update check cache missing"
# Verify that auto-update spawned a background job in job_dir
sleep 1
job_files=( "$job_dir"/*.json )
[ -f "${job_files[0]}" ] || fail "auto-update should create background job state"
# 2. Test scheduled apply
rm -rf "$job_dir"/*
TEST_COMPONENT_AUTO_UPDATE_ENABLED=1 \
TEST_COMPONENT_AUTO_UPDATE_MODE=schedule \
TEST_COMPONENT_AUTO_UPDATE_TIME="04:00" \
updates_ucode component-auto-update-apply
sleep 1
job_files=( "$job_dir"/*.json )
[ -f "${job_files[0]}" ] || fail "scheduled auto-update should create background job state"
printf 'component auto update test passed\n'
+18 -5
View File
@@ -50,14 +50,16 @@ fi
echo "=== Running ${#test_files[@]} Tests (-P $PARALLEL) ==="
results_dir="$(mktemp -d)"
results_dir="$(mktemp -d /tmp/tachyon-run-all-XXXXXX 2>/dev/null || mktemp -d)"
cleanup() { rm -rf "$results_dir"; }
trap cleanup EXIT
run_one() {
f="$1"
log="$2"
local f="$1"
local results_dir="$2"
local name
name="$(basename "$f")"
local log="$results_dir/${name%.sh}.log"
echo "--- START $name ---"
if bash "$f" >"$log" 2>&1; then
echo "--- PASS $name ---"
@@ -65,9 +67,20 @@ run_one() {
echo "--- FAIL $name ---"
fi
}
export -f run_one
printf '%s\0' "${test_files[@]}" | xargs -0 -n1 -I'{}' bash -c 'run_one "$1" "$2/$(basename "${1%.sh}").log"' _ '{}' "$results_dir" | tee "$results_dir/progress.log"
progress_log="$results_dir/progress.log"
touch "$progress_log"
running=0
for f in "${test_files[@]}"; do
( run_one "$f" "$results_dir" | tee -a "$progress_log" ) &
running=$((running + 1))
if [ "$running" -ge "$PARALLEL" ]; then
wait -n 2>/dev/null || wait
running=$((running - 1))
fi
done
wait
pass_count=0
fail_count=0
+18
View File
@@ -199,6 +199,24 @@ assert_eq "list-disabled" \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-list-disabled.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \
"disabled list cron plan"
cat >"$WORK_DIR/cron-plan-auto-update.json" <<'JSON'
{
"settings": {
"list_update_enabled": "0",
"component_update_check_enabled": "1",
"component_update_check_interval": "1d",
"component_auto_update_enabled": "1",
"component_auto_update_mode": "schedule",
"component_auto_update_time": "03:30"
},
"section": []
}
JSON
assert_eq $'component\t0 0 * * * /usr/bin/tachyon component_updates_if_due # component\ncomponent-auto-update\t30 3 * * * /usr/bin/tachyon component_auto_update_apply # component' \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-auto-update.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \
"component auto-update cron plan"
cat >"$WORK_DIR/cron-plan-invalid-list.json" <<'JSON'
{
"settings": {
+2 -2
View File
@@ -40,8 +40,8 @@ grep -Fq 'function strip_unmarked_tachyon_cron_lines(' "$UPD" ||
grep -Fq 'strip_unmarked_tachyon_cron_lines(' "$UPD" || true
[ "$(grep -c 'strip_unmarked_tachyon_cron_lines(' "$UPD")" -ge 3 ] ||
fail "unmarked cron line stripping must be applied to remove and refresh paths"
grep -Fq '(list_update|subscription_update|component_updates_if_due)' "$UPD" ||
fail "legacy cron pattern must cover all three tachyon jobs"
grep -Fq '(list_update|subscription_update|component_updates_if_due|component_auto_update_apply)' "$UPD" ||
fail "legacy cron pattern must cover all tachyon jobs"
# 4. Tunnel-style sections count as outbound configuration: a lone AWG/WARP
# section must not trigger "No proxy outbound sections found".