fix(updates): предотвратить повторное открытие и зависание модального окна обновления
- Исключить повторное открытие окна прогресса для уже завершенных задач (handledComponentJobs) - Актуализировать состояние фоновых задач в get_ui_state и нормализовать таймаут stale PID - Исправить определение WAN интерфейса в event_controller во избежание ложного перезапуска (#31) - Улучшить сверку версий при самообновлении
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user