feat(updates): добавить локальный бэкап и оффлайн-откат компонентов

This commit is contained in:
Dushnilin
2026-09-01 12:24:58 +03:00
parent 23aae78147
commit c432220af5
14 changed files with 591 additions and 10 deletions
+3
View File
@@ -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) {
+9
View File
@@ -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 [
+1
View File
@@ -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,
+9
View File
@@ -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."
+262
View File
@@ -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])
+90
View File
@@ -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'