fix(updates): resolve component update hang and multiple page reload loop

- action.uc: sweep stuck flock 1000 processes before tachyon restart, bound service restart with 25s timeout, and prevent worker lockup
- shell/index.ts: add 15-minute general hard timeout for non-Tachyon components to prevent infinite modal spin; allow same-version confirmation for both install and reinstall
- package.uc: remove disruptive uhttpd restart from luci_postinst to prevent active socket drops
- ui.uc: refresh stale PID state before checking value.running in action_ack so dead jobs can always be acknowledged
- initController.ts & sessionJobs.ts: persist handled job IDs across reloads in sessionStorage, eliminate redundant modal popups on page reload, and guard against concurrent reloads with safeReloadPage
- renderUpdateProgressModal.ts: record active job before reload and poll readiness with fast health check instead of blind 5s freeze
This commit is contained in:
Dushnilin
2026-09-03 07:36:30 +03:00
parent 36fe7ef312
commit abaa891dd3
12 changed files with 417 additions and 119 deletions
@@ -17,6 +17,7 @@ const COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS = 2000;
const COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS = 30000;
const COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS = 2000;
const COMPONENT_ACTION_SELF_UPDATE_HARD_TIMEOUT_MS = 120000;
const COMPONENT_ACTION_GENERAL_HARD_TIMEOUT_MS = 15 * 60 * 1000;
const COMPONENT_ACTION_STATE_DIR = '/var/run/tachyon/component-actions';
const GET_UI_STATE_RPC_TIMEOUT_MS = 3000;
@@ -736,16 +737,16 @@ export const TachyonShellMethods = {
// A reinstall of the same version never changes the version string, so
// version-based confirmation can never match. Equality with the baseline
// is the confirmation — but only once the job is no longer running and only
// for explicit reinstall actions.
// when target version matches baseline (or is unspecified).
const confirmedSameVersionReinstall = async () => {
if (!isSelfUpdate || action !== 'reinstall' || !baselineVersion) {
if (!isSelfUpdate || !baselineVersion) {
return '';
}
if (targetVersion && !versionsMatch(targetVersion, baselineVersion)) {
return '';
}
const version = await readTachyonVersion();
if (versionsMatch(version, baselineVersion)) {
if (version && versionsMatch(version, baselineVersion)) {
return version;
}
return '';
@@ -769,7 +770,7 @@ export const TachyonShellMethods = {
if ((await confirmedByVersion()) === version) {
return true;
}
} else if (action === 'reinstall') {
} else {
if ((await confirmedSameVersionReinstall()) === version) {
return true;
}
@@ -825,13 +826,22 @@ export const TachyonShellMethods = {
return selfUpdateResult(version || baselineVersion);
}
if (
!isSelfUpdate &&
Date.now() - jobStartedAt >= COMPONENT_ACTION_GENERAL_HARD_TIMEOUT_MS
) {
if (stateResponse && !stateResponse.running) {
return jobDoneResult(stateResponse);
}
return {
success: false,
error: _('Component action timed out'),
} as Tachyon.MethodFailureResponse;
}
// The job is over according to the state file.
if (stateResponse && !stateResponse.running) {
if (
isSelfUpdate &&
stateResponse.success === false &&
(isDifferentVersion || action === 'reinstall')
) {
if (isSelfUpdate && stateResponse.success === false) {
const version =
(await confirmedByVersion()) ||
(await confirmedSameVersionReinstall());
@@ -894,11 +904,7 @@ export const TachyonShellMethods = {
continue;
}
if (
isSelfUpdate &&
parsedResponse.success === false &&
(isDifferentVersion || action === 'reinstall')
) {
if (isSelfUpdate && parsedResponse.success === false) {
const version =
(await confirmedByVersion()) ||
(await confirmedSameVersionReinstall());
@@ -626,6 +626,67 @@ describe('TachyonShellMethods.componentAction', () => {
});
});
it('completes a same-version Tachyon install as success when target version matches baseline and job is stale', async () => {
mocks.fsRead.mockResolvedValue(
JSON.stringify({
success: false,
running: false,
component: 'tachyon',
action: 'install',
message:
'Component action job is stale or the worker process exited unexpectedly',
job_id: 'job-1',
}),
);
mocks.executeShellCommand.mockImplementation(({ args }) => {
if (args[0] === 'component_action_status') {
return Promise.resolve({
stdout: '',
stderr: 'Unknown command',
code: 1,
});
}
if (args[0] === 'show_version') {
return Promise.resolve({
stdout: '1.2.77\n',
stderr: '',
code: 0,
});
}
return Promise.resolve({
stdout: '',
stderr: 'Unexpected command',
code: 1,
});
});
const responsePromise = TachyonShellMethods.waitComponentActionJob(
'job-1',
'tachyon',
'install',
'1.2.77',
);
await vi.advanceTimersByTimeAsync(5000);
await expect(responsePromise).resolves.toEqual({
success: true,
data: {
success: true,
component: 'tachyon',
action: 'install',
message: 'Tachyon has been installed',
current_version: '1.2.77',
latest_version: '1.2.77',
changed: true,
status: 'latest',
},
});
});
it('closes the modal with success when a self-update never confirms and the hard timeout is reached', async () => {
mocks.fsRead.mockRejectedValue(new Error('Access denied'));
mocks.executeShellCommand.mockImplementation(({ args }) => {
@@ -49,6 +49,11 @@ import {
setActiveProgressModalJobId,
showUpdateProgressModal,
} from './partials/renderUpdateProgressModal';
import {
loadHandledJobsFromSession,
safeReloadPage,
saveHandledJobToSession,
} from './sessionJobs';
function getComponentCardTitle(component: Tachyon.ComponentName): string {
switch (component) {
@@ -128,18 +133,11 @@ let componentActionStateRefreshPromise: Promise<void> | null = null;
const followedComponentJobs = new Set<string>();
const handledComponentJobs = new Set<string>();
try {
const savedJob = sessionStorage.getItem('tachyon_post_update_job');
if (savedJob) {
for (const savedJob of loadHandledJobsFromSession()) {
handledComponentJobs.add(savedJob);
capSetSize(handledComponentJobs);
followedComponentJobs.add(savedJob);
sessionStorage.removeItem('tachyon_post_update_job');
}
} catch (_e) {
// sessionStorage may be unavailable (private mode); the saved job id is
// best-effort and its absence is fine.
}
capSetSize(handledComponentJobs);
if (typeof window !== 'undefined') {
window.addEventListener('pagehide', () => {
@@ -405,12 +403,15 @@ async function waitForTachyonResponsive() {
return false;
}
function reloadPageAfterTachyonUpdate() {
function reloadPageAfterTachyonUpdate(jobId?: string) {
if (jobId) {
saveHandledJobToSession(jobId);
}
// Reload only after the restarted backend actually answers get_ui_state:
// a fixed delay either reloaded too early (tab hung on stale component
// action state) or wasted seconds on fast routers.
void waitForTachyonResponsive().finally(() => {
window.location.reload();
safeReloadPage();
});
}
@@ -620,7 +621,7 @@ async function applyCompletedComponentAction({
if (modalController) {
modalController.completeSuccess(result.message, { reloadPage: true });
} else {
reloadPageAfterTachyonUpdate();
reloadPageAfterTachyonUpdate(result.job_id);
}
} else {
modalController?.completeSuccess(result.message);
@@ -669,6 +670,7 @@ async function completeComponentActionJob(
handledComponentJobs.add(jobId);
capSetSize(handledComponentJobs);
saveHandledJobToSession(jobId);
setActionLoading(key, false);
if (shouldNotify) {
showToast(message, 'error');
@@ -680,6 +682,7 @@ async function completeComponentActionJob(
handledComponentJobs.add(jobId);
capSetSize(handledComponentJobs);
saveHandledJobToSession(jobId);
await ackComponentActionJob(jobId);
await applyCompletedComponentAction({
key,
@@ -808,6 +811,7 @@ function handleComponentUiState(uiState: Tachyon.UiState) {
} else {
handledComponentJobs.add(jobId);
capSetSize(handledComponentJobs);
saveHandledJobToSession(jobId);
void ackComponentActionJob(jobId);
}
}
@@ -3,6 +3,11 @@ import { renderButton } from '../../../../partials';
import { copyToClipboard } from '../../../../helpers/copyToClipboard';
import { TachyonShellMethods } from '../../../methods';
import { Tachyon } from '../../../types';
import {
isReloadInProgress,
safeReloadPage,
saveHandledJobToSession,
} from '../sessionJobs';
export interface UpdateProgressModalOptions {
component: Tachyon.ComponentName;
@@ -349,7 +354,10 @@ export function showUpdateProgressModal(
text: _('Reload now'),
onClick: () => {
reloadProbeActive = false;
window.location.reload();
if (activeModalJobId) {
saveHandledJobToSession(activeModalJobId);
}
safeReloadPage();
},
});
@@ -362,51 +370,31 @@ export function showUpdateProgressModal(
);
const probeAndReload = async () => {
// Pause 2 seconds initially to allow init scripts and rpcd reload to commence
await new Promise((resolve) => setTimeout(resolve, 2000));
// Pause 1.5s initially to allow init scripts and rpcd reload to commence
await new Promise((resolve) => setTimeout(resolve, 1500));
while (reloadProbeActive && attempt < maxAttempts) {
while (
reloadProbeActive &&
attempt < maxAttempts &&
!isReloadInProgress()
) {
attempt += 1;
statusEl.textContent = `${_('Verifying router and service readiness...')} (${attempt}/${maxAttempts})`;
try {
const res = await TachyonShellMethods.getSystemInfo();
if (res && res.success) {
statusEl.textContent = _(
'Services are online, waiting for full initialization...',
);
// Wait for the service to fully stabilize after the first
// successful probe; rpcd can come up before tachyon init finishes.
await new Promise((resolve) => setTimeout(resolve, 5000));
if (!reloadProbeActive) return;
// Confirm services are still up after the settle window
try {
const confirm = await TachyonShellMethods.getSystemInfo();
if (confirm && confirm.success) {
statusEl.textContent = _(
'Services are online and ready! Reloading page...',
);
reloadBtn.textContent = _('Reloading...');
await new Promise((resolve) => setTimeout(resolve, 600));
if (activeModalJobId) {
try {
sessionStorage.setItem(
'tachyon_post_update_job',
activeModalJobId,
);
} catch (_e) {
// sessionStorage may be unavailable; cleanup is
// best-effort.
saveHandledJobToSession(activeModalJobId);
}
}
window.location.reload();
await new Promise((resolve) => setTimeout(resolve, 800));
safeReloadPage();
return;
}
} catch (_confirmErr) {
// Services went away during settle — keep probing
}
}
} catch (_err) {
// RPC is unavailable while daemon/rpcd is reloading; keep probing
}
@@ -414,7 +402,7 @@ export function showUpdateProgressModal(
await new Promise((resolve) => setTimeout(resolve, 1500));
}
if (reloadProbeActive) {
if (reloadProbeActive && !isReloadInProgress()) {
statusEl.textContent = _(
'Services restarted. Click to reload page.',
);
@@ -0,0 +1,65 @@
const HANDLED_JOBS_STORAGE_KEY = 'tachyon_handled_component_jobs';
const LEGACY_JOB_STORAGE_KEY = 'tachyon_post_update_job';
let isPageReloading = false;
export function safeReloadPage(): void {
if (isPageReloading) {
return;
}
isPageReloading = true;
if (typeof window !== 'undefined' && window.location) {
window.location.reload();
}
}
export function isReloadInProgress(): boolean {
return isPageReloading;
}
export function saveHandledJobToSession(jobId: string): void {
if (!jobId || typeof sessionStorage === 'undefined') {
return;
}
try {
const raw = sessionStorage.getItem(HANDLED_JOBS_STORAGE_KEY);
const list: string[] = raw ? JSON.parse(raw) : [];
if (!list.includes(jobId)) {
list.push(jobId);
if (list.length > 30) {
list.shift();
}
sessionStorage.setItem(HANDLED_JOBS_STORAGE_KEY, JSON.stringify(list));
}
sessionStorage.setItem(LEGACY_JOB_STORAGE_KEY, jobId);
} catch (_e) {
// Ignore private mode or quota errors
}
}
export function loadHandledJobsFromSession(): string[] {
if (typeof sessionStorage === 'undefined') {
return [];
}
const result = new Set<string>();
try {
const raw = sessionStorage.getItem(HANDLED_JOBS_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
for (const id of parsed) {
if (typeof id === 'string' && id) {
result.add(id);
}
}
}
}
const legacy = sessionStorage.getItem(LEGACY_JOB_STORAGE_KEY);
if (legacy) {
result.add(legacy);
}
} catch (_e) {
// Ignore private mode errors
}
return Array.from(result);
}
@@ -0,0 +1,69 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
isReloadInProgress,
loadHandledJobsFromSession,
safeReloadPage,
saveHandledJobToSession,
} from '../sessionJobs';
describe('sessionJobs', () => {
let storage: Record<string, string> = {};
beforeEach(() => {
storage = {};
vi.stubGlobal('sessionStorage', {
getItem: vi.fn((key: string) => storage[key] || null),
setItem: vi.fn((key: string, value: string) => {
storage[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete storage[key];
}),
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('saves and loads handled jobs from session storage', () => {
saveHandledJobToSession('job-1');
saveHandledJobToSession('job-2');
const loaded = loadHandledJobsFromSession();
expect(loaded).toContain('job-1');
expect(loaded).toContain('job-2');
});
it('loads legacy tachyon_post_update_job if present', () => {
storage['tachyon_post_update_job'] = 'legacy-job-99';
const loaded = loadHandledJobsFromSession();
expect(loaded).toContain('legacy-job-99');
});
it('does not duplicate job IDs in session storage', () => {
saveHandledJobToSession('job-dup');
saveHandledJobToSession('job-dup');
const loaded = loadHandledJobsFromSession();
expect(loaded.filter((id) => id === 'job-dup').length).toBe(1);
});
it('safeReloadPage calls window.location.reload only once', () => {
const reloadMock = vi.fn();
vi.stubGlobal('window', {
location: {
reload: reloadMock,
},
});
safeReloadPage();
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(isReloadInProgress()).toBe(true);
// Second call should be a no-op
safeReloadPage();
expect(reloadMock).toHaveBeenCalledTimes(1);
});
});
@@ -3258,6 +3258,7 @@ var COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS = 2e3;
var COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS = 3e4;
var COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS = 2e3;
var COMPONENT_ACTION_SELF_UPDATE_HARD_TIMEOUT_MS = 12e4;
var COMPONENT_ACTION_GENERAL_HARD_TIMEOUT_MS = 15 * 60 * 1e3;
var COMPONENT_ACTION_STATE_DIR = "/var/run/tachyon/component-actions";
var GET_UI_STATE_RPC_TIMEOUT_MS = 3e3;
function sleep(ms) {
@@ -3780,14 +3781,14 @@ var TachyonShellMethods = {
return "";
};
const confirmedSameVersionReinstall = async () => {
if (!isSelfUpdate || action !== "reinstall" || !baselineVersion) {
if (!isSelfUpdate || !baselineVersion) {
return "";
}
if (targetVersion && !versionsMatch(targetVersion, baselineVersion)) {
return "";
}
const version = await readTachyonVersion();
if (versionsMatch(version, baselineVersion)) {
if (version && versionsMatch(version, baselineVersion)) {
return version;
}
return "";
@@ -3804,7 +3805,7 @@ var TachyonShellMethods = {
if (await confirmedByVersion() === version) {
return true;
}
} else if (action === "reinstall") {
} else {
if (await confirmedSameVersionReinstall() === version) {
return true;
}
@@ -3845,8 +3846,17 @@ var TachyonShellMethods = {
}
return selfUpdateResult(version || baselineVersion);
}
if (!isSelfUpdate && Date.now() - jobStartedAt >= COMPONENT_ACTION_GENERAL_HARD_TIMEOUT_MS) {
if (stateResponse && !stateResponse.running) {
if (isSelfUpdate && stateResponse.success === false && (isDifferentVersion || action === "reinstall")) {
return jobDoneResult(stateResponse);
}
return {
success: false,
error: _("Component action timed out")
};
}
if (stateResponse && !stateResponse.running) {
if (isSelfUpdate && stateResponse.success === false) {
const version = await confirmedByVersion() || await confirmedSameVersionReinstall();
if (version) {
if (await settleVersion(version)) {
@@ -3893,7 +3903,7 @@ var TachyonShellMethods = {
if (parsedResponse.running) {
continue;
}
if (isSelfUpdate && parsedResponse.success === false && (isDifferentVersion || action === "reinstall")) {
if (isSelfUpdate && parsedResponse.success === false) {
const version = await confirmedByVersion() || await confirmedSameVersionReinstall();
if (version) {
if (await settleVersion(version)) {
@@ -18778,6 +18788,66 @@ function describeSameReleaseBuild({
return { kind: "fallback", text: "" };
}
// src/tachyon/tabs/updates/sessionJobs.ts
var HANDLED_JOBS_STORAGE_KEY = "tachyon_handled_component_jobs";
var LEGACY_JOB_STORAGE_KEY = "tachyon_post_update_job";
var isPageReloading = false;
function safeReloadPage() {
if (isPageReloading) {
return;
}
isPageReloading = true;
if (typeof window !== "undefined" && window.location) {
window.location.reload();
}
}
function isReloadInProgress() {
return isPageReloading;
}
function saveHandledJobToSession(jobId) {
if (!jobId || typeof sessionStorage === "undefined") {
return;
}
try {
const raw = sessionStorage.getItem(HANDLED_JOBS_STORAGE_KEY);
const list = raw ? JSON.parse(raw) : [];
if (!list.includes(jobId)) {
list.push(jobId);
if (list.length > 30) {
list.shift();
}
sessionStorage.setItem(HANDLED_JOBS_STORAGE_KEY, JSON.stringify(list));
}
sessionStorage.setItem(LEGACY_JOB_STORAGE_KEY, jobId);
} catch (_e) {
}
}
function loadHandledJobsFromSession() {
if (typeof sessionStorage === "undefined") {
return [];
}
const result = /* @__PURE__ */ new Set();
try {
const raw = sessionStorage.getItem(HANDLED_JOBS_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
for (const id of parsed) {
if (typeof id === "string" && id) {
result.add(id);
}
}
}
}
const legacy = sessionStorage.getItem(LEGACY_JOB_STORAGE_KEY);
if (legacy) {
result.add(legacy);
}
} catch (_e) {
}
return Array.from(result);
}
// src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts
function formatSha(sha) {
if (!sha) return "";
@@ -19020,7 +19090,10 @@ function showUpdateProgressModal(options) {
text: _("Reload now"),
onClick: () => {
reloadProbeActive = false;
window.location.reload();
if (activeModalJobId) {
saveHandledJobToSession(activeModalJobId);
}
safeReloadPage();
}
});
actionButtonContainer.replaceChildren(
@@ -19031,46 +19104,29 @@ function showUpdateProgressModal(options) {
])
);
const probeAndReload = async () => {
await new Promise((resolve) => setTimeout(resolve, 2e3));
while (reloadProbeActive && attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 1500));
while (reloadProbeActive && attempt < maxAttempts && !isReloadInProgress()) {
attempt += 1;
statusEl.textContent = `${_("Verifying router and service readiness...")} (${attempt}/${maxAttempts})`;
try {
const res = await TachyonShellMethods.getSystemInfo();
if (res && res.success) {
statusEl.textContent = _(
"Services are online, waiting for full initialization..."
);
await new Promise((resolve) => setTimeout(resolve, 5e3));
if (!reloadProbeActive) return;
try {
const confirm = await TachyonShellMethods.getSystemInfo();
if (confirm && confirm.success) {
statusEl.textContent = _(
"Services are online and ready! Reloading page..."
);
reloadBtn.textContent = _("Reloading...");
await new Promise((resolve) => setTimeout(resolve, 600));
if (activeModalJobId) {
try {
sessionStorage.setItem(
"tachyon_post_update_job",
activeModalJobId
);
} catch (_e) {
saveHandledJobToSession(activeModalJobId);
}
}
window.location.reload();
await new Promise((resolve) => setTimeout(resolve, 800));
safeReloadPage();
return;
}
} catch (_confirmErr) {
}
}
} catch (_err) {
}
await new Promise((resolve) => setTimeout(resolve, 1500));
}
if (reloadProbeActive) {
if (reloadProbeActive && !isReloadInProgress()) {
statusEl.textContent = _(
"Services restarted. Click to reload page."
);
@@ -19221,16 +19277,11 @@ var componentActionStateUnsubscribe = null;
var componentActionStateRefreshPromise = null;
var followedComponentJobs = /* @__PURE__ */ new Set();
var handledComponentJobs = /* @__PURE__ */ new Set();
try {
const savedJob = sessionStorage.getItem("tachyon_post_update_job");
if (savedJob) {
for (const savedJob of loadHandledJobsFromSession()) {
handledComponentJobs.add(savedJob);
capSetSize(handledComponentJobs);
followedComponentJobs.add(savedJob);
sessionStorage.removeItem("tachyon_post_update_job");
}
} catch (_e) {
}
capSetSize(handledComponentJobs);
if (typeof window !== "undefined") {
window.addEventListener("pagehide", () => {
pageUnloading2 = true;
@@ -19414,9 +19465,12 @@ async function waitForTachyonResponsive() {
}
return false;
}
function reloadPageAfterTachyonUpdate() {
function reloadPageAfterTachyonUpdate(jobId) {
if (jobId) {
saveHandledJobToSession(jobId);
}
void waitForTachyonResponsive().finally(() => {
window.location.reload();
safeReloadPage();
});
}
function patchSystemInfoAfterMutation(result) {
@@ -19570,7 +19624,7 @@ async function applyCompletedComponentAction({
if (modalController) {
modalController.completeSuccess(result.message, { reloadPage: true });
} else {
reloadPageAfterTachyonUpdate();
reloadPageAfterTachyonUpdate(result.job_id);
}
} else {
modalController?.completeSuccess(result.message);
@@ -19604,6 +19658,7 @@ async function completeComponentActionJob(key, jobId, response) {
}
handledComponentJobs.add(jobId);
capSetSize(handledComponentJobs);
saveHandledJobToSession(jobId);
setActionLoading(key, false);
if (shouldNotify) {
showToast(message, "error");
@@ -19614,6 +19669,7 @@ async function completeComponentActionJob(key, jobId, response) {
}
handledComponentJobs.add(jobId);
capSetSize(handledComponentJobs);
saveHandledJobToSession(jobId);
await ackComponentActionJob(jobId);
await applyCompletedComponentAction({
key,
@@ -19708,6 +19764,7 @@ function handleComponentUiState(uiState) {
} else {
handledComponentJobs.add(jobId);
capSetSize(handledComponentJobs);
saveHandledJobToSession(jobId);
void ackComponentActionJob(jobId);
}
}
+8 -6
View File
@@ -41,6 +41,8 @@ let command_output = common.command_output;
let command_output_from_args = common.command_output_from_args;
let write_json = common.write_json;
let write_file = common.write_file;
let bounded_command = common.bounded_command;
let kill_matching_command = common.kill_matching_command;
let tmp_dir = "";
let lock_held = false;
@@ -337,14 +339,15 @@ function action_fail(component, action, message, current_version, latest_version
exit(1);
}
function run_logged(description, command) {
function run_logged(description, command, timeout_seconds) {
init_tmp_dir();
let output_file = make_tmp_file("command");
if (output_file == "")
output_file = "/tmp/tachyon-updates-command." + owner_pid();
updates_log(description);
let status = command_status(as_string(command) + " >" + shell_quote(output_file) + " 2>&1");
let run_cmd = timeout_seconds ? bounded_command(command, timeout_seconds) : as_string(command);
let status = command_status(run_cmd + " >" + shell_quote(output_file) + " 2>&1");
for (let line in split(read_file(output_file), "\n"))
if (trim(as_string(line)) != "")
updates_log(line);
@@ -939,12 +942,11 @@ function restart_tachyon_after_successful_change() {
prepare_sing_box_service_disabled();
return;
}
// Clear any stuck flock holders or pending rc.common waits
system(kill_matching_command("-E '99-tachyon-wan|flock 1000|init[.]d/tachyon'"));
// Kill orphaned logread -f processes before restart to prevent FD cascade.
// action.uc runs as a child of the old watchdog and inherits its logread pipe FD.
// Without this, the old logread process may keep the pipe alive across the restart,
// causing the new watchdog's config generator to inherit stale FDs and hit the 1024 limit.
command_success_from_args([ "killall", "logread" ]);
run_logged("Restarting Tachyon after successful component change", command_from_args([ SERVICE_INIT, "restart" ]));
run_logged("Restarting Tachyon after successful component change", command_from_args([ SERVICE_INIT, "restart" ]), 25);
}
function stop_tachyon_before_sing_box_change() {
+40 -1
View File
@@ -447,6 +447,42 @@ function command_output_from_args(args) {
return command_output(command_from_args(args) + " 2>/dev/null");
}
let timeout_prefix_cache = null;
function timeout_prefix() {
if (timeout_prefix_cache != null)
return timeout_prefix_cache;
if (command_status("timeout 1 /bin/true >/dev/null 2>&1") == 0)
timeout_prefix_cache = [ "timeout" ];
else if (command_status("timeout -t 1 /bin/true >/dev/null 2>&1") == 0)
timeout_prefix_cache = [ "timeout", "-t" ];
else
timeout_prefix_cache = [];
return timeout_prefix_cache;
}
function bounded_command(command, seconds) {
seconds = as_string(seconds || "30");
let prefix = timeout_prefix();
if (length(prefix) == 0)
return as_string(command);
return join(" ", prefix) + " " + seconds + " " + as_string(command);
}
function kill_matching_command(grep_args) {
return "__anc=\" \"; __p=$$; " +
"while [ -n \"$__p\" ] && [ \"$__p\" -gt 1 ] 2>/dev/null; do " +
"__anc=\"$__anc$__p \"; " +
"__p=$(awk '{ sub(/.*\\) /, \"\"); print $2 }' \"/proc/$__p/stat\" 2>/dev/null); " +
"done; " +
"ps 2>/dev/null | grep " + grep_args + " | grep -v grep | grep -v -E 'action[.]uc|updates[.]uc' | awk '{print $1}' | " +
"while read _pid; do case \"$__anc\" in *\" $_pid \"*) continue;; esac; " +
"kill -9 \"$_pid\" 2>/dev/null; done; true";
}
function ensure_dir(path) {
path = as_string(path);
if (path == "") return false;
@@ -559,5 +595,8 @@ return {
write_file,
file_exists,
parent_dir,
get_mixed_port
get_mixed_port,
timeout_prefix,
bounded_command,
kill_matching_command
};
-2
View File
@@ -299,8 +299,6 @@ function luci_postinst() {
if (!PACKAGE_TEST_MODE) {
if (path_exists("/etc/init.d/rpcd"))
command_success_from_args([ "/etc/init.d/rpcd", "restart" ]);
if (path_exists("/etc/init.d/uhttpd"))
command_success_from_args([ "/etc/init.d/uhttpd", "restart" ]);
command_success_from_args([ "logger", "-t", "tachyon", "[info] Package defaults applied" ]);
}
return true;
+9
View File
@@ -1470,7 +1470,16 @@ function action_ack(kind, job_id_value) {
return;
}
refresh_pid_job_state(path, "UI action worker exited unexpectedly");
let value = read_json_file(path);
if (type(value) == "object" && value.running === true) {
let pid = as_string(value.pid || "");
if (!job_pid_valid(pid) || !pid_running(pid)) {
write_stale_action_state(path, "UI action worker is no longer running");
value = read_json_file(path);
}
}
if (type(value) == "object" && value.running === true) {
action_start_response(false, job_id_value, "UI action is still running");
exit(1);