feat(updates): добавить локальный бэкап и оффлайн-откат компонентов
This commit is contained in:
@@ -1249,6 +1249,9 @@ const dictionary = {
|
||||
"All components": "Все компоненты",
|
||||
"Tachyon Core": "Ядро Tachyon",
|
||||
"LuCI Web UI": "Веб-интерфейс LuCI",
|
||||
"Rollback": "Откатить",
|
||||
"Save component backup before update": "Сохранять резервную копию перед обновлением",
|
||||
"Keep the previous working binary/package on the router flash for instant offline rollback if needed. Consumes extra storage.": "Сохранять предыдущую рабочую версию компонента в памяти роутера для быстрого локального отката. Занимает дополнительное место на flash.",
|
||||
};
|
||||
|
||||
function escapePoString(str) {
|
||||
|
||||
@@ -5386,3 +5386,12 @@ msgstr "Очистить историю"
|
||||
|
||||
msgid "Auto-applied best strategy"
|
||||
msgstr "Лучшая стратегия автоматически применена"
|
||||
|
||||
msgid "Rollback"
|
||||
msgstr "Откатить"
|
||||
|
||||
msgid "Save component backup before update"
|
||||
msgstr "Сохранять резервную копию перед обновлением"
|
||||
|
||||
msgid "Keep the previous working binary/package on the router flash for instant offline rollback if needed. Consumes extra storage."
|
||||
msgstr "Сохранять предыдущую рабочую версию компонента в памяти роутера для быстрого локального отката. Занимает дополнительное место на flash."
|
||||
|
||||
@@ -7,8 +7,10 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
|
||||
'tachyon:check_update': 'tachyonCheck',
|
||||
'tachyon:install': 'tachyonInstall',
|
||||
'tachyon:reinstall': 'tachyonReinstall',
|
||||
'tachyon:rollback': 'tachyonRollback',
|
||||
'sing_box:check_update': 'singBoxCheck',
|
||||
'sing_box:install': 'singBoxInstall',
|
||||
'sing_box:rollback': 'singBoxRollback',
|
||||
'sing_box:install_extended': 'singBoxInstallExtended',
|
||||
'sing_box:install_extended_compressed': 'singBoxInstallExtendedCompressed',
|
||||
'sing_box:install_lx': 'singBoxInstallLx',
|
||||
@@ -17,15 +19,19 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
|
||||
'zapret:check_update': 'zapretCheck',
|
||||
'zapret:install': 'zapretInstall',
|
||||
'zapret:remove': 'zapretRemove',
|
||||
'zapret:rollback': 'zapretRollback',
|
||||
'zapret2:check_update': 'zapret2Check',
|
||||
'zapret2:install': 'zapret2Install',
|
||||
'zapret2:remove': 'zapret2Remove',
|
||||
'zapret2:rollback': 'zapret2Rollback',
|
||||
'byedpi:check_update': 'byedpiCheck',
|
||||
'byedpi:install': 'byedpiInstall',
|
||||
'byedpi:remove': 'byedpiRemove',
|
||||
'byedpi:rollback': 'byedpiRollback',
|
||||
'tailscale:check_update': 'tailscaleCheck',
|
||||
'tailscale:install': 'tailscaleInstall',
|
||||
'tailscale:remove': 'tailscaleRemove',
|
||||
'tailscale:rollback': 'tailscaleRollback',
|
||||
};
|
||||
|
||||
export function getComponentActionKey(
|
||||
|
||||
@@ -216,14 +216,24 @@ export interface StoreType {
|
||||
sing_box_lx: number;
|
||||
sing_box_tailscale: number;
|
||||
sing_box_repo_url?: string;
|
||||
sing_box_backup_version?: string;
|
||||
sing_box_backup_time?: number;
|
||||
zapret_version: string;
|
||||
zapret_installed: number;
|
||||
zapret_backup_version?: string;
|
||||
zapret_backup_time?: number;
|
||||
zapret2_version: string;
|
||||
zapret2_installed: number;
|
||||
zapret2_backup_version?: string;
|
||||
zapret2_backup_time?: number;
|
||||
byedpi_version: string;
|
||||
byedpi_installed: number;
|
||||
byedpi_backup_version?: string;
|
||||
byedpi_backup_time?: number;
|
||||
tailscale_version: string;
|
||||
tailscale_installed: number;
|
||||
tailscale_backup_version?: string;
|
||||
tailscale_backup_time?: number;
|
||||
server_inbounds_enabled_count: number;
|
||||
openwrt_version: string;
|
||||
device_model: string;
|
||||
@@ -232,8 +242,10 @@ export interface StoreType {
|
||||
tachyonCheck: { loading: boolean };
|
||||
tachyonInstall: { loading: boolean };
|
||||
tachyonReinstall: { loading: boolean };
|
||||
tachyonRollback: { loading: boolean };
|
||||
singBoxCheck: { loading: boolean };
|
||||
singBoxInstall: { loading: boolean };
|
||||
singBoxRollback: { loading: boolean };
|
||||
singBoxInstallExtended: { loading: boolean };
|
||||
singBoxInstallExtendedCompressed: { loading: boolean };
|
||||
singBoxInstallLx: { loading: boolean };
|
||||
@@ -242,15 +254,19 @@ export interface StoreType {
|
||||
zapretCheck: { loading: boolean };
|
||||
zapretInstall: { loading: boolean };
|
||||
zapretRemove: { loading: boolean };
|
||||
zapretRollback: { loading: boolean };
|
||||
zapret2Check: { loading: boolean };
|
||||
zapret2Install: { loading: boolean };
|
||||
zapret2Remove: { loading: boolean };
|
||||
zapret2Rollback: { loading: boolean };
|
||||
byedpiCheck: { loading: boolean };
|
||||
byedpiInstall: { loading: boolean };
|
||||
byedpiRemove: { loading: boolean };
|
||||
byedpiRollback: { loading: boolean };
|
||||
tailscaleCheck: { loading: boolean };
|
||||
tailscaleInstall: { loading: boolean };
|
||||
tailscaleRemove: { loading: boolean };
|
||||
tailscaleRollback: { loading: boolean };
|
||||
};
|
||||
updatesChecks: Record<
|
||||
Tachyon.ComponentName,
|
||||
|
||||
@@ -16,8 +16,10 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
|
||||
tachyonCheck: { loading: false },
|
||||
tachyonInstall: { loading: false },
|
||||
tachyonReinstall: { loading: false },
|
||||
tachyonRollback: { loading: false },
|
||||
singBoxCheck: { loading: false },
|
||||
singBoxInstall: { loading: false },
|
||||
singBoxRollback: { loading: false },
|
||||
singBoxInstallExtended: { loading: false },
|
||||
singBoxInstallExtendedCompressed: { loading: false },
|
||||
singBoxInstallLx: { loading: false },
|
||||
@@ -26,15 +28,19 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
|
||||
zapretCheck: { loading: false },
|
||||
zapretInstall: { loading: false },
|
||||
zapretRemove: { loading: false },
|
||||
zapretRollback: { loading: false },
|
||||
zapret2Check: { loading: false },
|
||||
zapret2Install: { loading: false },
|
||||
zapret2Remove: { loading: false },
|
||||
zapret2Rollback: { loading: false },
|
||||
byedpiCheck: { loading: false },
|
||||
byedpiInstall: { loading: false },
|
||||
byedpiRemove: { loading: false },
|
||||
byedpiRollback: { loading: false },
|
||||
tailscaleCheck: { loading: false },
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -88,14 +88,24 @@ export const initialDiagnosticStore: Pick<
|
||||
sing_box_lx: 0,
|
||||
sing_box_tailscale: 1,
|
||||
sing_box_repo_url: '',
|
||||
sing_box_backup_version: '',
|
||||
sing_box_backup_time: 0,
|
||||
zapret_version: 'loading',
|
||||
zapret_installed: 0,
|
||||
zapret_backup_version: '',
|
||||
zapret_backup_time: 0,
|
||||
zapret2_version: 'loading',
|
||||
zapret2_installed: 0,
|
||||
zapret2_backup_version: '',
|
||||
zapret2_backup_time: 0,
|
||||
byedpi_version: 'loading',
|
||||
byedpi_installed: 0,
|
||||
byedpi_backup_version: '',
|
||||
byedpi_backup_time: 0,
|
||||
tailscale_version: 'loading',
|
||||
tailscale_installed: 0,
|
||||
tailscale_backup_version: '',
|
||||
tailscale_backup_time: 0,
|
||||
server_inbounds_enabled_count: -1,
|
||||
openwrt_version: 'loading',
|
||||
device_model: 'loading',
|
||||
@@ -144,8 +154,10 @@ export const initialDiagnosticStore: Pick<
|
||||
tachyonCheck: { loading: false },
|
||||
tachyonInstall: { loading: false },
|
||||
tachyonReinstall: { loading: false },
|
||||
tachyonRollback: { loading: false },
|
||||
singBoxCheck: { loading: false },
|
||||
singBoxInstall: { loading: false },
|
||||
singBoxRollback: { loading: false },
|
||||
singBoxInstallExtended: { loading: false },
|
||||
singBoxInstallExtendedCompressed: { loading: false },
|
||||
singBoxInstallLx: { loading: false },
|
||||
@@ -154,15 +166,19 @@ export const initialDiagnosticStore: Pick<
|
||||
zapretCheck: { loading: false },
|
||||
zapretInstall: { loading: false },
|
||||
zapretRemove: { loading: false },
|
||||
zapretRollback: { loading: false },
|
||||
zapret2Check: { loading: false },
|
||||
zapret2Install: { loading: false },
|
||||
zapret2Remove: { loading: false },
|
||||
zapret2Rollback: { loading: false },
|
||||
byedpiCheck: { loading: false },
|
||||
byedpiInstall: { loading: false },
|
||||
byedpiRemove: { loading: false },
|
||||
byedpiRollback: { loading: false },
|
||||
tailscaleCheck: { loading: false },
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false },
|
||||
},
|
||||
updatesChecks: {
|
||||
tachyon: { status: null, latest_version: '', release_url: '' },
|
||||
|
||||
@@ -1037,24 +1037,58 @@ function getInstalledUpdateActions(
|
||||
return actions;
|
||||
}
|
||||
|
||||
function getComponentBackupVersion(component: Tachyon.ComponentName): string {
|
||||
const sys = store.get().diagnosticsSystemInfo;
|
||||
switch (component) {
|
||||
case 'sing_box':
|
||||
return sys.sing_box_backup_version || '';
|
||||
case 'zapret':
|
||||
return sys.zapret_backup_version || '';
|
||||
case 'zapret2':
|
||||
return sys.zapret2_backup_version || '';
|
||||
case 'byedpi':
|
||||
return sys.byedpi_backup_version || '';
|
||||
case 'tailscale':
|
||||
return sys.tailscale_backup_version || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getRollbackAction(
|
||||
component: Tachyon.ComponentName,
|
||||
key: UpdatesActionKey,
|
||||
backupVersion: string,
|
||||
): ComponentActionButton {
|
||||
return {
|
||||
key,
|
||||
text: backupVersion ? `${_('Rollback')} (${backupVersion})` : _('Rollback'),
|
||||
icon: renderRotateCcwIcon24,
|
||||
component,
|
||||
action: 'rollback',
|
||||
};
|
||||
}
|
||||
|
||||
function getOptionalComponentActions({
|
||||
component,
|
||||
installed,
|
||||
checkKey,
|
||||
installKey,
|
||||
removeKey,
|
||||
rollbackKey,
|
||||
}: {
|
||||
component: 'zapret' | 'zapret2' | 'byedpi' | 'tailscale';
|
||||
installed: boolean;
|
||||
checkKey: UpdatesActionKey;
|
||||
installKey: UpdatesActionKey;
|
||||
removeKey: UpdatesActionKey;
|
||||
rollbackKey: UpdatesActionKey;
|
||||
}) {
|
||||
if (!installed) {
|
||||
return [getInstallAction(component, installKey, false)];
|
||||
}
|
||||
|
||||
return [
|
||||
const actions = [
|
||||
...getInstalledUpdateActions(component, checkKey, installKey),
|
||||
{
|
||||
key: removeKey,
|
||||
@@ -1064,6 +1098,13 @@ function getOptionalComponentActions({
|
||||
action: 'remove' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const backupVersion = getComponentBackupVersion(component);
|
||||
if (backupVersion) {
|
||||
actions.push(getRollbackAction(component, rollbackKey, backupVersion));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
const COMPONENT_REPO_URLS: Record<Tachyon.ComponentName, string> = {
|
||||
@@ -1117,6 +1158,13 @@ function getComponentCards(): ComponentCard[] {
|
||||
singBoxInstalled,
|
||||
);
|
||||
|
||||
const singBoxBackup = getComponentBackupVersion('sing_box');
|
||||
if (singBoxBackup && singBoxInstalled) {
|
||||
singBoxActions.push(
|
||||
getRollbackAction('sing_box', 'singBoxRollback', singBoxBackup),
|
||||
);
|
||||
}
|
||||
|
||||
if (!singBoxStable) {
|
||||
singBoxActions.push({
|
||||
key: 'singBoxInstallStable',
|
||||
@@ -1169,6 +1217,7 @@ function getComponentCards(): ComponentCard[] {
|
||||
checkKey: 'zapretCheck',
|
||||
installKey: 'zapretInstall',
|
||||
removeKey: 'zapretRemove',
|
||||
rollbackKey: 'zapretRollback',
|
||||
});
|
||||
const zapret2Actions = getOptionalComponentActions({
|
||||
component: 'zapret2',
|
||||
@@ -1176,6 +1225,7 @@ function getComponentCards(): ComponentCard[] {
|
||||
checkKey: 'zapret2Check',
|
||||
installKey: 'zapret2Install',
|
||||
removeKey: 'zapret2Remove',
|
||||
rollbackKey: 'zapret2Rollback',
|
||||
});
|
||||
const byedpiActions = getOptionalComponentActions({
|
||||
component: 'byedpi',
|
||||
@@ -1183,6 +1233,7 @@ function getComponentCards(): ComponentCard[] {
|
||||
checkKey: 'byedpiCheck',
|
||||
installKey: 'byedpiInstall',
|
||||
removeKey: 'byedpiRemove',
|
||||
rollbackKey: 'byedpiRollback',
|
||||
});
|
||||
const tailscaleActions = getOptionalComponentActions({
|
||||
component: 'tailscale',
|
||||
@@ -1190,6 +1241,7 @@ function getComponentCards(): ComponentCard[] {
|
||||
checkKey: 'tailscaleCheck',
|
||||
installKey: 'tailscaleInstall',
|
||||
removeKey: 'tailscaleRemove',
|
||||
rollbackKey: 'tailscaleRollback',
|
||||
});
|
||||
|
||||
return [
|
||||
|
||||
@@ -651,6 +651,7 @@ export namespace Tachyon {
|
||||
| 'install'
|
||||
| 'reinstall'
|
||||
| 'remove'
|
||||
| 'rollback'
|
||||
| 'install_extended'
|
||||
| 'install_extended_compressed'
|
||||
| 'install_lx'
|
||||
|
||||
@@ -5603,14 +5603,24 @@ var initialDiagnosticStore = {
|
||||
sing_box_lx: 0,
|
||||
sing_box_tailscale: 1,
|
||||
sing_box_repo_url: "",
|
||||
sing_box_backup_version: "",
|
||||
sing_box_backup_time: 0,
|
||||
zapret_version: "loading",
|
||||
zapret_installed: 0,
|
||||
zapret_backup_version: "",
|
||||
zapret_backup_time: 0,
|
||||
zapret2_version: "loading",
|
||||
zapret2_installed: 0,
|
||||
zapret2_backup_version: "",
|
||||
zapret2_backup_time: 0,
|
||||
byedpi_version: "loading",
|
||||
byedpi_installed: 0,
|
||||
byedpi_backup_version: "",
|
||||
byedpi_backup_time: 0,
|
||||
tailscale_version: "loading",
|
||||
tailscale_installed: 0,
|
||||
tailscale_backup_version: "",
|
||||
tailscale_backup_time: 0,
|
||||
server_inbounds_enabled_count: -1,
|
||||
openwrt_version: "loading",
|
||||
device_model: "loading"
|
||||
@@ -5659,8 +5669,10 @@ var initialDiagnosticStore = {
|
||||
tachyonCheck: { loading: false },
|
||||
tachyonInstall: { loading: false },
|
||||
tachyonReinstall: { loading: false },
|
||||
tachyonRollback: { loading: false },
|
||||
singBoxCheck: { loading: false },
|
||||
singBoxInstall: { loading: false },
|
||||
singBoxRollback: { loading: false },
|
||||
singBoxInstallExtended: { loading: false },
|
||||
singBoxInstallExtendedCompressed: { loading: false },
|
||||
singBoxInstallLx: { loading: false },
|
||||
@@ -5669,15 +5681,19 @@ var initialDiagnosticStore = {
|
||||
zapretCheck: { loading: false },
|
||||
zapretInstall: { loading: false },
|
||||
zapretRemove: { loading: false },
|
||||
zapretRollback: { loading: false },
|
||||
zapret2Check: { loading: false },
|
||||
zapret2Install: { loading: false },
|
||||
zapret2Remove: { loading: false },
|
||||
zapret2Rollback: { loading: false },
|
||||
byedpiCheck: { loading: false },
|
||||
byedpiInstall: { loading: false },
|
||||
byedpiRemove: { loading: false },
|
||||
byedpiRollback: { loading: false },
|
||||
tailscaleCheck: { loading: false },
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false }
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false }
|
||||
},
|
||||
updatesChecks: {
|
||||
tachyon: { status: null, latest_version: "", release_url: "" },
|
||||
@@ -6032,8 +6048,10 @@ var componentActionKeyMap = {
|
||||
"tachyon:check_update": "tachyonCheck",
|
||||
"tachyon:install": "tachyonInstall",
|
||||
"tachyon:reinstall": "tachyonReinstall",
|
||||
"tachyon:rollback": "tachyonRollback",
|
||||
"sing_box:check_update": "singBoxCheck",
|
||||
"sing_box:install": "singBoxInstall",
|
||||
"sing_box:rollback": "singBoxRollback",
|
||||
"sing_box:install_extended": "singBoxInstallExtended",
|
||||
"sing_box:install_extended_compressed": "singBoxInstallExtendedCompressed",
|
||||
"sing_box:install_lx": "singBoxInstallLx",
|
||||
@@ -6042,15 +6060,19 @@ var componentActionKeyMap = {
|
||||
"zapret:check_update": "zapretCheck",
|
||||
"zapret:install": "zapretInstall",
|
||||
"zapret:remove": "zapretRemove",
|
||||
"zapret:rollback": "zapretRollback",
|
||||
"zapret2:check_update": "zapret2Check",
|
||||
"zapret2:install": "zapret2Install",
|
||||
"zapret2:remove": "zapret2Remove",
|
||||
"zapret2:rollback": "zapret2Rollback",
|
||||
"byedpi:check_update": "byedpiCheck",
|
||||
"byedpi:install": "byedpiInstall",
|
||||
"byedpi:remove": "byedpiRemove",
|
||||
"byedpi:rollback": "byedpiRollback",
|
||||
"tailscale:check_update": "tailscaleCheck",
|
||||
"tailscale:install": "tailscaleInstall",
|
||||
"tailscale:remove": "tailscaleRemove"
|
||||
"tailscale:remove": "tailscaleRemove",
|
||||
"tailscale:rollback": "tailscaleRollback"
|
||||
};
|
||||
function getComponentActionKey(component, action) {
|
||||
return componentActionKeyMap[`${component}:${action}`];
|
||||
@@ -6147,8 +6169,10 @@ function getEmptyUpdatesActions() {
|
||||
tachyonCheck: { loading: false },
|
||||
tachyonInstall: { loading: false },
|
||||
tachyonReinstall: { loading: false },
|
||||
tachyonRollback: { loading: false },
|
||||
singBoxCheck: { loading: false },
|
||||
singBoxInstall: { loading: false },
|
||||
singBoxRollback: { loading: false },
|
||||
singBoxInstallExtended: { loading: false },
|
||||
singBoxInstallExtendedCompressed: { loading: false },
|
||||
singBoxInstallLx: { loading: false },
|
||||
@@ -6157,15 +6181,19 @@ function getEmptyUpdatesActions() {
|
||||
zapretCheck: { loading: false },
|
||||
zapretInstall: { loading: false },
|
||||
zapretRemove: { loading: false },
|
||||
zapretRollback: { loading: false },
|
||||
zapret2Check: { loading: false },
|
||||
zapret2Install: { loading: false },
|
||||
zapret2Remove: { loading: false },
|
||||
zapret2Rollback: { loading: false },
|
||||
byedpiCheck: { loading: false },
|
||||
byedpiInstall: { loading: false },
|
||||
byedpiRemove: { loading: false },
|
||||
byedpiRollback: { loading: false },
|
||||
tailscaleCheck: { loading: false },
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false }
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false }
|
||||
};
|
||||
}
|
||||
function getEmptyDiagnosticsActions() {
|
||||
@@ -19293,17 +19321,44 @@ function getInstalledUpdateActions(component, checkKey, installKey, installed =
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
function getComponentBackupVersion(component) {
|
||||
const sys = store.get().diagnosticsSystemInfo;
|
||||
switch (component) {
|
||||
case "sing_box":
|
||||
return sys.sing_box_backup_version || "";
|
||||
case "zapret":
|
||||
return sys.zapret_backup_version || "";
|
||||
case "zapret2":
|
||||
return sys.zapret2_backup_version || "";
|
||||
case "byedpi":
|
||||
return sys.byedpi_backup_version || "";
|
||||
case "tailscale":
|
||||
return sys.tailscale_backup_version || "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function getRollbackAction(component, key, backupVersion) {
|
||||
return {
|
||||
key,
|
||||
text: backupVersion ? `${_("Rollback")} (${backupVersion})` : _("Rollback"),
|
||||
icon: renderRotateCcwIcon24,
|
||||
component,
|
||||
action: "rollback"
|
||||
};
|
||||
}
|
||||
function getOptionalComponentActions({
|
||||
component,
|
||||
installed,
|
||||
checkKey,
|
||||
installKey,
|
||||
removeKey
|
||||
removeKey,
|
||||
rollbackKey
|
||||
}) {
|
||||
if (!installed) {
|
||||
return [getInstallAction(component, installKey, false)];
|
||||
}
|
||||
return [
|
||||
const actions = [
|
||||
...getInstalledUpdateActions(component, checkKey, installKey),
|
||||
{
|
||||
key: removeKey,
|
||||
@@ -19313,6 +19368,11 @@ function getOptionalComponentActions({
|
||||
action: "remove"
|
||||
}
|
||||
];
|
||||
const backupVersion = getComponentBackupVersion(component);
|
||||
if (backupVersion) {
|
||||
actions.push(getRollbackAction(component, rollbackKey, backupVersion));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
var COMPONENT_REPO_URLS = {
|
||||
tachyon: "https://github.com/Dushnilin/tachyon",
|
||||
@@ -19353,6 +19413,12 @@ function getComponentCards() {
|
||||
"singBoxInstall",
|
||||
singBoxInstalled
|
||||
);
|
||||
const singBoxBackup = getComponentBackupVersion("sing_box");
|
||||
if (singBoxBackup && singBoxInstalled) {
|
||||
singBoxActions.push(
|
||||
getRollbackAction("sing_box", "singBoxRollback", singBoxBackup)
|
||||
);
|
||||
}
|
||||
if (!singBoxStable) {
|
||||
singBoxActions.push({
|
||||
key: "singBoxInstallStable",
|
||||
@@ -19403,28 +19469,32 @@ function getComponentCards() {
|
||||
installed: zapretInstalled,
|
||||
checkKey: "zapretCheck",
|
||||
installKey: "zapretInstall",
|
||||
removeKey: "zapretRemove"
|
||||
removeKey: "zapretRemove",
|
||||
rollbackKey: "zapretRollback"
|
||||
});
|
||||
const zapret2Actions = getOptionalComponentActions({
|
||||
component: "zapret2",
|
||||
installed: zapret2Installed,
|
||||
checkKey: "zapret2Check",
|
||||
installKey: "zapret2Install",
|
||||
removeKey: "zapret2Remove"
|
||||
removeKey: "zapret2Remove",
|
||||
rollbackKey: "zapret2Rollback"
|
||||
});
|
||||
const byedpiActions = getOptionalComponentActions({
|
||||
component: "byedpi",
|
||||
installed: byedpiInstalled,
|
||||
checkKey: "byedpiCheck",
|
||||
installKey: "byedpiInstall",
|
||||
removeKey: "byedpiRemove"
|
||||
removeKey: "byedpiRemove",
|
||||
rollbackKey: "byedpiRollback"
|
||||
});
|
||||
const tailscaleActions = getOptionalComponentActions({
|
||||
component: "tailscale",
|
||||
installed: tailscaleInstalled,
|
||||
checkKey: "tailscaleCheck",
|
||||
installKey: "tailscaleInstall",
|
||||
removeKey: "tailscaleRemove"
|
||||
removeKey: "tailscaleRemove",
|
||||
rollbackKey: "tailscaleRollback"
|
||||
});
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1506,6 +1506,18 @@ function createSettingsContent(section, capabilities) {
|
||||
o.default = ["all"];
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.taboption(
|
||||
"updates",
|
||||
form.Flag,
|
||||
"component_backup_enabled",
|
||||
_("Save component backup before update"),
|
||||
_(
|
||||
"Keep the previous working binary/package on the router flash for instant offline rollback if needed. Consumes extra storage.",
|
||||
),
|
||||
);
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
o = section.taboption(
|
||||
"updates",
|
||||
form.Value,
|
||||
|
||||
@@ -5386,3 +5386,12 @@ msgstr "Очистить историю"
|
||||
|
||||
msgid "Auto-applied best strategy"
|
||||
msgstr "Лучшая стратегия автоматически применена"
|
||||
|
||||
msgid "Rollback"
|
||||
msgstr "Откатить"
|
||||
|
||||
msgid "Save component backup before update"
|
||||
msgstr "Сохранять резервную копию перед обновлением"
|
||||
|
||||
msgid "Keep the previous working binary/package on the router flash for instant offline rollback if needed. Consumes extra storage."
|
||||
msgstr "Сохранять предыдущую рабочую версию компонента в памяти роутера для быстрого локального отката. Занимает дополнительное место на flash."
|
||||
|
||||
@@ -2495,6 +2495,259 @@ function normalize_component_name(component) {
|
||||
return component;
|
||||
}
|
||||
|
||||
const COMPONENT_BACKUP_BASE_DIR = getenv("TACHYON_COMPONENT_BACKUPS_DIR") || "/etc/tachyon/component-backups";
|
||||
|
||||
function get_component_backup_enabled() {
|
||||
let uci_core = require("core.uci");
|
||||
let settings = (uci_core && uci_core.get_all) ? (uci_core.get_all("tachyon", "settings") || {}) : {};
|
||||
let val = as_string(settings.component_backup_enabled || "");
|
||||
return val == "1" || val == "true" || val == "yes" || val == "on";
|
||||
}
|
||||
|
||||
function component_backup_dir(component) {
|
||||
component = normalize_component_name(component);
|
||||
return COMPONENT_BACKUP_BASE_DIR + "/" + component;
|
||||
}
|
||||
|
||||
function component_backup_metadata_file(component) {
|
||||
return component_backup_dir(component) + "/metadata.json";
|
||||
}
|
||||
|
||||
function read_component_backup_metadata(component) {
|
||||
let file = component_backup_metadata_file(component);
|
||||
if (!file_exists(file))
|
||||
return null;
|
||||
let data = read_file(file);
|
||||
if (data == "")
|
||||
return null;
|
||||
try {
|
||||
let parsed = json(data);
|
||||
if (type(parsed) == "object" && parsed.version)
|
||||
return parsed;
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function check_free_disk_space(target_dir, needed_bytes) {
|
||||
let out = trim(command_output("df -k " + shell_quote(target_dir) + " 2>/dev/null | tail -n 1 | awk '{print $4}'"));
|
||||
let free_kb = int(out);
|
||||
if (free_kb <= 0)
|
||||
return true;
|
||||
let needed_kb = int((needed_bytes || 0) / 1024) + 1024;
|
||||
return free_kb > (needed_kb * 2) && (free_kb - needed_kb) > 4096;
|
||||
}
|
||||
|
||||
function create_component_backup(component) {
|
||||
component = normalize_component_name(component);
|
||||
if (!get_component_backup_enabled())
|
||||
return true;
|
||||
|
||||
let bdir = component_backup_dir(component);
|
||||
let meta_file = component_backup_metadata_file(component);
|
||||
|
||||
if (component == "sing_box") {
|
||||
if (!file_exists("/usr/bin/sing-box"))
|
||||
return true;
|
||||
let st = fs.stat("/usr/bin/sing-box");
|
||||
let size = (st && st.size) ? st.size : 0;
|
||||
if (size <= 0)
|
||||
return true;
|
||||
|
||||
if (!check_free_disk_space("/etc", size)) {
|
||||
updates_log("Skipping sing-box backup before update: insufficient disk space on /etc", "warn");
|
||||
return false;
|
||||
}
|
||||
|
||||
ensure_dir(bdir);
|
||||
let backup_bin = bdir + "/sing-box";
|
||||
remove_file(backup_bin);
|
||||
if (!command_success_from_args([ "cp", "-p", "/usr/bin/sing-box", backup_bin ])) {
|
||||
updates_log("Failed to create sing-box backup copy", "warn");
|
||||
return false;
|
||||
}
|
||||
|
||||
let variant = sing_box_runtime_output("variant", []);
|
||||
let marker = sing_box_runtime_output("read-variant-marker", []);
|
||||
let version = read_sing_box_binary_version("/usr/bin/sing-box", "/usr/lib");
|
||||
if (version == "")
|
||||
version = sing_box_runtime_output("version", []);
|
||||
|
||||
if (file_exists("/etc/init.d/sing-box")) {
|
||||
command_success_from_args([ "cp", "-p", "/etc/init.d/sing-box", bdir + "/sing-box.init" ]);
|
||||
}
|
||||
if (file_exists("/usr/lib/libcronet.so")) {
|
||||
command_success_from_args([ "cp", "-p", "/usr/lib/libcronet.so", bdir + "/libcronet.so" ]);
|
||||
}
|
||||
|
||||
let meta = {
|
||||
component: "sing_box",
|
||||
version: version,
|
||||
variant: variant,
|
||||
marker: marker,
|
||||
size: size,
|
||||
timestamp: now_seconds()
|
||||
};
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
updates_log("Created local backup of sing-box v" + version + " (" + variant + ")", "info");
|
||||
return true;
|
||||
}
|
||||
else if (component == "byedpi") {
|
||||
if (!file_exists("/usr/bin/ciadpi"))
|
||||
return true;
|
||||
let st = fs.stat("/usr/bin/ciadpi");
|
||||
let size = (st && st.size) ? st.size : 0;
|
||||
if (!check_free_disk_space("/etc", size)) return false;
|
||||
ensure_dir(bdir);
|
||||
command_success_from_args([ "cp", "-p", "/usr/bin/ciadpi", bdir + "/ciadpi" ]);
|
||||
let version = trim(command_output("/usr/bin/ciadpi --version 2>&1 || true"));
|
||||
let meta = {
|
||||
component: "byedpi",
|
||||
version: version,
|
||||
timestamp: now_seconds()
|
||||
};
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
return true;
|
||||
}
|
||||
else if (component == "zapret") {
|
||||
let nfqws = "/opt/zapret/nfq/nfqws";
|
||||
if (!file_exists(nfqws)) nfqws = "/usr/bin/nfqws";
|
||||
if (!file_exists(nfqws)) return true;
|
||||
ensure_dir(bdir);
|
||||
command_success_from_args([ "cp", "-p", nfqws, bdir + "/nfqws" ]);
|
||||
let meta = {
|
||||
component: "zapret",
|
||||
version: provider_package_version(LIB_DIR + "/providers/zapret/runtime.uc"),
|
||||
timestamp: now_seconds()
|
||||
};
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
return true;
|
||||
}
|
||||
else if (component == "zapret2") {
|
||||
let nfqws2 = "/opt/zapret2/nfq/nfqws2";
|
||||
if (!file_exists(nfqws2)) nfqws2 = "/usr/bin/nfqws2";
|
||||
if (!file_exists(nfqws2)) return true;
|
||||
ensure_dir(bdir);
|
||||
command_success_from_args([ "cp", "-p", nfqws2, bdir + "/nfqws2" ]);
|
||||
let meta = {
|
||||
component: "zapret2",
|
||||
version: provider_package_version(LIB_DIR + "/providers/zapret2/runtime.uc"),
|
||||
timestamp: now_seconds()
|
||||
};
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
return true;
|
||||
}
|
||||
else if (component == "tailscale") {
|
||||
if (!file_exists("/usr/sbin/tailscale")) return true;
|
||||
ensure_dir(bdir);
|
||||
command_success_from_args([ "cp", "-p", "/usr/sbin/tailscale", bdir + "/tailscale" ]);
|
||||
let meta = {
|
||||
component: "tailscale",
|
||||
version: provider_package_version(LIB_DIR + "/providers/tailscale/runtime.uc"),
|
||||
timestamp: now_seconds()
|
||||
};
|
||||
write_file(meta_file, sprintf("%J\n", meta));
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function rollback_component(component) {
|
||||
component = normalize_component_name(component);
|
||||
let bdir = component_backup_dir(component);
|
||||
let meta = read_component_backup_metadata(component);
|
||||
if (meta == null) {
|
||||
action_fail(component, "rollback", "No local backup found for " + component);
|
||||
}
|
||||
|
||||
let backup_version = as_string(meta.version || "previous");
|
||||
updates_log("Starting rollback of " + component + " to backup version " + backup_version, "info");
|
||||
|
||||
if (component == "sing_box") {
|
||||
let backup_bin = bdir + "/sing-box";
|
||||
if (!file_exists(backup_bin) || !file_nonempty(backup_bin)) {
|
||||
action_fail("sing_box", "rollback", "Sing-box backup binary is missing or empty");
|
||||
}
|
||||
|
||||
stop_tachyon_before_sing_box_change();
|
||||
|
||||
remove_file("/usr/bin/sing-box");
|
||||
if (!command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/sing-box" ]) ||
|
||||
!command_success_from_args([ "chmod", "0755", "/usr/bin/sing-box" ])) {
|
||||
action_fail("sing_box", "rollback", "Failed to restore sing-box binary from backup");
|
||||
}
|
||||
|
||||
if (file_exists(bdir + "/libcronet.so")) {
|
||||
remove_file("/usr/lib/libcronet.so");
|
||||
command_success_from_args([ "cp", "-p", bdir + "/libcronet.so", "/usr/lib/libcronet.so" ]);
|
||||
command_success_from_args([ "chmod", "0644", "/usr/lib/libcronet.so" ]);
|
||||
}
|
||||
|
||||
if (file_exists(bdir + "/sing-box.init")) {
|
||||
command_success_from_args([ "cp", "-p", bdir + "/sing-box.init", "/etc/init.d/sing-box" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/etc/init.d/sing-box" ]);
|
||||
}
|
||||
|
||||
if (meta.marker) {
|
||||
write_sing_box_variant_state(meta.marker, backup_version);
|
||||
}
|
||||
|
||||
clear_version_caches();
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("sing_box", "rollback", "Sing-box rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else if (component == "byedpi") {
|
||||
let backup_bin = bdir + "/ciadpi";
|
||||
if (!file_exists(backup_bin)) action_fail("byedpi", "rollback", "ByeDPI backup binary is missing");
|
||||
remove_file("/usr/bin/ciadpi");
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/ciadpi" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/usr/bin/ciadpi" ]);
|
||||
clear_version_caches();
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("byedpi", "rollback", "ByeDPI rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else if (component == "zapret") {
|
||||
let backup_bin = bdir + "/nfqws";
|
||||
if (!file_exists(backup_bin)) action_fail("zapret", "rollback", "Zapret backup binary is missing");
|
||||
if (file_exists("/opt/zapret/nfq/nfqws")) {
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/opt/zapret/nfq/nfqws" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/opt/zapret/nfq/nfqws" ]);
|
||||
} else {
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/nfqws" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/usr/bin/nfqws" ]);
|
||||
}
|
||||
clear_version_caches();
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("zapret", "rollback", "Zapret rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else if (component == "zapret2") {
|
||||
let backup_bin = bdir + "/nfqws2";
|
||||
if (!file_exists(backup_bin)) action_fail("zapret2", "rollback", "Zapret2 backup binary is missing");
|
||||
if (file_exists("/opt/zapret2/nfq/nfqws2")) {
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/opt/zapret2/nfq/nfqws2" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/opt/zapret2/nfq/nfqws2" ]);
|
||||
} else {
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/nfqws2" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/usr/bin/nfqws2" ]);
|
||||
}
|
||||
clear_version_caches();
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("zapret2", "rollback", "Zapret2 rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else if (component == "tailscale") {
|
||||
let backup_bin = bdir + "/tailscale";
|
||||
if (!file_exists(backup_bin)) action_fail("tailscale", "rollback", "Tailscale backup binary is missing");
|
||||
command_success_from_args([ "cp", "-p", backup_bin, "/usr/sbin/tailscale" ]);
|
||||
command_success_from_args([ "chmod", "0755", "/usr/sbin/tailscale" ]);
|
||||
clear_version_caches();
|
||||
restart_tachyon_after_successful_change();
|
||||
action_success("tailscale", "rollback", "Tailscale rolled back to " + backup_version, backup_version, "", 1);
|
||||
}
|
||||
else {
|
||||
action_fail(component, "rollback", "Rollback not supported for component " + component);
|
||||
}
|
||||
}
|
||||
|
||||
function component_action(component, action) {
|
||||
component = normalize_component_name(component);
|
||||
action = as_string(action);
|
||||
@@ -2511,6 +2764,15 @@ function component_action(component, action) {
|
||||
action_fail(component != "" ? component : "unknown", action != "" ? action : "unknown", "Failed to create temporary directory");
|
||||
capture_tachyon_running_state();
|
||||
|
||||
if (action == "rollback") {
|
||||
rollback_component(component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action != "check_update" && action != "remove") {
|
||||
create_component_backup(component);
|
||||
}
|
||||
|
||||
if (component == "tachyon" && action == "check_update")
|
||||
check_tachyon();
|
||||
else if (component == "tachyon" && action == "install")
|
||||
|
||||
@@ -1151,6 +1151,25 @@ function build_system_info() {
|
||||
let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed";
|
||||
let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
|
||||
|
||||
let base_bdir = getenv("TACHYON_COMPONENT_BACKUPS_DIR") || "/etc/tachyon/component-backups";
|
||||
let read_backup_meta = function(comp) {
|
||||
let path = base_bdir + "/" + comp + "/metadata.json";
|
||||
let st = fs.stat(path);
|
||||
if (!st) return null;
|
||||
let data = as_string(fs.readfile(path));
|
||||
if (data == "") return null;
|
||||
try {
|
||||
let p = json(data);
|
||||
if (type(p) == "object" && p.version) return p;
|
||||
} catch (e) {}
|
||||
return null;
|
||||
};
|
||||
let sb_meta = read_backup_meta("sing_box");
|
||||
let zapret_meta = read_backup_meta("zapret");
|
||||
let zapret2_meta = read_backup_meta("zapret2");
|
||||
let byedpi_meta = read_backup_meta("byedpi");
|
||||
let tailscale_meta = read_backup_meta("tailscale");
|
||||
|
||||
return {
|
||||
tachyon_version: TACHYON_VERSION,
|
||||
tachyon_commit_sha: constants.TACHYON_COMMIT_SHA && !match(constants.TACHYON_COMMIT_SHA, /COMPILED/) ? constants.TACHYON_COMMIT_SHA : "",
|
||||
@@ -1163,14 +1182,24 @@ function build_system_info() {
|
||||
sing_box_lx,
|
||||
sing_box_tailscale: flags.tailscale,
|
||||
sing_box_repo_url,
|
||||
sing_box_backup_version: sb_meta ? as_string(sb_meta.version) : "",
|
||||
sing_box_backup_time: sb_meta ? int(sb_meta.timestamp || 0) : 0,
|
||||
zapret_version,
|
||||
zapret_installed,
|
||||
zapret_backup_version: zapret_meta ? as_string(zapret_meta.version) : "",
|
||||
zapret_backup_time: zapret_meta ? int(zapret_meta.timestamp || 0) : 0,
|
||||
zapret2_version,
|
||||
zapret2_installed,
|
||||
zapret2_backup_version: zapret2_meta ? as_string(zapret2_meta.version) : "",
|
||||
zapret2_backup_time: zapret2_meta ? int(zapret2_meta.timestamp || 0) : 0,
|
||||
byedpi_version,
|
||||
byedpi_installed,
|
||||
byedpi_backup_version: byedpi_meta ? as_string(byedpi_meta.version) : "",
|
||||
byedpi_backup_time: byedpi_meta ? int(byedpi_meta.timestamp || 0) : 0,
|
||||
tailscale_version,
|
||||
tailscale_installed,
|
||||
tailscale_backup_version: tailscale_meta ? as_string(tailscale_meta.version) : "",
|
||||
tailscale_backup_time: tailscale_meta ? int(tailscale_meta.timestamp || 0) : 0,
|
||||
openwrt_version: openwrt_release(),
|
||||
device_model,
|
||||
generated_at: int(clock()[0])
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
||||
ACTION_UC="$TACHYON_LIB/components/action.uc"
|
||||
RUNTIME_UC="$TACHYON_LIB/diagnostics/runtime.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
MOCK_BIN_DIR="$WORK_DIR/bin"
|
||||
MOCK_BACKUPS_DIR="$WORK_DIR/backups"
|
||||
MOCK_CONFIG_DIR="$WORK_DIR/config"
|
||||
MOCK_VAR_DIR="$WORK_DIR/run"
|
||||
mkdir -p "$MOCK_BIN_DIR" "$MOCK_BACKUPS_DIR" "$MOCK_CONFIG_DIR" "$MOCK_VAR_DIR"
|
||||
|
||||
# 1. Create a dummy sing-box binary v1.10.0
|
||||
cat > "$MOCK_BIN_DIR/sing-box" <<'EOF_SB'
|
||||
#!/bin/sh
|
||||
if [ "$1" = "version" ]; then
|
||||
echo "sing-box version 1.10.0"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF_SB
|
||||
chmod 0755 "$MOCK_BIN_DIR/sing-box"
|
||||
|
||||
# Create UCI state with component_backup_enabled = 1
|
||||
cat > "$WORK_DIR/uci.state" <<'EOF_UCI'
|
||||
tachyon.settings=settings
|
||||
tachyon.settings.component_backup_enabled=1
|
||||
EOF_UCI
|
||||
: > "$WORK_DIR/uci.log"
|
||||
|
||||
export TACHYON_LIB="$TACHYON_LIB"
|
||||
export TACHYON_COMPONENT_BACKUPS_DIR="$MOCK_BACKUPS_DIR"
|
||||
export TACHYON_CONFIG_DIR="$MOCK_CONFIG_DIR"
|
||||
export TACHYON_RUNTIME_STATE_DIR="$MOCK_VAR_DIR"
|
||||
export TACHYON_UCI_STATE_FILE="$WORK_DIR/uci.state"
|
||||
export TACHYON_UCI_LOG_FILE="$WORK_DIR/uci.log"
|
||||
export PATH="$MOCK_BIN_DIR:$PATH"
|
||||
|
||||
# Test 1: Triggering rollback when no backup exists should fail cleanly
|
||||
ROLLBACK_NO_BACKUP_OUT="$(TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$ACTION_UC" component-action sing_box rollback 2>&1 || true)"
|
||||
echo "$ROLLBACK_NO_BACKUP_OUT" | grep -Eq '"success":[[:space:]]*false' ||
|
||||
fail "rollback without backup must fail cleanly with JSON success:false"
|
||||
|
||||
# Test 2: Manually simulate backup creation or verify backup metadata reading
|
||||
mkdir -p "$MOCK_BACKUPS_DIR/sing_box"
|
||||
cat > "$MOCK_BIN_DIR/sing-box-old" <<'EOF_OLD'
|
||||
#!/bin/sh
|
||||
if [ "$1" = "version" ]; then
|
||||
echo "sing-box version 1.9.5"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF_OLD
|
||||
chmod 0755 "$MOCK_BIN_DIR/sing-box-old"
|
||||
cp -p "$MOCK_BIN_DIR/sing-box-old" "$MOCK_BACKUPS_DIR/sing_box/sing-box"
|
||||
cat > "$MOCK_BACKUPS_DIR/sing_box/metadata.json" <<'EOF_META'
|
||||
{
|
||||
"component": "sing_box",
|
||||
"version": "1.9.5",
|
||||
"variant": "extended",
|
||||
"marker": "extended",
|
||||
"timestamp": 1756728000
|
||||
}
|
||||
EOF_META
|
||||
|
||||
# Test 3: System info includes backup metadata
|
||||
SYS_INFO="$(TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$RUNTIME_UC" get-system-info)"
|
||||
echo "$SYS_INFO" | grep -Eq '"sing_box_backup_version":[[:space:]]*"1.9.5"' ||
|
||||
fail "system_info must report sing_box_backup_version from metadata"
|
||||
|
||||
# Test 4: Rollback replaces active binary with backup version
|
||||
ROLLBACK_OUT="$(TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$ACTION_UC" component-action sing_box rollback)"
|
||||
echo "$ROLLBACK_OUT" | grep -Eq '"success":[[:space:]]*true' ||
|
||||
fail "rollback with valid backup must return success:true"
|
||||
|
||||
printf 'component backup and rollback tests passed\n'
|
||||
Reference in New Issue
Block a user