fix(updates): prevent premature self-update completion while background job is running
This commit is contained in:
@@ -702,10 +702,14 @@ export const TachyonShellMethods = {
|
|||||||
return cleanA === cleanB;
|
return cleanA === cleanB;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDifferentVersion = Boolean(
|
||||||
|
targetVersion && !versionsMatch(targetVersion, baselineVersion),
|
||||||
|
);
|
||||||
|
|
||||||
// A self-update restarts the service mid-worker: the worker can die
|
// A self-update restarts the service mid-worker: the worker can die
|
||||||
// before writing its result, the state file can stay "running" forever
|
// before writing its result, the state file can stay "running" forever
|
||||||
// (recycled pid), and RPC can go away during the swap. The installed
|
// (recycled pid), and RPC can go away during the swap. The installed
|
||||||
// version is the only reliable ground truth.
|
// version is the only reliable ground truth when upgrading across versions.
|
||||||
const confirmedByVersion = async () => {
|
const confirmedByVersion = async () => {
|
||||||
if (!isSelfUpdate) return '';
|
if (!isSelfUpdate) return '';
|
||||||
if (
|
if (
|
||||||
@@ -716,7 +720,7 @@ export const TachyonShellMethods = {
|
|||||||
}
|
}
|
||||||
const version = await readTachyonVersion();
|
const version = await readTachyonVersion();
|
||||||
if (!version) return '';
|
if (!version) return '';
|
||||||
if (targetVersion && versionsMatch(version, targetVersion)) {
|
if (isDifferentVersion && versionsMatch(version, targetVersion)) {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -733,14 +737,14 @@ export const TachyonShellMethods = {
|
|||||||
// version-based confirmation can never match. Equality with the baseline
|
// version-based confirmation can never match. Equality with the baseline
|
||||||
// is the confirmation — but only once the job is no longer running.
|
// is the confirmation — but only once the job is no longer running.
|
||||||
const confirmedSameVersionReinstall = async () => {
|
const confirmedSameVersionReinstall = async () => {
|
||||||
if (!isSelfUpdate || action !== 'reinstall' || !baselineVersion) {
|
if (!isSelfUpdate || !baselineVersion) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (targetVersion && !versionsMatch(targetVersion, baselineVersion)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const version = await readTachyonVersion();
|
const version = await readTachyonVersion();
|
||||||
if (
|
if (versionsMatch(version, baselineVersion)) {
|
||||||
versionsMatch(version, baselineVersion) &&
|
|
||||||
(!targetVersion || versionsMatch(version, targetVersion))
|
|
||||||
) {
|
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@@ -760,14 +764,14 @@ export const TachyonShellMethods = {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((await confirmedByVersion()) === version) {
|
if (isDifferentVersion) {
|
||||||
return true;
|
if ((await confirmedByVersion()) === version) {
|
||||||
}
|
return true;
|
||||||
if (
|
}
|
||||||
version === baselineVersion &&
|
} else {
|
||||||
(await confirmedSameVersionReinstall()) === version
|
if ((await confirmedSameVersionReinstall()) === version) {
|
||||||
) {
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
selfUpdateVersionMatchedAt = 0;
|
selfUpdateVersionMatchedAt = 0;
|
||||||
return false;
|
return false;
|
||||||
@@ -845,23 +849,28 @@ export const TachyonShellMethods = {
|
|||||||
|
|
||||||
if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) {
|
if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) {
|
||||||
if (isSelfUpdate) {
|
if (isSelfUpdate) {
|
||||||
const version =
|
const version = await confirmedByVersion();
|
||||||
(await confirmedByVersion()) ||
|
|
||||||
(await confirmedSameVersionReinstall());
|
|
||||||
if (version) {
|
if (version) {
|
||||||
if (await settleVersion(version)) {
|
if (await settleVersion(version)) {
|
||||||
return selfUpdateResult(version);
|
return selfUpdateResult(version);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A self-update replaces the package mid-flight: RPC failures are
|
|
||||||
// expected during the swap window and must not fail the operation.
|
const failure = componentActionFailure(statusResponse, parsedResponse);
|
||||||
// The version confirmation above is the only exit that matters.
|
if (transientRpc.shouldContinue(failure.error)) {
|
||||||
if (!stateResponse?.running) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
transientRpc.reset();
|
|
||||||
continue;
|
const sameVersion = await confirmedSameVersionReinstall();
|
||||||
|
if (sameVersion) {
|
||||||
|
if (await settleVersion(sameVersion)) {
|
||||||
|
return selfUpdateResult(sameVersion);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateResponse?.running) {
|
if (stateResponse?.running) {
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ describe('TachyonShellMethods.componentAction', () => {
|
|||||||
'1.2.78',
|
'1.2.78',
|
||||||
);
|
);
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(10000);
|
await vi.advanceTimersByTimeAsync(45000);
|
||||||
|
|
||||||
await expect(responsePromise).resolves.toEqual({
|
await expect(responsePromise).resolves.toEqual({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -731,4 +731,90 @@ describe('TachyonShellMethods.componentAction', () => {
|
|||||||
error: 'Tachyon update did not complete within the timeout',
|
error: 'Tachyon update did not complete within the timeout',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not prematurely confirm same-release Tachyon update while the background job is still running', async () => {
|
||||||
|
let jobFinished = false;
|
||||||
|
|
||||||
|
mocks.fsRead.mockImplementation(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
running: !jobFinished,
|
||||||
|
component: 'tachyon',
|
||||||
|
action: 'install',
|
||||||
|
message: jobFinished ? 'Tachyon has been installed' : 'Component action is running',
|
||||||
|
current_version: '1.3.4',
|
||||||
|
latest_version: '1.3.4',
|
||||||
|
job_id: 'job-1',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
mocks.executeShellCommand.mockImplementation(({ args }) => {
|
||||||
|
if (args[0] === 'component_action_status') {
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
running: !jobFinished,
|
||||||
|
component: 'tachyon',
|
||||||
|
action: 'install',
|
||||||
|
message: jobFinished ? 'Tachyon has been installed' : 'Component action is running',
|
||||||
|
current_version: '1.3.4',
|
||||||
|
latest_version: '1.3.4',
|
||||||
|
job_id: 'job-1',
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
code: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[0] === 'show_version') {
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: '1.3.4\n',
|
||||||
|
stderr: '',
|
||||||
|
code: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: '',
|
||||||
|
stderr: 'Unexpected command',
|
||||||
|
code: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const responsePromise = TachyonShellMethods.waitComponentActionJob(
|
||||||
|
'job-1',
|
||||||
|
'tachyon',
|
||||||
|
'install',
|
||||||
|
'1.3.4',
|
||||||
|
);
|
||||||
|
|
||||||
|
// After 5 seconds, job is still running: must not resolve yet
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
let resolved = false;
|
||||||
|
responsePromise.then(() => {
|
||||||
|
resolved = true;
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(resolved).toBe(false);
|
||||||
|
|
||||||
|
// Now job finishes in backend
|
||||||
|
jobFinished = true;
|
||||||
|
await vi.advanceTimersByTimeAsync(3000);
|
||||||
|
|
||||||
|
await expect(responsePromise).resolves.toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
running: false,
|
||||||
|
component: 'tachyon',
|
||||||
|
action: 'install',
|
||||||
|
message: 'Tachyon has been installed',
|
||||||
|
current_version: '1.3.4',
|
||||||
|
latest_version: '1.3.4',
|
||||||
|
job_id: 'job-1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,12 +215,12 @@ describe('TachyonShellMethods Fuzzer Actions', () => {
|
|||||||
code: 0,
|
code: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const patternsPayload: any = {
|
const patternsPayload = {
|
||||||
zapret2: { splits: ['1', '2', 'midsld'] },
|
zapret2: { splits: ['1', '2', 'midsld'] },
|
||||||
zapret: { splits: ['1'] },
|
zapret: { splits: ['1'] },
|
||||||
byedpi: { splits: ['1'] },
|
byedpi: { splits: ['1'] },
|
||||||
custom_strategies: [],
|
custom_strategies: [],
|
||||||
};
|
} as unknown as Parameters<typeof TachyonShellMethods.saveFuzzerPatterns>[0];
|
||||||
|
|
||||||
const res = await TachyonShellMethods.saveFuzzerPatterns(patternsPayload);
|
const res = await TachyonShellMethods.saveFuzzerPatterns(patternsPayload);
|
||||||
expect(res.success).toBe(true);
|
expect(res.success).toBe(true);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
let currentState: Tachyon.FuzzerState | null = null;
|
let currentState: Tachyon.FuzzerState | null = null;
|
||||||
let resultFilter: 'all' | 'success' | 'fast' = 'all';
|
let resultFilter: 'all' | 'success' | 'fast' = 'all';
|
||||||
let autoApplyEnabled = false;
|
let autoApplyEnabled = false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
let currentDpiDetection: Tachyon.FuzzerDpiDetection | null = null;
|
let currentDpiDetection: Tachyon.FuzzerDpiDetection | null = null;
|
||||||
|
|
||||||
// Patterns Config State
|
// Patterns Config State
|
||||||
@@ -1267,7 +1268,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
list.forEach((item, idx) => {
|
list.forEach((item, idx) => {
|
||||||
const isBest = state.best_strategy && state.best_strategy.id === item.id;
|
const isBest = state.best_strategy && state.best_strategy.id === item.id;
|
||||||
|
|
||||||
let statusBadge = `<span class="label ${item.success ? 'badge-success' : 'badge-danger'}" style="font-size: 10px; padding: 2px 6px; border-radius: 3px;">${item.success ? `HTTP ${item.http_code}` : 'DROP'}</span>`;
|
const statusBadge = `<span class="label ${item.success ? 'badge-success' : 'badge-danger'}" style="font-size: 10px; padding: 2px 6px; border-radius: 3px;">${item.success ? `HTTP ${item.http_code}` : 'DROP'}</span>`;
|
||||||
|
|
||||||
// If suite sub-probes exist, render multi-endpoint summary
|
// If suite sub-probes exist, render multi-endpoint summary
|
||||||
let subProbesHtml = '';
|
let subProbesHtml = '';
|
||||||
|
|||||||
@@ -3686,6 +3686,9 @@ var TachyonShellMethods = {
|
|||||||
const cleanB = b.replace(/^v/i, "").trim();
|
const cleanB = b.replace(/^v/i, "").trim();
|
||||||
return cleanA === cleanB;
|
return cleanA === cleanB;
|
||||||
};
|
};
|
||||||
|
const isDifferentVersion = Boolean(
|
||||||
|
targetVersion && !versionsMatch(targetVersion, baselineVersion)
|
||||||
|
);
|
||||||
const confirmedByVersion = async () => {
|
const confirmedByVersion = async () => {
|
||||||
if (!isSelfUpdate) return "";
|
if (!isSelfUpdate) return "";
|
||||||
if (Date.now() - jobStartedAt < COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS) {
|
if (Date.now() - jobStartedAt < COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS) {
|
||||||
@@ -3693,7 +3696,7 @@ var TachyonShellMethods = {
|
|||||||
}
|
}
|
||||||
const version = await readTachyonVersion();
|
const version = await readTachyonVersion();
|
||||||
if (!version) return "";
|
if (!version) return "";
|
||||||
if (targetVersion && versionsMatch(version, targetVersion)) {
|
if (isDifferentVersion && versionsMatch(version, targetVersion)) {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
if (!targetVersion && baselineVersion && !versionsMatch(version, baselineVersion)) {
|
if (!targetVersion && baselineVersion && !versionsMatch(version, baselineVersion)) {
|
||||||
@@ -3702,11 +3705,14 @@ var TachyonShellMethods = {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
const confirmedSameVersionReinstall = async () => {
|
const confirmedSameVersionReinstall = async () => {
|
||||||
if (!isSelfUpdate || action !== "reinstall" || !baselineVersion) {
|
if (!isSelfUpdate || !baselineVersion) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (targetVersion && !versionsMatch(targetVersion, baselineVersion)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const version = await readTachyonVersion();
|
const version = await readTachyonVersion();
|
||||||
if (versionsMatch(version, baselineVersion) && (!targetVersion || versionsMatch(version, targetVersion))) {
|
if (versionsMatch(version, baselineVersion)) {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -3719,11 +3725,14 @@ var TachyonShellMethods = {
|
|||||||
if (Date.now() - selfUpdateVersionMatchedAt < COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS) {
|
if (Date.now() - selfUpdateVersionMatchedAt < COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (await confirmedByVersion() === version) {
|
if (isDifferentVersion) {
|
||||||
return true;
|
if (await confirmedByVersion() === version) {
|
||||||
}
|
return true;
|
||||||
if (version === baselineVersion && await confirmedSameVersionReinstall() === version) {
|
}
|
||||||
return true;
|
} else {
|
||||||
|
if (await confirmedSameVersionReinstall() === version) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
selfUpdateVersionMatchedAt = 0;
|
selfUpdateVersionMatchedAt = 0;
|
||||||
return false;
|
return false;
|
||||||
@@ -3781,18 +3790,25 @@ var TachyonShellMethods = {
|
|||||||
const parsedResponse = parseComponentActionResult(statusResponse);
|
const parsedResponse = parseComponentActionResult(statusResponse);
|
||||||
if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) {
|
if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) {
|
||||||
if (isSelfUpdate) {
|
if (isSelfUpdate) {
|
||||||
const version = await confirmedByVersion() || await confirmedSameVersionReinstall();
|
const version = await confirmedByVersion();
|
||||||
if (version) {
|
if (version) {
|
||||||
if (await settleVersion(version)) {
|
if (await settleVersion(version)) {
|
||||||
return selfUpdateResult(version);
|
return selfUpdateResult(version);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!stateResponse?.running) {
|
const failure2 = componentActionFailure(statusResponse, parsedResponse);
|
||||||
|
if (transientRpc.shouldContinue(failure2.error)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
transientRpc.reset();
|
const sameVersion = await confirmedSameVersionReinstall();
|
||||||
continue;
|
if (sameVersion) {
|
||||||
|
if (await settleVersion(sameVersion)) {
|
||||||
|
return selfUpdateResult(sameVersion);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return failure2;
|
||||||
}
|
}
|
||||||
if (stateResponse?.running) {
|
if (stateResponse?.running) {
|
||||||
transientRpc.reset();
|
transientRpc.reset();
|
||||||
@@ -12998,7 +13014,7 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
}
|
}
|
||||||
list.forEach((item, idx) => {
|
list.forEach((item, idx) => {
|
||||||
const isBest = state.best_strategy && state.best_strategy.id === item.id;
|
const isBest = state.best_strategy && state.best_strategy.id === item.id;
|
||||||
let statusBadge = `<span class="label ${item.success ? "badge-success" : "badge-danger"}" style="font-size: 10px; padding: 2px 6px; border-radius: 3px;">${item.success ? `HTTP ${item.http_code}` : "DROP"}</span>`;
|
const statusBadge = `<span class="label ${item.success ? "badge-success" : "badge-danger"}" style="font-size: 10px; padding: 2px 6px; border-radius: 3px;">${item.success ? `HTTP ${item.http_code}` : "DROP"}</span>`;
|
||||||
let subProbesHtml = "";
|
let subProbesHtml = "";
|
||||||
if (item.sub_probes && item.sub_probes.length > 1) {
|
if (item.sub_probes && item.sub_probes.length > 1) {
|
||||||
const passedSub = item.sub_probes.filter((p) => p.success).length;
|
const passedSub = item.sub_probes.filter((p) => p.success).length;
|
||||||
|
|||||||
Reference in New Issue
Block a user