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:
@@ -1233,6 +1233,22 @@ const dictionary = {
|
|||||||
"Proxy route to Telegram API works": "Маршрут через прокси к Telegram API работает",
|
"Proxy route to Telegram API works": "Маршрут через прокси к Telegram API работает",
|
||||||
"sing-box is running": "sing-box запущен",
|
"sing-box is running": "sing-box запущен",
|
||||||
"Test message sent to chat": "Тестовое сообщение отправлено в чат",
|
"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) {
|
function escapePoString(str) {
|
||||||
|
|||||||
+291
-164
File diff suppressed because it is too large
Load Diff
+236
-163
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-01 08:41+0300\n"
|
"POT-Creation-Date: 2026-09-01 10:48+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-01 08:41+0300\n"
|
"PO-Revision-Date: 2026-09-01 10:48+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
@@ -314,6 +314,9 @@ msgstr "Все"
|
|||||||
msgid "All checks passed — Telegram bot should work correctly"
|
msgid "All checks passed — Telegram bot should work correctly"
|
||||||
msgstr "Все проверки пройдены — бот должен работать корректно"
|
msgstr "Все проверки пройдены — бот должен работать корректно"
|
||||||
|
|
||||||
|
msgid "All components"
|
||||||
|
msgstr "Все компоненты"
|
||||||
|
|
||||||
msgid "All devices"
|
msgid "All devices"
|
||||||
msgstr "Все устройства"
|
msgstr "Все устройства"
|
||||||
|
|
||||||
@@ -494,9 +497,15 @@ msgstr "Автолечение сетевого стека"
|
|||||||
msgid "Auto-update Hosts Lists"
|
msgid "Auto-update Hosts Lists"
|
||||||
msgstr "Автообновление списков Hosts"
|
msgstr "Автообновление списков Hosts"
|
||||||
|
|
||||||
|
msgid "Auto-update mode"
|
||||||
|
msgstr "Режим автообновления"
|
||||||
|
|
||||||
msgid "Automatic component update checks"
|
msgid "Automatic component update checks"
|
||||||
msgstr "Автоматическая проверка обновлений компонентов"
|
msgstr "Автоматическая проверка обновлений компонентов"
|
||||||
|
|
||||||
|
msgid "Automatic component updates"
|
||||||
|
msgstr "Автоматическое обновление компонентов"
|
||||||
|
|
||||||
msgid "Automatic User-Agent selection"
|
msgid "Automatic User-Agent selection"
|
||||||
msgstr "Автоматический выбор User-Agent"
|
msgstr "Автоматический выбор User-Agent"
|
||||||
|
|
||||||
@@ -506,6 +515,9 @@ msgstr "Автоматически добавлять текст к имени
|
|||||||
msgid "Automatically check installed components for new versions"
|
msgid "Automatically check installed components for new versions"
|
||||||
msgstr "Автоматически проверять установленные компоненты на наличие новых версий"
|
msgstr "Автоматически проверять установленные компоненты на наличие новых версий"
|
||||||
|
|
||||||
|
msgid "Automatically download and install new versions of components"
|
||||||
|
msgstr "Автоматически загружать и устанавливать новые версии компонентов"
|
||||||
|
|
||||||
msgid "Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings."
|
msgid "Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings."
|
||||||
msgstr "Автоматически создать аккаунт Cloudflare WARP и настроить все параметры AmneziaWG."
|
msgstr "Автоматически создать аккаунт Cloudflare WARP и настроить все параметры AmneziaWG."
|
||||||
|
|
||||||
@@ -626,6 +638,12 @@ msgstr "Встроенные наборы правил"
|
|||||||
msgid "By flag emoji from name"
|
msgid "By flag emoji from name"
|
||||||
msgstr "По эмодзи флага из имени"
|
msgstr "По эмодзи флага из имени"
|
||||||
|
|
||||||
|
msgid "By schedule"
|
||||||
|
msgstr "По расписанию"
|
||||||
|
|
||||||
|
msgid "ByeDPI"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ByeDPI (ciadpi)"
|
msgid "ByeDPI (ciadpi)"
|
||||||
msgstr "ByeDPI (ciadpi)"
|
msgstr "ByeDPI (ciadpi)"
|
||||||
|
|
||||||
@@ -776,6 +794,9 @@ msgstr "Выберите визуальную иконку для профиля
|
|||||||
msgid "Choose Hosts File..."
|
msgid "Choose Hosts File..."
|
||||||
msgstr "Выбрать файл Hosts..."
|
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."
|
msgid "Choose whether to restrict the entire internet or only specific Tachyon routing sections."
|
||||||
msgstr "Выберите, блокировать ли весь доступ в интернет или только определенные секции Tachyon."
|
msgstr "Выберите, блокировать ли весь доступ в интернет или только определенные секции Tachyon."
|
||||||
|
|
||||||
@@ -1337,6 +1358,9 @@ msgstr "Каждая запись — \"proto\", \"proto:port\" или \"proto:p
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Изменить"
|
msgstr "Изменить"
|
||||||
|
|
||||||
|
msgid "Eligible components for auto-update"
|
||||||
|
msgstr "Разрешенные компоненты для автообновления"
|
||||||
|
|
||||||
msgid "Enable"
|
msgid "Enable"
|
||||||
msgstr "Включить"
|
msgstr "Включить"
|
||||||
|
|
||||||
@@ -1454,6 +1478,9 @@ msgstr "Введите домен или IP для проверки (напр. e
|
|||||||
msgid "Enter a level name"
|
msgid "Enter a level name"
|
||||||
msgstr "Введите имя уровня"
|
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"
|
msgid "Enter a whole number of minutes between 0 and 1440"
|
||||||
msgstr "Введите целое число минут от 0 до 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."
|
msgid "If one health check fails with an error, continue running other checks instead of stopping."
|
||||||
msgstr "Если одна проверка завершается ошибкой, продолжать другие проверки, а не останавливаться."
|
msgstr "Если одна проверка завершается ошибкой, продолжать другие проверки, а не останавливаться."
|
||||||
|
|
||||||
|
msgid "Immediately upon release"
|
||||||
|
msgstr "Сразу при выходе новой версии"
|
||||||
|
|
||||||
msgid "Import"
|
msgid "Import"
|
||||||
msgstr "Импорт"
|
msgstr "Импорт"
|
||||||
|
|
||||||
@@ -2495,6 +2525,9 @@ msgstr "Локальный адрес"
|
|||||||
msgid "Log Level"
|
msgid "Log Level"
|
||||||
msgstr "Уровень логирования"
|
msgstr "Уровень логирования"
|
||||||
|
|
||||||
|
msgid "LuCI Web UI"
|
||||||
|
msgstr "Веб-интерфейс LuCI"
|
||||||
|
|
||||||
msgid "lx"
|
msgid "lx"
|
||||||
msgstr "lx"
|
msgstr "lx"
|
||||||
|
|
||||||
@@ -3506,6 +3539,9 @@ msgstr "Расписание"
|
|||||||
msgid "Schedule Rule"
|
msgid "Schedule Rule"
|
||||||
msgstr "Правило расписания"
|
msgstr "Правило расписания"
|
||||||
|
|
||||||
|
msgid "Scheduled auto-update time"
|
||||||
|
msgstr "Время обновления по расписанию"
|
||||||
|
|
||||||
msgid "Score"
|
msgid "Score"
|
||||||
msgstr "Баллы"
|
msgstr "Баллы"
|
||||||
|
|
||||||
@@ -3635,6 +3671,9 @@ msgstr "Выберите WAN-интерфейсы для отслеживани
|
|||||||
msgid "Select whether to disable the target during the schedule or enable it ONLY during the schedule."
|
msgid "Select whether to disable the target during the schedule or enable it ONLY during the schedule."
|
||||||
msgstr "Выберите, отключать ли цель в период расписания или включать ТОЛЬКО в период расписания."
|
msgstr "Выберите, отключать ли цель в период расписания или включать ТОЛЬКО в период расписания."
|
||||||
|
|
||||||
|
msgid "Select which components are permitted to update automatically"
|
||||||
|
msgstr "Выберите, какие компоненты разрешено обновлять автоматически"
|
||||||
|
|
||||||
msgid "Selected"
|
msgid "Selected"
|
||||||
msgstr "Выбран"
|
msgstr "Выбран"
|
||||||
|
|
||||||
@@ -3800,6 +3839,9 @@ msgstr "Не удалось показать конфигурацию sing-box"
|
|||||||
msgid "Show subscription metadata for this source on the dashboard"
|
msgid "Show subscription metadata for this source on the dashboard"
|
||||||
msgstr "Показывать метаданные подписки для этого источника на дашборде"
|
msgstr "Показывать метаданные подписки для этого источника на дашборде"
|
||||||
|
|
||||||
|
msgid "sing-box"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Sing-box autostart disabled"
|
msgid "Sing-box autostart disabled"
|
||||||
msgstr "Автостарт sing-box отключен"
|
msgstr "Автостарт sing-box отключен"
|
||||||
|
|
||||||
@@ -4070,6 +4112,9 @@ msgstr "Системная информация"
|
|||||||
msgid "Table exist"
|
msgid "Table exist"
|
||||||
msgstr "Таблица существует"
|
msgstr "Таблица существует"
|
||||||
|
|
||||||
|
msgid "Tachyon Core"
|
||||||
|
msgstr "Ядро Tachyon"
|
||||||
|
|
||||||
msgid "Tachyon Error"
|
msgid "Tachyon Error"
|
||||||
msgstr "Ошибка Tachyon"
|
msgstr "Ошибка Tachyon"
|
||||||
|
|
||||||
@@ -4295,6 +4340,9 @@ msgstr "Время кэширования DNS-записей в секундах
|
|||||||
msgid "Time is required (e.g. 22:00)"
|
msgid "Time is required (e.g. 22:00)"
|
||||||
msgstr "Укажите время (например, 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"
|
msgid "Time skew tolerance"
|
||||||
msgstr "Допуск отклонения времени"
|
msgstr "Допуск отклонения времени"
|
||||||
|
|
||||||
@@ -4775,6 +4823,9 @@ msgstr "Да"
|
|||||||
msgid "You can select Output Network Interface, by default autodetect"
|
msgid "You can select Output Network Interface, by default autodetect"
|
||||||
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
|
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
|
||||||
|
|
||||||
|
msgid "Zapret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
|
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
|
||||||
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
|
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
|
||||||
|
|
||||||
@@ -4802,6 +4853,9 @@ msgstr "Zapret v1 (nfqws)"
|
|||||||
msgid "Zapret v2 (nfqws2)"
|
msgid "Zapret v2 (nfqws2)"
|
||||||
msgstr "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."
|
msgid "Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies."
|
||||||
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
|
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "fe-app-tachyon",
|
"name": "fe-app-tachyon",
|
||||||
"version": "1.3.9",
|
"version": "1.3.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fe-app-tachyon",
|
"name": "fe-app-tachyon",
|
||||||
"version": "1.3.9",
|
"version": "1.3.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/parser": "7.28.4",
|
"@babel/parser": "7.28.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fe-app-tachyon",
|
"name": "fe-app-tachyon",
|
||||||
"version": "1.3.9",
|
"version": "1.3.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1437,6 +1437,75 @@ function createSettingsContent(section, capabilities) {
|
|||||||
return _("Use sing-box duration format like 1d, 12h or 30m");
|
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(
|
o = section.taboption(
|
||||||
"updates",
|
"updates",
|
||||||
form.Value,
|
form.Value,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-01 08:41+0300\n"
|
"POT-Creation-Date: 2026-09-01 10:48+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-01 08:41+0300\n"
|
"PO-Revision-Date: 2026-09-01 10:48+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
@@ -314,6 +314,9 @@ msgstr "Все"
|
|||||||
msgid "All checks passed — Telegram bot should work correctly"
|
msgid "All checks passed — Telegram bot should work correctly"
|
||||||
msgstr "Все проверки пройдены — бот должен работать корректно"
|
msgstr "Все проверки пройдены — бот должен работать корректно"
|
||||||
|
|
||||||
|
msgid "All components"
|
||||||
|
msgstr "Все компоненты"
|
||||||
|
|
||||||
msgid "All devices"
|
msgid "All devices"
|
||||||
msgstr "Все устройства"
|
msgstr "Все устройства"
|
||||||
|
|
||||||
@@ -494,9 +497,15 @@ msgstr "Автолечение сетевого стека"
|
|||||||
msgid "Auto-update Hosts Lists"
|
msgid "Auto-update Hosts Lists"
|
||||||
msgstr "Автообновление списков Hosts"
|
msgstr "Автообновление списков Hosts"
|
||||||
|
|
||||||
|
msgid "Auto-update mode"
|
||||||
|
msgstr "Режим автообновления"
|
||||||
|
|
||||||
msgid "Automatic component update checks"
|
msgid "Automatic component update checks"
|
||||||
msgstr "Автоматическая проверка обновлений компонентов"
|
msgstr "Автоматическая проверка обновлений компонентов"
|
||||||
|
|
||||||
|
msgid "Automatic component updates"
|
||||||
|
msgstr "Автоматическое обновление компонентов"
|
||||||
|
|
||||||
msgid "Automatic User-Agent selection"
|
msgid "Automatic User-Agent selection"
|
||||||
msgstr "Автоматический выбор User-Agent"
|
msgstr "Автоматический выбор User-Agent"
|
||||||
|
|
||||||
@@ -506,6 +515,9 @@ msgstr "Автоматически добавлять текст к имени
|
|||||||
msgid "Automatically check installed components for new versions"
|
msgid "Automatically check installed components for new versions"
|
||||||
msgstr "Автоматически проверять установленные компоненты на наличие новых версий"
|
msgstr "Автоматически проверять установленные компоненты на наличие новых версий"
|
||||||
|
|
||||||
|
msgid "Automatically download and install new versions of components"
|
||||||
|
msgstr "Автоматически загружать и устанавливать новые версии компонентов"
|
||||||
|
|
||||||
msgid "Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings."
|
msgid "Automatically generate a Cloudflare WARP account and configure all AmneziaWG settings."
|
||||||
msgstr "Автоматически создать аккаунт Cloudflare WARP и настроить все параметры AmneziaWG."
|
msgstr "Автоматически создать аккаунт Cloudflare WARP и настроить все параметры AmneziaWG."
|
||||||
|
|
||||||
@@ -626,6 +638,12 @@ msgstr "Встроенные наборы правил"
|
|||||||
msgid "By flag emoji from name"
|
msgid "By flag emoji from name"
|
||||||
msgstr "По эмодзи флага из имени"
|
msgstr "По эмодзи флага из имени"
|
||||||
|
|
||||||
|
msgid "By schedule"
|
||||||
|
msgstr "По расписанию"
|
||||||
|
|
||||||
|
msgid "ByeDPI"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ByeDPI (ciadpi)"
|
msgid "ByeDPI (ciadpi)"
|
||||||
msgstr "ByeDPI (ciadpi)"
|
msgstr "ByeDPI (ciadpi)"
|
||||||
|
|
||||||
@@ -776,6 +794,9 @@ msgstr "Выберите визуальную иконку для профиля
|
|||||||
msgid "Choose Hosts File..."
|
msgid "Choose Hosts File..."
|
||||||
msgstr "Выбрать файл Hosts..."
|
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."
|
msgid "Choose whether to restrict the entire internet or only specific Tachyon routing sections."
|
||||||
msgstr "Выберите, блокировать ли весь доступ в интернет или только определенные секции Tachyon."
|
msgstr "Выберите, блокировать ли весь доступ в интернет или только определенные секции Tachyon."
|
||||||
|
|
||||||
@@ -1337,6 +1358,9 @@ msgstr "Каждая запись — \"proto\", \"proto:port\" или \"proto:p
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Изменить"
|
msgstr "Изменить"
|
||||||
|
|
||||||
|
msgid "Eligible components for auto-update"
|
||||||
|
msgstr "Разрешенные компоненты для автообновления"
|
||||||
|
|
||||||
msgid "Enable"
|
msgid "Enable"
|
||||||
msgstr "Включить"
|
msgstr "Включить"
|
||||||
|
|
||||||
@@ -1454,6 +1478,9 @@ msgstr "Введите домен или IP для проверки (напр. e
|
|||||||
msgid "Enter a level name"
|
msgid "Enter a level name"
|
||||||
msgstr "Введите имя уровня"
|
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"
|
msgid "Enter a whole number of minutes between 0 and 1440"
|
||||||
msgstr "Введите целое число минут от 0 до 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."
|
msgid "If one health check fails with an error, continue running other checks instead of stopping."
|
||||||
msgstr "Если одна проверка завершается ошибкой, продолжать другие проверки, а не останавливаться."
|
msgstr "Если одна проверка завершается ошибкой, продолжать другие проверки, а не останавливаться."
|
||||||
|
|
||||||
|
msgid "Immediately upon release"
|
||||||
|
msgstr "Сразу при выходе новой версии"
|
||||||
|
|
||||||
msgid "Import"
|
msgid "Import"
|
||||||
msgstr "Импорт"
|
msgstr "Импорт"
|
||||||
|
|
||||||
@@ -2495,6 +2525,9 @@ msgstr "Локальный адрес"
|
|||||||
msgid "Log Level"
|
msgid "Log Level"
|
||||||
msgstr "Уровень логирования"
|
msgstr "Уровень логирования"
|
||||||
|
|
||||||
|
msgid "LuCI Web UI"
|
||||||
|
msgstr "Веб-интерфейс LuCI"
|
||||||
|
|
||||||
msgid "lx"
|
msgid "lx"
|
||||||
msgstr "lx"
|
msgstr "lx"
|
||||||
|
|
||||||
@@ -3506,6 +3539,9 @@ msgstr "Расписание"
|
|||||||
msgid "Schedule Rule"
|
msgid "Schedule Rule"
|
||||||
msgstr "Правило расписания"
|
msgstr "Правило расписания"
|
||||||
|
|
||||||
|
msgid "Scheduled auto-update time"
|
||||||
|
msgstr "Время обновления по расписанию"
|
||||||
|
|
||||||
msgid "Score"
|
msgid "Score"
|
||||||
msgstr "Баллы"
|
msgstr "Баллы"
|
||||||
|
|
||||||
@@ -3635,6 +3671,9 @@ msgstr "Выберите WAN-интерфейсы для отслеживани
|
|||||||
msgid "Select whether to disable the target during the schedule or enable it ONLY during the schedule."
|
msgid "Select whether to disable the target during the schedule or enable it ONLY during the schedule."
|
||||||
msgstr "Выберите, отключать ли цель в период расписания или включать ТОЛЬКО в период расписания."
|
msgstr "Выберите, отключать ли цель в период расписания или включать ТОЛЬКО в период расписания."
|
||||||
|
|
||||||
|
msgid "Select which components are permitted to update automatically"
|
||||||
|
msgstr "Выберите, какие компоненты разрешено обновлять автоматически"
|
||||||
|
|
||||||
msgid "Selected"
|
msgid "Selected"
|
||||||
msgstr "Выбран"
|
msgstr "Выбран"
|
||||||
|
|
||||||
@@ -3800,6 +3839,9 @@ msgstr "Не удалось показать конфигурацию sing-box"
|
|||||||
msgid "Show subscription metadata for this source on the dashboard"
|
msgid "Show subscription metadata for this source on the dashboard"
|
||||||
msgstr "Показывать метаданные подписки для этого источника на дашборде"
|
msgstr "Показывать метаданные подписки для этого источника на дашборде"
|
||||||
|
|
||||||
|
msgid "sing-box"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Sing-box autostart disabled"
|
msgid "Sing-box autostart disabled"
|
||||||
msgstr "Автостарт sing-box отключен"
|
msgstr "Автостарт sing-box отключен"
|
||||||
|
|
||||||
@@ -4070,6 +4112,9 @@ msgstr "Системная информация"
|
|||||||
msgid "Table exist"
|
msgid "Table exist"
|
||||||
msgstr "Таблица существует"
|
msgstr "Таблица существует"
|
||||||
|
|
||||||
|
msgid "Tachyon Core"
|
||||||
|
msgstr "Ядро Tachyon"
|
||||||
|
|
||||||
msgid "Tachyon Error"
|
msgid "Tachyon Error"
|
||||||
msgstr "Ошибка Tachyon"
|
msgstr "Ошибка Tachyon"
|
||||||
|
|
||||||
@@ -4295,6 +4340,9 @@ msgstr "Время кэширования DNS-записей в секундах
|
|||||||
msgid "Time is required (e.g. 22:00)"
|
msgid "Time is required (e.g. 22:00)"
|
||||||
msgstr "Укажите время (например, 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"
|
msgid "Time skew tolerance"
|
||||||
msgstr "Допуск отклонения времени"
|
msgstr "Допуск отклонения времени"
|
||||||
|
|
||||||
@@ -4775,6 +4823,9 @@ msgstr "Да"
|
|||||||
msgid "You can select Output Network Interface, by default autodetect"
|
msgid "You can select Output Network Interface, by default autodetect"
|
||||||
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
|
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию автоопределение"
|
||||||
|
|
||||||
|
msgid "Zapret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
|
msgid "Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies."
|
||||||
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
|
msgstr "Шаблоны hostlist Zapret не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS по правилам."
|
||||||
|
|
||||||
@@ -4802,6 +4853,9 @@ msgstr "Zapret v1 (nfqws)"
|
|||||||
msgid "Zapret v2 (nfqws2)"
|
msgid "Zapret v2 (nfqws2)"
|
||||||
msgstr "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."
|
msgid "Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies."
|
||||||
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
|
msgstr "Шаблоны hostlist Zapret2 не поддерживаются здесь, потому что Tachyon не расширяет их для стратегий NFQWS2 по правилам."
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,10 @@ config settings 'settings'
|
|||||||
option download_all_presets '0'
|
option download_all_presets '0'
|
||||||
option component_update_check_enabled '1'
|
option component_update_check_enabled '1'
|
||||||
option component_update_check_interval '1d'
|
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 latency_test_url 'https://www.gstatic.com/generate_204'
|
||||||
option download_lists_via_proxy '0'
|
option download_lists_via_proxy '0'
|
||||||
option download_components_via_proxy '0'
|
option download_components_via_proxy '0'
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ function command_spec(command) {
|
|||||||
component_action_status: [ "components/updates.uc", "component-action-status", 1 ],
|
component_action_status: [ "components/updates.uc", "component-action-status", 1 ],
|
||||||
component_action_log: [ "components/updates.uc", "component-action-log", 2 ],
|
component_action_log: [ "components/updates.uc", "component-action-log", 2 ],
|
||||||
component_updates_if_due: [ "components/updates.uc", "component-updates-if-due", 0 ],
|
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 ],
|
component_update_check_cache: [ "components/updates.uc", "component-update-check-cache", 0 ],
|
||||||
hosts_list_update: [ "components/hosts.uc", "list-update", 1 ],
|
hosts_list_update: [ "components/hosts.uc", "list-update", 1 ],
|
||||||
hosts_list_status: [ "components/hosts.uc", "list-status", 0 ],
|
hosts_list_status: [ "components/hosts.uc", "list-status", 0 ],
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ let object_or_empty = common.object_or_empty;
|
|||||||
let array_or_empty = common.array_or_empty;
|
let array_or_empty = common.array_or_empty;
|
||||||
let option = common.option;
|
let option = common.option;
|
||||||
let bool_option = common.bool_option;
|
let bool_option = common.bool_option;
|
||||||
|
let list_option = common.list_option;
|
||||||
|
|
||||||
function read_stdin() {
|
function read_stdin() {
|
||||||
let data = fs.readfile("/dev/stdin");
|
let data = fs.readfile("/dev/stdin");
|
||||||
@@ -568,6 +569,56 @@ function settings_component_update_check_interval(settings) {
|
|||||||
return value != "" ? value : "1d";
|
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) {
|
function section_subscription_update_interval(section) {
|
||||||
let result = "";
|
let result = "";
|
||||||
let result_seconds = 0;
|
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 {
|
return {
|
||||||
status,
|
status,
|
||||||
rows
|
rows
|
||||||
@@ -720,7 +781,7 @@ function strip_unmarked_tachyon_cron_lines(data, bin, markers) {
|
|||||||
return "";
|
return "";
|
||||||
|
|
||||||
let escaped_bin = replace(as_string(bin), /([^0-9A-Za-z_])/g, "\\$1");
|
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 lines = split(data, "\n");
|
||||||
let has_trailing_newline = substr(data, length(data) - 1) == "\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";
|
cron_jobs += rest + "\n";
|
||||||
push(logs, { level: "info", message: "The component update check cron job has been created: " + rest });
|
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") {
|
else if (type == "component-error") {
|
||||||
push(logs, { level: "error", message: "Invalid component_update_check_interval value: " + rest });
|
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) {
|
function component_update_check_cache_path(component) {
|
||||||
component = normalize_component_name(component);
|
component = normalize_component_name(component);
|
||||||
if (!valid_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);
|
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);
|
component = normalize_component_name(component);
|
||||||
if (!ensure_component_runtime_dirs()) {
|
if (!ensure_component_runtime_dirs())
|
||||||
component_job_json_response(false, "", "Failed to create component action state directory");
|
return { success: false, job_id: "", message: "Failed to create component action state directory" };
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
component_cleanup_jobs();
|
component_cleanup_jobs();
|
||||||
|
|
||||||
let job_id = component_job_id();
|
let job_id = component_job_id();
|
||||||
let state_file = component_job_state_path_value(job_id);
|
let state_file = component_job_state_path_value(job_id);
|
||||||
if (state_file == "") {
|
if (state_file == "")
|
||||||
component_job_json_response(false, "", "Failed to prepare component action job");
|
return { success: false, job_id: "", message: "Failed to prepare component action job" };
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let running_state = component_running_job_state_value(component, action, now_seconds());
|
let running_state = component_running_job_state_value(component, action, now_seconds());
|
||||||
running_state.log_path = component_job_log_path(job_id);
|
running_state.log_path = component_job_log_path(job_id);
|
||||||
if (!write_state_file(state_file, running_state)) {
|
if (!write_state_file(state_file, running_state))
|
||||||
component_job_json_response(false, "", "Failed to write component action state");
|
return { success: false, job_id: "", message: "Failed to write component action state" };
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let output_file = component_job_output_path(job_id);
|
let output_file = component_job_output_path(job_id);
|
||||||
let pid = launch_component_worker([
|
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 == "" || !set_component_running_job_pid(state_file, pid)) {
|
||||||
if (pid != "")
|
if (pid != "")
|
||||||
command_success_from_args([ "kill", pid ]);
|
command_success_from_args([ "kill", pid ]);
|
||||||
component_job_json_response(false, "", "Failed to write component action worker pid");
|
return { success: false, job_id: "", message: "Failed to write component action worker pid" };
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
component_job_json_response(true, job_id, "Component action started");
|
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) {
|
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);
|
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() {
|
function component_updates_if_due() {
|
||||||
let interval = settings_component_update_check_interval(uci_settings());
|
let interval = settings_component_update_check_interval(uci_settings());
|
||||||
if (interval == "")
|
if (interval == "")
|
||||||
@@ -1976,6 +2078,22 @@ function component_updates_if_due() {
|
|||||||
for (let component in automatic_component_check_names())
|
for (let component in automatic_component_check_names())
|
||||||
run_automatic_component_update_check(component);
|
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")) {
|
if (!write_file(COMPONENT_UPDATE_CHECK_STATE_FILE, as_string(now_seconds()) + "\n")) {
|
||||||
release_runtime_lock(COMPONENT_UPDATE_CHECK_LOCK_DIR);
|
release_runtime_lock(COMPONENT_UPDATE_CHECK_LOCK_DIR);
|
||||||
log_message("Failed to write component update check timestamp", "error");
|
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]);
|
component_action_log(ARGV[1], ARGV[2]);
|
||||||
else if (mode == "component-updates-if-due")
|
else if (mode == "component-updates-if-due")
|
||||||
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")
|
else if (mode == "component-update-check-cache")
|
||||||
component_update_check_cache();
|
component_update_check_cache();
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -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
@@ -50,14 +50,16 @@ fi
|
|||||||
|
|
||||||
echo "=== Running ${#test_files[@]} Tests (-P $PARALLEL) ==="
|
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"; }
|
cleanup() { rm -rf "$results_dir"; }
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
run_one() {
|
run_one() {
|
||||||
f="$1"
|
local f="$1"
|
||||||
log="$2"
|
local results_dir="$2"
|
||||||
|
local name
|
||||||
name="$(basename "$f")"
|
name="$(basename "$f")"
|
||||||
|
local log="$results_dir/${name%.sh}.log"
|
||||||
echo "--- START $name ---"
|
echo "--- START $name ---"
|
||||||
if bash "$f" >"$log" 2>&1; then
|
if bash "$f" >"$log" 2>&1; then
|
||||||
echo "--- PASS $name ---"
|
echo "--- PASS $name ---"
|
||||||
@@ -65,9 +67,20 @@ run_one() {
|
|||||||
echo "--- FAIL $name ---"
|
echo "--- FAIL $name ---"
|
||||||
fi
|
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
|
pass_count=0
|
||||||
fail_count=0
|
fail_count=0
|
||||||
|
|||||||
@@ -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')" \
|
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-list-disabled.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \
|
||||||
"disabled list cron plan"
|
"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'
|
cat >"$WORK_DIR/cron-plan-invalid-list.json" <<'JSON'
|
||||||
{
|
{
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ grep -Fq 'function strip_unmarked_tachyon_cron_lines(' "$UPD" ||
|
|||||||
grep -Fq 'strip_unmarked_tachyon_cron_lines(' "$UPD" || true
|
grep -Fq 'strip_unmarked_tachyon_cron_lines(' "$UPD" || true
|
||||||
[ "$(grep -c 'strip_unmarked_tachyon_cron_lines(' "$UPD")" -ge 3 ] ||
|
[ "$(grep -c 'strip_unmarked_tachyon_cron_lines(' "$UPD")" -ge 3 ] ||
|
||||||
fail "unmarked cron line stripping must be applied to remove and refresh paths"
|
fail "unmarked cron line stripping must be applied to remove and refresh paths"
|
||||||
grep -Fq '(list_update|subscription_update|component_updates_if_due)' "$UPD" ||
|
grep -Fq '(list_update|subscription_update|component_updates_if_due|component_auto_update_apply)' "$UPD" ||
|
||||||
fail "legacy cron pattern must cover all three tachyon jobs"
|
fail "legacy cron pattern must cover all tachyon jobs"
|
||||||
|
|
||||||
# 4. Tunnel-style sections count as outbound configuration: a lone AWG/WARP
|
# 4. Tunnel-style sections count as outbound configuration: a lone AWG/WARP
|
||||||
# section must not trigger "No proxy outbound sections found".
|
# section must not trigger "No proxy outbound sections found".
|
||||||
|
|||||||
Reference in New Issue
Block a user