fix(updates): предотвратить повторное открытие и зависание модального окна обновления

- Исключить повторное открытие окна прогресса для уже завершенных задач (handledComponentJobs)

- Актуализировать состояние фоновых задач в get_ui_state и нормализовать таймаут stale PID

- Исправить определение WAN интерфейса в event_controller во избежание ложного перезапуска (#31)

- Улучшить сверку версий при самообновлении
This commit is contained in:
Dushnilin
2026-08-15 09:23:07 +03:00
parent d6c4f81daf
commit 1e406c3e06
11 changed files with 90 additions and 86 deletions
+1
View File
@@ -15,3 +15,4 @@ scratch/
tmp_*/
tmp_test*.uc
test_out.txt
uc.out
@@ -659,6 +659,12 @@ export const TachyonShellMethods = {
COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS,
);
const versionsMatch = (a: string, b: string) => {
const cleanA = a.replace(/^v/i, '').trim();
const cleanB = b.replace(/^v/i, '').trim();
return cleanA === cleanB;
};
// A self-update restarts the service mid-worker: the worker can die
// before writing its result, the state file can stay "running" forever
// (recycled pid), and RPC can go away during the swap. The installed
@@ -673,10 +679,14 @@ export const TachyonShellMethods = {
}
const version = await readTachyonVersion();
if (!version) return '';
if (targetVersion && version === targetVersion) {
if (targetVersion && versionsMatch(version, targetVersion)) {
return version;
}
if (!targetVersion && baselineVersion && version !== baselineVersion) {
if (
!targetVersion &&
baselineVersion &&
!versionsMatch(version, baselineVersion)
) {
return version;
}
return '';
@@ -691,8 +701,8 @@ export const TachyonShellMethods = {
}
const version = await readTachyonVersion();
if (
version === baselineVersion &&
(!targetVersion || version === targetVersion)
versionsMatch(version, baselineVersion) &&
(!targetVersion || versionsMatch(version, targetVersion))
) {
return version;
}
@@ -75,8 +75,6 @@ let dashboardDataUpdatesStarted = false;
let dashboardDataUpdatesId = 0;
let connectionsRefreshTimer: ReturnType<typeof setInterval> | null = null;
let currentConnections: IConnection[] = [];
let connectionsLoading = true;
let connectionsFailed = false;
let pageUnloading = false;
const followedSubscriptionJobs = new Set<string>();
const followedLatencyJobs = new Set<string>();
@@ -1596,13 +1594,9 @@ async function fetchConnections() {
currentConnections = Array.from(map.values()).sort(
(a, b) => b.download + b.upload - (a.download + a.upload),
);
connectionsLoading = false;
connectionsFailed = false;
} else {
connectionsFailed = true;
}
} catch (e) {
connectionsFailed = true;
} catch (_e) {
// Ignore connections fetch errors
}
renderConnectionsWidget();
}
@@ -1,6 +1,5 @@
import { TachyonShellMethods } from '../../../methods';
import { showToast } from '../../../../helpers/showToast';
import { Tachyon } from '../../../types';
import { getConfigSections } from '../../../methods/custom/getConfigSections';
import { TACHYON_UCI_PACKAGE } from '../../../../constants';
@@ -42,7 +42,6 @@ import {
import { TachyonShellMethods } from '../../methods';
import { fetchServicesInfo } from '../../fetchers/fetchServicesInfo';
import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion';
import { copyToClipboard } from '../../../helpers/copyToClipboard';
import { renderModal, renderButton } from '../../../partials';
import { TACHYON_LUCI_APP_VERSION } from '../../../constants';
import { renderWikiDisclaimer } from './partials/renderWikiDisclaimer';
@@ -785,7 +784,7 @@ function getAiDoctorHistory(): AiDoctorHistoryEntry[] {
try {
const raw = localStorage.getItem('tachyon_ai_doctor_history');
return raw ? (JSON.parse(raw) as AiDoctorHistoryEntry[]) : [];
} catch (e) {
} catch (_e) {
return [];
}
}
@@ -795,7 +794,7 @@ function saveAiDoctorHistory(entry: AiDoctorHistoryEntry) {
const current = getAiDoctorHistory();
const updated = [entry, ...current].slice(0, 5);
localStorage.setItem('tachyon_ai_doctor_history', JSON.stringify(updated));
} catch (e) {
} catch (_e) {
// Ignore storage errors
}
}
@@ -647,11 +647,12 @@ async function followComponentActionState(
const jobId = state.job_id;
const key = getComponentActionKey(state.component, state.action);
if (!jobId || !key || followedComponentJobs.has(jobId)) {
return;
}
if (!state.running && handledComponentJobs.has(jobId)) {
if (
!jobId ||
!key ||
followedComponentJobs.has(jobId) ||
handledComponentJobs.has(jobId)
) {
return;
}
@@ -714,12 +715,16 @@ async function followAlreadyRunningComponentAction(
(item) =>
item.running &&
item.component === button.component &&
item.action === button.action,
item.action === button.action &&
(!item.job_id || !handledComponentJobs.has(item.job_id)),
);
if (!state) {
state = uiState.actions.component.find(
(item) => item.running && item.component === button.component,
(item) =>
item.running &&
item.component === button.component &&
(!item.job_id || !handledComponentJobs.has(item.job_id)),
);
}
@@ -118,7 +118,6 @@ export function showUpdateProgressModal(
let elapsedSeconds = 0;
let timerInterval: ReturnType<typeof setInterval> | null = null;
let isCompleted = false;
let currentModalVersions = { ...options };
const timerBadgeEl = E(
@@ -149,21 +148,17 @@ export function showUpdateProgressModal(
class: 'tachyon-update-modal__log',
}) as HTMLPreElement;
const logPanelEl = E(
'div',
{ class: 'tachyon-update-modal__log-panel' },
[
E('div', { class: 'tachyon-update-modal__log-header' }, [
E('b', {}, _('Operation log')),
renderButton({
classNames: ['cbi-button-action', 'tachyon-update-modal__log-copy'],
text: _('Copy log'),
onClick: copyLog,
}),
]),
logPreEl,
],
);
const logPanelEl = E('div', { class: 'tachyon-update-modal__log-panel' }, [
E('div', { class: 'tachyon-update-modal__log-header' }, [
E('b', {}, _('Operation log')),
renderButton({
classNames: ['cbi-button-action', 'tachyon-update-modal__log-copy'],
text: _('Copy log'),
onClick: copyLog,
}),
]),
logPreEl,
]);
const actionButtonContainer = E(
'div',
@@ -312,7 +307,6 @@ export function showUpdateProgressModal(
titleBadgeEl.textContent = renderVersionBadgeText(currentModalVersions);
},
completeSuccess: (message?: string, opts?: CompleteSuccessOptions) => {
isCompleted = true;
cleanupTimers();
finishLogTracking();
@@ -400,7 +394,6 @@ export function showUpdateProgressModal(
}
},
completeError: (errorMessage: string) => {
isCompleted = true;
cleanupTimers();
finishLogTracking();
@@ -3587,6 +3587,11 @@ var TachyonShellMethods = {
const transientRpc = createTransientRpcGraceTracker(
COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS
);
const versionsMatch = (a, b) => {
const cleanA = a.replace(/^v/i, "").trim();
const cleanB = b.replace(/^v/i, "").trim();
return cleanA === cleanB;
};
const confirmedByVersion = async () => {
if (!isSelfUpdate) return "";
if (Date.now() - jobStartedAt < COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS) {
@@ -3594,10 +3599,10 @@ var TachyonShellMethods = {
}
const version = await readTachyonVersion();
if (!version) return "";
if (targetVersion && version === targetVersion) {
if (targetVersion && versionsMatch(version, targetVersion)) {
return version;
}
if (!targetVersion && baselineVersion && version !== baselineVersion) {
if (!targetVersion && baselineVersion && !versionsMatch(version, baselineVersion)) {
return version;
}
return "";
@@ -3607,7 +3612,7 @@ var TachyonShellMethods = {
return "";
}
const version = await readTachyonVersion();
if (version === baselineVersion && (!targetVersion || version === targetVersion)) {
if (versionsMatch(version, baselineVersion) && (!targetVersion || versionsMatch(version, targetVersion))) {
return version;
}
return "";
@@ -6459,8 +6464,6 @@ var dashboardDataUpdatesStarted = false;
var dashboardDataUpdatesId = 0;
var connectionsRefreshTimer = null;
var currentConnections = [];
var connectionsLoading = true;
var connectionsFailed = false;
var pageUnloading = false;
var followedSubscriptionJobs = /* @__PURE__ */ new Set();
var followedLatencyJobs = /* @__PURE__ */ new Set();
@@ -7625,13 +7628,8 @@ async function fetchConnections() {
currentConnections = Array.from(map.values()).sort(
(a, b) => b.download + b.upload - (a.download + a.upload)
);
connectionsLoading = false;
connectionsFailed = false;
} else {
connectionsFailed = true;
}
} catch (e) {
connectionsFailed = true;
} catch (_e) {
}
renderConnectionsWidget();
}
@@ -12167,7 +12165,7 @@ function getAiDoctorHistory() {
try {
const raw = localStorage.getItem("tachyon_ai_doctor_history");
return raw ? JSON.parse(raw) : [];
} catch (e) {
} catch (_e) {
return [];
}
}
@@ -12176,7 +12174,7 @@ function saveAiDoctorHistory(entry) {
const current = getAiDoctorHistory();
const updated = [entry, ...current].slice(0, 5);
localStorage.setItem("tachyon_ai_doctor_history", JSON.stringify(updated));
} catch (e) {
} catch (_e) {
}
}
async function handleViewLogs() {
@@ -15323,7 +15321,6 @@ function showUpdateProgressModal(options) {
}
let elapsedSeconds = 0;
let timerInterval = null;
let isCompleted = false;
let currentModalVersions = { ...options };
const timerBadgeEl = E(
"div",
@@ -15349,21 +15346,17 @@ function showUpdateProgressModal(options) {
const logPreEl = E("pre", {
class: "tachyon-update-modal__log"
});
const logPanelEl = E(
"div",
{ class: "tachyon-update-modal__log-panel" },
[
E("div", { class: "tachyon-update-modal__log-header" }, [
E("b", {}, _("Operation log")),
renderButton({
classNames: ["cbi-button-action", "tachyon-update-modal__log-copy"],
text: _("Copy log"),
onClick: copyLog
})
]),
logPreEl
]
);
const logPanelEl = E("div", { class: "tachyon-update-modal__log-panel" }, [
E("div", { class: "tachyon-update-modal__log-header" }, [
E("b", {}, _("Operation log")),
renderButton({
classNames: ["cbi-button-action", "tachyon-update-modal__log-copy"],
text: _("Copy log"),
onClick: copyLog
})
]),
logPreEl
]);
const actionButtonContainer = E(
"div",
{ class: "tachyon-update-modal__actions" },
@@ -15484,7 +15477,6 @@ function showUpdateProgressModal(options) {
titleBadgeEl.textContent = renderVersionBadgeText(currentModalVersions);
},
completeSuccess: (message, opts) => {
isCompleted = true;
cleanupTimers();
finishLogTracking();
const successMsg = message || (isCheckAction ? _("Check completed!") : options.action === "remove" ? _("Removal completed successfully!") : _("Update completed successfully!"));
@@ -15548,7 +15540,6 @@ function showUpdateProgressModal(options) {
}
},
completeError: (errorMessage) => {
isCompleted = true;
cleanupTimers();
finishLogTracking();
actionButtonContainer.replaceChildren(
@@ -16015,10 +16006,7 @@ async function completeComponentActionJob(key, jobId, response) {
async function followComponentActionState(state) {
const jobId = state.job_id;
const key = getComponentActionKey(state.component, state.action);
if (!jobId || !key || followedComponentJobs.has(jobId)) {
return;
}
if (!state.running && handledComponentJobs.has(jobId)) {
if (!jobId || !key || followedComponentJobs.has(jobId) || handledComponentJobs.has(jobId)) {
return;
}
followedComponentJobs.add(jobId);
@@ -16067,11 +16055,11 @@ async function followAlreadyRunningComponentAction(button) {
return false;
}
let state = uiState.actions.component.find(
(item) => item.running && item.component === button.component && item.action === button.action
(item) => item.running && item.component === button.component && item.action === button.action && (!item.job_id || !handledComponentJobs.has(item.job_id))
);
if (!state) {
state = uiState.actions.component.find(
(item) => item.running && item.component === button.component
(item) => item.running && item.component === button.component && (!item.job_id || !handledComponentJobs.has(item.job_id))
);
}
if (!state) {
@@ -732,13 +732,31 @@ function controller(bus, opts) {
}
if (setting("recovery_bypass", "0") == "1") return;
let proto = uci_core.get("network", "wan", "proto") || "pppoe";
let device = trim(uci_core.get("network", "wan", "device") || "eth0");
let proto = uci_core.get("network", "wan", "proto") || "dhcp";
let device = trim(setting("output_network_interface", "") ||
uci_core.get("network", "wan", "device") ||
uci_core.get("network", "wan", "ifname") || "");
if (device == "") {
let def_route = command_capture("ip -4 route show default 2>/dev/null").output;
let m_dev = match(def_route, /dev\s+([^\s]+)/);
if (m_dev) device = m_dev[1];
}
if (device == "") device = "eth0";
let iface = (proto == "pppoe") ? "pppoe-wan" : device;
let addr_out = command_capture("ip addr show " + shell_quote(iface) + " 2>/dev/null").output;
let no_address = index(addr_out, "inet ") < 0;
if (no_address) {
let ubus_data = command_output_from_args([ "ubus", "-S", "call", "network.interface.wan", "status" ]);
try {
let ubus_obj = json(ubus_data);
if (type(ubus_obj) == "object" && ubus_obj.up && length(common.array_or_empty(ubus_obj["ipv4-address"])) > 0) {
no_address = false;
}
} catch (e) {}
}
let route_out = command_capture("ip route 2>/dev/null").output;
let no_gateway = index(route_out, "default") < 0;
+2 -5
View File
@@ -723,16 +723,12 @@ function refresh_pid_job_state(path, stale_message) {
return;
let now = now_seconds();
let updated_at = arg_number(value.updated_at || value.started_at);
let pid = as_string(value.pid || "");
if (job_pid_valid(pid) && pid_running(pid))
return;
if (updated_at > 0 && (now - updated_at) < 180)
return;
let within_grace = job_started_at_within_grace(value.started_at, now, 600);
let within_grace = job_started_at_within_grace(value.started_at, now, int(ACTION_STALE_GRACE_SECONDS));
if (!within_grace)
write_stale_action_state(path, stale_message);
}
@@ -812,6 +808,7 @@ function action_state_from_dir(dir) {
}
function action_state_from_dirs() {
refresh_action_dirs();
return {
service: action_state_from_dir(SERVICE_ACTION_DIR),
latency: action_state_from_dir(LATENCY_ACTION_DIR),
BIN
View File
Binary file not shown.