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 работает",
|
||||
"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) {
|
||||
|
||||
+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 ""
|
||||
"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 по правилам."
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -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,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,
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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 ],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) ==="
|
||||
|
||||
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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user