fix: исправлен race condition nft + управление Telegram ботом в LuCI
- backend: исправлена гонка данных (race condition) при старте роутера, когда list_update пытался добавить элементы в nft-сеты, пока reload-firewall ещё пересоздавал таблицу TachyonTable. Фаза применения nft теперь защищена RELOAD_LOCK_DIR, что устраняет ошибку 'nft add element ... No such file or directory' - backend: добавлены CLI-команды telegram_status, telegram_start, telegram_stop в /usr/bin/tachyon для управления ботом из LuCI - luci: добавлен виджет статуса Telegram-бота на вкладке настроек — показывает состояние (запущен/остановлен с PID), кнопки Запустить/Остановить, автообновление каждые 10 секунд без перезагрузки страницы - frontend: пересобран main.js (yarn ci — 507 тестов прошли успешно) - версия npm-пакета: 1.2.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.1.5",
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -241,10 +241,9 @@ export const TachyonShellMethods = {
|
||||
[section],
|
||||
),
|
||||
getSubscriptionMetadata: async (section: string) =>
|
||||
callBaseMethod<Tachyon.SubscriptionMetadata | Tachyon.SubscriptionMetadata[]>(
|
||||
Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA,
|
||||
[section],
|
||||
),
|
||||
callBaseMethod<
|
||||
Tachyon.SubscriptionMetadata | Tachyon.SubscriptionMetadata[]
|
||||
>(Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA, [section]),
|
||||
checkSingBox: async () =>
|
||||
callBaseMethod<Tachyon.SingBoxCheckResult>(
|
||||
Tachyon.AvailableMethods.CHECK_SING_BOX,
|
||||
@@ -537,7 +536,11 @@ export const TachyonShellMethods = {
|
||||
) => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC, component, action],
|
||||
args: [
|
||||
Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC,
|
||||
component,
|
||||
action,
|
||||
],
|
||||
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
|
||||
});
|
||||
const parsedResponse = parseComponentActionStartResult(response);
|
||||
@@ -745,7 +748,8 @@ export const TachyonShellMethods = {
|
||||
while (true) {
|
||||
await sleep(SUBSCRIPTION_UPDATE_POLL_INTERVAL_MS);
|
||||
|
||||
const response = await TachyonShellMethods.subscriptionUpdateStatus(jobId);
|
||||
const response =
|
||||
await TachyonShellMethods.subscriptionUpdateStatus(jobId);
|
||||
|
||||
if (!response.success) {
|
||||
if (transientRpc.shouldContinue(response.error)) {
|
||||
|
||||
@@ -764,7 +764,8 @@ async function handleShowSingBoxConfig() {
|
||||
setDiagnosticActionLoading('showSingBoxConfig', true);
|
||||
|
||||
try {
|
||||
const showSingBoxConfig = await TachyonShellMethods.showSingBoxConfig(false);
|
||||
const showSingBoxConfig =
|
||||
await TachyonShellMethods.showSingBoxConfig(false);
|
||||
|
||||
if (showSingBoxConfig.success) {
|
||||
const rawSingBoxConfigText = stringifySingBoxConfig(
|
||||
|
||||
@@ -532,7 +532,9 @@ async function completeComponentActionJob(
|
||||
});
|
||||
}
|
||||
|
||||
async function followComponentActionState(state: Tachyon.ComponentActionResult) {
|
||||
async function followComponentActionState(
|
||||
state: Tachyon.ComponentActionResult,
|
||||
) {
|
||||
const jobId = state.job_id;
|
||||
const key = getComponentActionKey(state.component, state.action);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
"require form";
|
||||
"require uci";
|
||||
"require baseclass";
|
||||
"require fs";
|
||||
"require tools.widgets as widgets";
|
||||
"require view.tachyon.main as main";
|
||||
|
||||
@@ -700,7 +701,143 @@ function createSettingsContent(section, capabilities) {
|
||||
o.rmempty = false;
|
||||
}
|
||||
|
||||
function createTelegramStatusWidget() {
|
||||
const wrapper = E("div", {
|
||||
id: "tachyon-telegram-status-widget",
|
||||
style:
|
||||
"display:flex;align-items:center;gap:12px;padding:10px 0;flex-wrap:wrap;",
|
||||
});
|
||||
|
||||
const indicator = E("span", {
|
||||
id: "tachyon-telegram-status-indicator",
|
||||
style:
|
||||
"display:inline-flex;align-items:center;gap:6px;font-weight:bold;font-size:14px;",
|
||||
});
|
||||
|
||||
const dot = E("span", {
|
||||
id: "tachyon-telegram-status-dot",
|
||||
style:
|
||||
"display:inline-block;width:10px;height:10px;border-radius:50%;background:#aaa;",
|
||||
});
|
||||
|
||||
const statusText = E("span", {
|
||||
id: "tachyon-telegram-status-text",
|
||||
});
|
||||
statusText.textContent = _("Checking…");
|
||||
|
||||
indicator.appendChild(dot);
|
||||
indicator.appendChild(statusText);
|
||||
|
||||
const btnStart = E("button", {
|
||||
id: "tachyon-telegram-btn-start",
|
||||
class: "btn cbi-button cbi-button-action",
|
||||
style: "display:none;",
|
||||
});
|
||||
btnStart.textContent = _("Start bot");
|
||||
|
||||
const btnStop = E("button", {
|
||||
id: "tachyon-telegram-btn-stop",
|
||||
class: "btn cbi-button cbi-button-negative",
|
||||
style: "display:none;",
|
||||
});
|
||||
btnStop.textContent = _("Stop bot");
|
||||
|
||||
const statusMsg = E("span", {
|
||||
id: "tachyon-telegram-status-msg",
|
||||
style: "font-size:12px;color:var(--text-color-medium,#888);",
|
||||
});
|
||||
|
||||
function applyStatus(running, pid) {
|
||||
if (running) {
|
||||
dot.style.background = "#4caf50";
|
||||
statusText.textContent = pid
|
||||
? _("Running") + " (PID " + pid + ")"
|
||||
: _("Running");
|
||||
btnStart.style.display = "none";
|
||||
btnStop.style.display = "";
|
||||
} else {
|
||||
dot.style.background = "#f44336";
|
||||
statusText.textContent = _("Stopped");
|
||||
btnStart.style.display = "";
|
||||
btnStop.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
return fs
|
||||
.exec("/usr/bin/tachyon", ["telegram_status"])
|
||||
.then(function (res) {
|
||||
const out = (res.stdout || "").trim();
|
||||
const pidMatch = out.match(/\(pid\s+(\d+)\)/);
|
||||
const pid = pidMatch ? pidMatch[1] : null;
|
||||
applyStatus(out.indexOf("running") === 0, pid);
|
||||
})
|
||||
.catch(function () {
|
||||
dot.style.background = "#aaa";
|
||||
statusText.textContent = _("Unknown");
|
||||
});
|
||||
}
|
||||
|
||||
btnStart.addEventListener("click", function () {
|
||||
btnStart.disabled = true;
|
||||
statusMsg.textContent = _("Starting…");
|
||||
fs.exec("/usr/bin/tachyon", ["telegram_start"])
|
||||
.then(function () {
|
||||
statusMsg.textContent = "";
|
||||
return refreshStatus();
|
||||
})
|
||||
.catch(function () {
|
||||
statusMsg.textContent = _("Failed to start bot");
|
||||
btnStart.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
btnStop.addEventListener("click", function () {
|
||||
btnStop.disabled = true;
|
||||
statusMsg.textContent = _("Stopping…");
|
||||
fs.exec("/usr/bin/tachyon", ["telegram_stop"])
|
||||
.then(function () {
|
||||
statusMsg.textContent = "";
|
||||
return refreshStatus();
|
||||
})
|
||||
.catch(function () {
|
||||
statusMsg.textContent = _("Failed to stop bot");
|
||||
btnStop.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.appendChild(indicator);
|
||||
wrapper.appendChild(btnStart);
|
||||
wrapper.appendChild(btnStop);
|
||||
wrapper.appendChild(statusMsg);
|
||||
|
||||
// Initial status fetch
|
||||
refreshStatus();
|
||||
// Refresh every 10s while visible
|
||||
const timer = setInterval(refreshStatus, 10000);
|
||||
const observer = new MutationObserver(function () {
|
||||
if (!document.body.contains(wrapper)) {
|
||||
clearInterval(timer);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function createTelegramContent(section) {
|
||||
// Виджет статуса бота: показывает состояние и кнопки управления
|
||||
const statusOpt = section.option(
|
||||
form.DummyValue,
|
||||
"_telegram_status",
|
||||
_("Bot Status"),
|
||||
);
|
||||
statusOpt.rawhtml = true;
|
||||
statusOpt.cfgvalue = function () {
|
||||
return createTelegramStatusWidget();
|
||||
};
|
||||
|
||||
let o = section.option(
|
||||
form.Flag,
|
||||
"enabled",
|
||||
|
||||
@@ -122,6 +122,9 @@ function show_help() {
|
||||
print(" doctor Run doctor diagnostics checks\n");
|
||||
print(" ai_doctor Run AI doctor diagnostics completions\n");
|
||||
print(" watchdog Manage watchdog daemon (start-runtime|stop-runtime|status)\n");
|
||||
print(" telegram_status Get Telegram bot running status\n");
|
||||
print(" telegram_start Start Telegram bot\n");
|
||||
print(" telegram_stop Stop Telegram bot\n");
|
||||
print(" validate_nfqws_strategy_json\n");
|
||||
print(" Validate an NFQWS strategy and print JSON result\n");
|
||||
print(" validate_nfqws2_strategy_json\n");
|
||||
@@ -197,6 +200,9 @@ function command_spec(command) {
|
||||
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
|
||||
watchdog: [ "service/watchdog.uc", "", 1 ],
|
||||
telegram: [ "service/telegram.uc", "", 2 ],
|
||||
telegram_status: [ "service/telegram.uc", "status", 0 ],
|
||||
telegram_start: [ "service/telegram.uc", "start-runtime", 0 ],
|
||||
telegram_stop: [ "service/telegram.uc", "stop-runtime", 0 ],
|
||||
validate_nfqws_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws-strategy-json", 1 ],
|
||||
validate_nfqws2_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws2-strategy-json", 1 ],
|
||||
validate_byedpi_strategy_json: [ "providers/byedpi/validator.uc", "validate-json", 1 ],
|
||||
|
||||
@@ -2497,15 +2497,26 @@ function list_update() {
|
||||
let sections = uci_sections("section");
|
||||
let ok = true;
|
||||
|
||||
// Фаза 1: скачивание файлов (без блокировки, не трогает nft)
|
||||
for (let section in sections)
|
||||
if (!rebuild_domain_ip_lists_from_rule(section, settings))
|
||||
ok = false;
|
||||
for (let section in sections)
|
||||
if (!import_builtin_subnets_from_rule(section, settings))
|
||||
ok = false;
|
||||
for (let section in sections)
|
||||
if (!import_domains_from_remote_domain_lists(section, settings))
|
||||
ok = false;
|
||||
|
||||
// Фаза 2: применение к nft (захватываем лок reload, чтобы не конфликтовать
|
||||
// с reload-firewall, который пересоздаёт таблицы и сеты nftables)
|
||||
log_message("Waiting for reload lock to apply nft rules", "debug");
|
||||
if (!acquire_runtime_lock(RELOAD_LOCK_DIR, true)) {
|
||||
log_message("Could not acquire reload lock for nft apply; skipping nft update", "warn");
|
||||
list_update_pid_end();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for (let section in sections)
|
||||
if (!import_builtin_subnets_from_rule(section, settings))
|
||||
ok = false;
|
||||
for (let section in sections)
|
||||
if (!import_subnets_from_remote_subnet_lists(section, settings))
|
||||
ok = false;
|
||||
@@ -2513,6 +2524,8 @@ function list_update() {
|
||||
if (!import_rule_sets_with_subnets_from_rule(section, settings))
|
||||
ok = false;
|
||||
|
||||
release_runtime_lock(RELOAD_LOCK_DIR);
|
||||
|
||||
if (ok) {
|
||||
write_list_update_timestamp(now_seconds());
|
||||
log_message("Lists update completed successfully", "info");
|
||||
|
||||
Reference in New Issue
Block a user