feat(reset): кнопка сброса настроек в LuCI и дефолтное состояние при чистой установке
- новый модуль service/reset.uc (режим reset-settings, флаг no-start): остановка сервиса, чистка runtime/кэшей, сохранение tailscale, запись дефолтного конфига
This commit is contained in:
@@ -20,6 +20,8 @@ function stripIllegalReturn(code) {
|
||||
const files = await glob([
|
||||
'src/**/*.ts',
|
||||
'src/**/*.tsx',
|
||||
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js',
|
||||
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js',
|
||||
], {
|
||||
cwd: __dirname,
|
||||
ignore: [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,15 +60,13 @@ function parsePo(content) {
|
||||
try {
|
||||
msgid = JSON.parse(line.slice(6));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse msgid on line:', line);
|
||||
throw e;
|
||||
msgid = parseUnquotedPoValue(line, 'msgid');
|
||||
}
|
||||
} else if (line.startsWith('msgstr ') && msgid !== null) {
|
||||
try {
|
||||
msgstr = JSON.parse(line.slice(7));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse msgstr on line:', line);
|
||||
throw e;
|
||||
msgstr = parseUnquotedPoValue(line, 'msgstr');
|
||||
}
|
||||
translations.set(msgid, msgstr);
|
||||
msgid = null;
|
||||
@@ -78,6 +76,19 @@ function parsePo(content) {
|
||||
return translations;
|
||||
}
|
||||
|
||||
function parseUnquotedPoValue(line, prefix) {
|
||||
const match = line.match(new RegExp(`^${prefix} "(.+)"$`));
|
||||
if (!match) {
|
||||
console.error('Failed to parse line:', line);
|
||||
throw new Error('Failed to parse PO line');
|
||||
}
|
||||
return match[1]
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
function escapePoString(str) {
|
||||
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
+4686
-116
File diff suppressed because it is too large
Load Diff
+2762
-79
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -58,23 +58,23 @@ function formatSectionName(name: string): string {
|
||||
'Базовая связность': 'Базовая связность',
|
||||
'Заблокированные в РФ': 'Заблокированные ресурсы',
|
||||
'Свои домены': 'Собственные домены',
|
||||
'Custom': 'Пользовательский',
|
||||
Custom: 'Пользовательский',
|
||||
'ChatGPT / OpenAI': 'ChatGPT / OpenAI',
|
||||
'Gemini / Google AI': 'Gemini / Google AI',
|
||||
'X (Twitter)': 'Twitter (X)',
|
||||
'UDP / QUIC': 'UDP / QUIC протоколы',
|
||||
'Telegram': 'Telegram',
|
||||
'Instagram': 'Instagram',
|
||||
'YouTube': 'YouTube',
|
||||
'TikTok': 'TikTok',
|
||||
'Discord': 'Discord',
|
||||
'Facebook': 'Facebook',
|
||||
'WhatsApp': 'WhatsApp',
|
||||
'Netflix': 'Netflix',
|
||||
'Spotify': 'Spotify',
|
||||
'Twitch': 'Twitch',
|
||||
'Steam': 'Steam',
|
||||
'GitHub': 'GitHub',
|
||||
Telegram: 'Telegram',
|
||||
Instagram: 'Instagram',
|
||||
YouTube: 'YouTube',
|
||||
TikTok: 'TikTok',
|
||||
Discord: 'Discord',
|
||||
Facebook: 'Facebook',
|
||||
WhatsApp: 'WhatsApp',
|
||||
Netflix: 'Netflix',
|
||||
Spotify: 'Spotify',
|
||||
Twitch: 'Twitch',
|
||||
Steam: 'Steam',
|
||||
GitHub: 'GitHub',
|
||||
};
|
||||
|
||||
return sectionMap[clean] || clean;
|
||||
@@ -375,11 +375,7 @@ export function renderServiceCheckModal() {
|
||||
'padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'option',
|
||||
{ value: 'ALL' },
|
||||
`Все сервисы (${targets.length})`,
|
||||
),
|
||||
E('option', { value: 'ALL' }, `Все сервисы (${targets.length})`),
|
||||
...sectionNames.map((sec) => {
|
||||
const count = targets.filter((t) => t.section === sec).length;
|
||||
return E(
|
||||
@@ -524,11 +520,15 @@ export function renderServiceCheckModal() {
|
||||
}[] = [];
|
||||
|
||||
targets.forEach((item, index) => {
|
||||
const rowClass = index % 2 === 0 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2';
|
||||
const rowClass =
|
||||
index % 2 === 0 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2';
|
||||
const badge = renderStatusBadge('', false, true);
|
||||
|
||||
const tr = E('tr', { class: `tr ${rowClass}` }, [
|
||||
E('td', { class: 'td', style: 'word-break: break-all; padding: 8px;' }, [
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'word-break: break-all; padding: 8px;' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ style: 'font-weight: 600; font-size: 12px;' },
|
||||
@@ -536,7 +536,8 @@ export function renderServiceCheckModal() {
|
||||
),
|
||||
E('br'),
|
||||
E('small', { style: 'opacity: 0.8;' }, item.domain),
|
||||
]),
|
||||
],
|
||||
),
|
||||
E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [
|
||||
E(
|
||||
'span',
|
||||
@@ -554,20 +555,33 @@ export function renderServiceCheckModal() {
|
||||
]),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: right; font-size: 12px; padding: 8px 4px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: right; font-size: 12px; padding: 8px 4px;',
|
||||
},
|
||||
'-',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: right; font-size: 12px; padding: 8px 4px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: right; font-size: 12px; padding: 8px 4px;',
|
||||
},
|
||||
'-',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: right; font-size: 12px; padding: 8px 4px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: right; font-size: 12px; padding: 8px 4px;',
|
||||
},
|
||||
'-',
|
||||
),
|
||||
E('td', { class: 'td', style: 'text-align: center; padding: 8px 6px;' }, badge),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: center; padding: 8px 6px;' },
|
||||
badge,
|
||||
),
|
||||
]);
|
||||
|
||||
rowMap.push({ target: item, tr });
|
||||
@@ -582,7 +596,9 @@ export function renderServiceCheckModal() {
|
||||
!searchQuery ||
|
||||
target.domain.toLowerCase().includes(searchQuery) ||
|
||||
target.section.toLowerCase().includes(searchQuery) ||
|
||||
formatSectionName(target.section).toLowerCase().includes(searchQuery);
|
||||
formatSectionName(target.section)
|
||||
.toLowerCase()
|
||||
.includes(searchQuery);
|
||||
tr.style.display = matchesSection && matchesSearch ? '' : 'none';
|
||||
});
|
||||
};
|
||||
@@ -663,7 +679,10 @@ export function renderServiceCheckModal() {
|
||||
const customTr = E('tr', { class: 'tr cbi-rowstyle-1' }, [
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'word-break: break-all; padding: 8px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'word-break: break-all; padding: 8px;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
@@ -674,7 +693,13 @@ export function renderServiceCheckModal() {
|
||||
E('small', {}, cItem.domain),
|
||||
],
|
||||
),
|
||||
E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [
|
||||
E(
|
||||
'td',
|
||||
{
|
||||
class: 'td',
|
||||
style: 'font-size: 12px; padding: 8px;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
@@ -683,30 +708,50 @@ export function renderServiceCheckModal() {
|
||||
},
|
||||
formatRouteType(cItem.route_type),
|
||||
),
|
||||
]),
|
||||
E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [
|
||||
],
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{
|
||||
class: 'td',
|
||||
style: 'font-size: 12px; padding: 8px;',
|
||||
},
|
||||
[
|
||||
cItem.ip || '?',
|
||||
E('br'),
|
||||
E('small', {}, `${cItem.dns_ms}ms`),
|
||||
]),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: right; padding: 8px 4px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: right; padding: 8px 4px;',
|
||||
},
|
||||
cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : '-',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: right; padding: 8px 4px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: right; padding: 8px 4px;',
|
||||
},
|
||||
cItem.tls_ms > 0 ? `${cItem.tls_ms}` : '-',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: right; padding: 8px 4px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: right; padding: 8px 4px;',
|
||||
},
|
||||
cItem.http_ms > 0 ? `${cItem.http_ms}` : '-',
|
||||
),
|
||||
E(
|
||||
'td',
|
||||
{ class: 'td', style: 'text-align: center; padding: 8px 6px;' },
|
||||
{
|
||||
class: 'td',
|
||||
style: 'text-align: center; padding: 8px 6px;',
|
||||
},
|
||||
cBadge,
|
||||
),
|
||||
]);
|
||||
|
||||
+13
@@ -331,6 +331,7 @@ LOG_FILE="/tmp/tachyon-install.log"
|
||||
START_TIME=""
|
||||
TACHYON_WAS_ENABLED=0
|
||||
TACHYON_WAS_RUNNING=0
|
||||
TACHYON_WAS_INSTALLED=0
|
||||
TACHYON_LEGACY_DETECTED=0
|
||||
TACHYON_FORKOP_MIGRATION=0
|
||||
TACHYON_I18N_REQUESTED=0
|
||||
@@ -1337,6 +1338,7 @@ function installer_cleanup_legacy() {
|
||||
|
||||
print("TACHYON_WAS_ENABLED=", was_enabled ? "1" : "0", "\n");
|
||||
print("TACHYON_WAS_RUNNING=", was_running ? "1" : "0", "\n");
|
||||
print("TACHYON_WAS_INSTALLED=", tachyon_installed ? "1" : "0", "\n");
|
||||
print("TACHYON_LEGACY_DETECTED=", legacy_installed ? "1" : "0", "\n");
|
||||
return true;
|
||||
}
|
||||
@@ -1447,6 +1449,16 @@ function installer_post_install() {
|
||||
warn("Failed to start Tachyon after upgrade.\n");
|
||||
}
|
||||
|
||||
// Fresh installs must end up in a pristine default state: wipe leftover
|
||||
// caches and runtime state from previous installations and rewrite the
|
||||
// config from the bundled default. Upgrades and legacy migrations keep
|
||||
// the user configuration untouched.
|
||||
if (env("TACHYON_WAS_INSTALLED", "0") != "1" &&
|
||||
env("TACHYON_LEGACY_DETECTED", "0") != "1") {
|
||||
if (!run_args([ "timeout", "60", INSTALLER_TACHYON_BIN, "reset_settings", "no-start" ]))
|
||||
warn("Failed to reset Tachyon settings to defaults after a fresh install.\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2576,6 +2588,7 @@ post_install() {
|
||||
fi
|
||||
|
||||
TACHYON_WAS_ENABLED="$TACHYON_WAS_ENABLED" TACHYON_WAS_RUNNING="$TACHYON_WAS_RUNNING" \
|
||||
TACHYON_WAS_INSTALLED="$TACHYON_WAS_INSTALLED" TACHYON_LEGACY_DETECTED="$TACHYON_LEGACY_DETECTED" \
|
||||
install_json_ucode installer-post-install ||
|
||||
fail "Failed to complete Tachyon post-install actions"
|
||||
}
|
||||
|
||||
@@ -7937,13 +7937,10 @@ var styles = `
|
||||
|
||||
.tachyon_dashboard-page__subscription-meta__title {
|
||||
flex: 0 1 auto;
|
||||
width: max-content;
|
||||
max-width: min(28ch, 30%);
|
||||
min-width: min-content;
|
||||
color: var(--text-color-high);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tachyon_dashboard-page__subscription-meta__facts {
|
||||
@@ -10066,23 +10063,23 @@ function formatSectionName(name) {
|
||||
"\u0411\u0430\u0437\u043E\u0432\u0430\u044F \u0441\u0432\u044F\u0437\u043D\u043E\u0441\u0442\u044C": "\u0411\u0430\u0437\u043E\u0432\u0430\u044F \u0441\u0432\u044F\u0437\u043D\u043E\u0441\u0442\u044C",
|
||||
"\u0417\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0432 \u0420\u0424": "\u0417\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044B",
|
||||
"\u0421\u0432\u043E\u0438 \u0434\u043E\u043C\u0435\u043D\u044B": "\u0421\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0435 \u0434\u043E\u043C\u0435\u043D\u044B",
|
||||
"Custom": "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439",
|
||||
Custom: "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439",
|
||||
"ChatGPT / OpenAI": "ChatGPT / OpenAI",
|
||||
"Gemini / Google AI": "Gemini / Google AI",
|
||||
"X (Twitter)": "Twitter (X)",
|
||||
"UDP / QUIC": "UDP / QUIC \u043F\u0440\u043E\u0442\u043E\u043A\u043E\u043B\u044B",
|
||||
"Telegram": "Telegram",
|
||||
"Instagram": "Instagram",
|
||||
"YouTube": "YouTube",
|
||||
"TikTok": "TikTok",
|
||||
"Discord": "Discord",
|
||||
"Facebook": "Facebook",
|
||||
"WhatsApp": "WhatsApp",
|
||||
"Netflix": "Netflix",
|
||||
"Spotify": "Spotify",
|
||||
"Twitch": "Twitch",
|
||||
"Steam": "Steam",
|
||||
"GitHub": "GitHub"
|
||||
Telegram: "Telegram",
|
||||
Instagram: "Instagram",
|
||||
YouTube: "YouTube",
|
||||
TikTok: "TikTok",
|
||||
Discord: "Discord",
|
||||
Facebook: "Facebook",
|
||||
WhatsApp: "WhatsApp",
|
||||
Netflix: "Netflix",
|
||||
Spotify: "Spotify",
|
||||
Twitch: "Twitch",
|
||||
Steam: "Steam",
|
||||
GitHub: "GitHub"
|
||||
};
|
||||
return sectionMap[clean] || clean;
|
||||
}
|
||||
@@ -10328,11 +10325,7 @@ function renderServiceCheckModal() {
|
||||
style: "padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"option",
|
||||
{ value: "ALL" },
|
||||
`\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044B (${targets.length})`
|
||||
),
|
||||
E("option", { value: "ALL" }, `\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044B (${targets.length})`),
|
||||
...sectionNames.map((sec) => {
|
||||
const count = targets.filter((t) => t.section === sec).length;
|
||||
return E(
|
||||
@@ -10460,7 +10453,10 @@ function renderServiceCheckModal() {
|
||||
const rowClass = index % 2 === 0 ? "cbi-rowstyle-1" : "cbi-rowstyle-2";
|
||||
const badge = renderStatusBadge("", false, true);
|
||||
const tr = E("tr", { class: `tr ${rowClass}` }, [
|
||||
E("td", { class: "td", style: "word-break: break-all; padding: 8px;" }, [
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "word-break: break-all; padding: 8px;" },
|
||||
[
|
||||
E(
|
||||
"span",
|
||||
{ style: "font-weight: 600; font-size: 12px;" },
|
||||
@@ -10468,7 +10464,8 @@ function renderServiceCheckModal() {
|
||||
),
|
||||
E("br"),
|
||||
E("small", { style: "opacity: 0.8;" }, item.domain)
|
||||
]),
|
||||
]
|
||||
),
|
||||
E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [
|
||||
E(
|
||||
"span",
|
||||
@@ -10486,20 +10483,33 @@ function renderServiceCheckModal() {
|
||||
]),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: right; font-size: 12px; padding: 8px 4px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: right; font-size: 12px; padding: 8px 4px;"
|
||||
},
|
||||
"-"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: right; font-size: 12px; padding: 8px 4px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: right; font-size: 12px; padding: 8px 4px;"
|
||||
},
|
||||
"-"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: right; font-size: 12px; padding: 8px 4px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: right; font-size: 12px; padding: 8px 4px;"
|
||||
},
|
||||
"-"
|
||||
),
|
||||
E("td", { class: "td", style: "text-align: center; padding: 8px 6px;" }, badge)
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: center; padding: 8px 6px;" },
|
||||
badge
|
||||
)
|
||||
]);
|
||||
rowMap.push({ target: item, tr });
|
||||
tableBody.appendChild(tr);
|
||||
@@ -10567,7 +10577,10 @@ function renderServiceCheckModal() {
|
||||
const customTr = E("tr", { class: "tr cbi-rowstyle-1" }, [
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "word-break: break-all; padding: 8px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "word-break: break-all; padding: 8px;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"span",
|
||||
@@ -10578,7 +10591,13 @@ function renderServiceCheckModal() {
|
||||
E("small", {}, cItem.domain)
|
||||
]
|
||||
),
|
||||
E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [
|
||||
E(
|
||||
"td",
|
||||
{
|
||||
class: "td",
|
||||
style: "font-size: 12px; padding: 8px;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"span",
|
||||
{
|
||||
@@ -10587,30 +10606,50 @@ function renderServiceCheckModal() {
|
||||
},
|
||||
formatRouteType(cItem.route_type)
|
||||
)
|
||||
]),
|
||||
E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [
|
||||
]
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{
|
||||
class: "td",
|
||||
style: "font-size: 12px; padding: 8px;"
|
||||
},
|
||||
[
|
||||
cItem.ip || "?",
|
||||
E("br"),
|
||||
E("small", {}, `${cItem.dns_ms}ms`)
|
||||
]),
|
||||
]
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: right; padding: 8px 4px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: right; padding: 8px 4px;"
|
||||
},
|
||||
cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : "-"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: right; padding: 8px 4px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: right; padding: 8px 4px;"
|
||||
},
|
||||
cItem.tls_ms > 0 ? `${cItem.tls_ms}` : "-"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: right; padding: 8px 4px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: right; padding: 8px 4px;"
|
||||
},
|
||||
cItem.http_ms > 0 ? `${cItem.http_ms}` : "-"
|
||||
),
|
||||
E(
|
||||
"td",
|
||||
{ class: "td", style: "text-align: center; padding: 8px 6px;" },
|
||||
{
|
||||
class: "td",
|
||||
style: "text-align: center; padding: 8px 6px;"
|
||||
},
|
||||
cBadge
|
||||
)
|
||||
]);
|
||||
|
||||
@@ -376,6 +376,82 @@ function createWatchdogStatusWidget() {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function createResetSettingsWidget() {
|
||||
const wrapper = E("div", {
|
||||
id: "tachyon-reset-settings-widget",
|
||||
style: "display:flex;align-items:center;gap:12px;padding:4px 0;flex-wrap:wrap;",
|
||||
});
|
||||
|
||||
const btnReset = E("button", {
|
||||
class: "btn cbi-button cbi-button-negative",
|
||||
type: "button",
|
||||
});
|
||||
btnReset.textContent = _("Reset Settings");
|
||||
|
||||
const msgEl = E("span", {
|
||||
style: "font-size:12px;color:var(--text-color-medium,#888);",
|
||||
});
|
||||
|
||||
function performReset() {
|
||||
if (btnReset.disabled)
|
||||
return;
|
||||
btnReset.disabled = true;
|
||||
msgEl.textContent = _("Resetting\u2026");
|
||||
fs.exec("/usr/bin/tachyon", ["reset_settings"])
|
||||
.then(function (res) {
|
||||
let success = false;
|
||||
try {
|
||||
success = JSON.parse((res && res.stdout) || "").success !== false;
|
||||
} catch (e) {
|
||||
success = false;
|
||||
}
|
||||
if (!success)
|
||||
throw new Error("reset failed");
|
||||
msgEl.textContent = "";
|
||||
ui.addNotification(null, E("p", {}, _("Settings have been reset to defaults. The page will reload.")), "info");
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(function () {
|
||||
msgEl.textContent = _("Failed to reset settings");
|
||||
btnReset.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
btnReset.addEventListener("click", function () {
|
||||
ui.showModal(
|
||||
_("Reset Settings"),
|
||||
[
|
||||
E("p", {}, _("This will erase all Tachyon settings and restore the factory defaults. The service will restart. This action cannot be undone.")),
|
||||
E("div", { class: "button-row" }, [
|
||||
E("button", {
|
||||
class: "btn cbi-button cbi-button-neutral",
|
||||
type: "button",
|
||||
click: function () {
|
||||
ui.hideModal();
|
||||
},
|
||||
}, _("Cancel")),
|
||||
E("button", {
|
||||
class: "btn cbi-button cbi-button-negative",
|
||||
type: "button",
|
||||
click: function () {
|
||||
ui.hideModal();
|
||||
performReset();
|
||||
},
|
||||
}, _("Reset")),
|
||||
]),
|
||||
],
|
||||
"cbi-modal",
|
||||
);
|
||||
});
|
||||
|
||||
wrapper.appendChild(btnReset);
|
||||
wrapper.appendChild(msgEl);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function createSmartDetectSectionsWidget(section_id) {
|
||||
const TESTABLE_ACTIONS = ["connection", "proxy", "outbound", "vpn", "zapret", "zapret2", "byedpi"];
|
||||
const allSections = (uci.sections(UCI_PACKAGE, "section") || [])
|
||||
@@ -1217,6 +1293,20 @@ function createSettingsContent(section, capabilities) {
|
||||
o.default = "0";
|
||||
o.rmempty = false;
|
||||
|
||||
const resetOpt = section.taboption(
|
||||
"advanced",
|
||||
form.DummyValue,
|
||||
"_reset_settings",
|
||||
_("Reset Settings"),
|
||||
_(
|
||||
"Restores the Tachyon factory defaults: all rules, subscriptions, custom DNS servers and other options will be lost.",
|
||||
),
|
||||
);
|
||||
resetOpt.rawhtml = true;
|
||||
resetOpt.cfgvalue = function () {
|
||||
return createResetSettingsWidget();
|
||||
};
|
||||
|
||||
o = section.taboption(
|
||||
"ai",
|
||||
form.Flag,
|
||||
|
||||
+2374
-88
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,9 @@ define Package/tachyon/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/tachyon
|
||||
$(CP) ./files/usr/lib/. $(1)/usr/lib/tachyon/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/lib/tachyon/defaults
|
||||
$(INSTALL_CONF) ./files/etc/config/tachyon $(1)/usr/lib/tachyon/defaults/config
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
|
||||
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/99-tachyon-wan-monitor $(1)/etc/hotplug.d/iface/99-tachyon-wan-monitor
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ function restore_dnsmasq_failsafe_on_loader_failure(command) {
|
||||
command = as_string(command);
|
||||
if (command == "start" || command == "stop" || command == "restart" ||
|
||||
command == "reload" || command == "uninstall" ||
|
||||
command == "reset_settings" ||
|
||||
command == "dnsmasq_restore" || command == "restore_dnsmasq")
|
||||
restore_dnsmasq_failsafe();
|
||||
}
|
||||
@@ -64,6 +65,8 @@ function show_help() {
|
||||
print(" restart Restart Tachyon service\n");
|
||||
print(" enable Enable Tachyon autostart\n");
|
||||
print(" disable Disable Tachyon autostart\n");
|
||||
print(" reset_settings [no-start]\n");
|
||||
print(" Reset Tachyon settings to factory defaults\n");
|
||||
print(" uninstall Remove Tachyon files installed outside opkg/apk\n");
|
||||
print(" main Run main Tachyon process\n");
|
||||
print(" list_update Update remote lists\n");
|
||||
@@ -160,6 +163,7 @@ function command_spec(command) {
|
||||
enable: [ "service/lifecycle.uc", "enable", 0 ],
|
||||
disable: [ "service/lifecycle.uc", "disable", 0 ],
|
||||
uninstall: [ "service/uninstall.uc", "uninstall", 1 ],
|
||||
reset_settings: [ "service/reset.uc", "reset-settings", 1 ],
|
||||
dnsmasq_restore: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
|
||||
restore_dnsmasq: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
|
||||
main: [ "service/lifecycle.uc", "main", 0 ],
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
|
||||
function as_string(value) {
|
||||
return value == null ? "" : "" + value;
|
||||
}
|
||||
|
||||
function env(name, fallback) {
|
||||
let value = getenv(name);
|
||||
return value == null ? as_string(fallback) : as_string(value);
|
||||
}
|
||||
|
||||
function shell_quote(value) {
|
||||
return "'" + replace(as_string(value), /'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
function command_from_args(args) {
|
||||
let parts = [];
|
||||
for (let arg in args)
|
||||
push(parts, shell_quote(arg));
|
||||
return join(" ", parts);
|
||||
}
|
||||
|
||||
function normalize_status(status) {
|
||||
status = int(status);
|
||||
if (status == -1)
|
||||
return 255;
|
||||
let signal = status & 127;
|
||||
if (signal != 0)
|
||||
return 128 + signal;
|
||||
return (status >> 8) & 255;
|
||||
}
|
||||
|
||||
function command_success_from_args(args) {
|
||||
return normalize_status(system(command_from_args(args) + " >/dev/null 2>&1")) == 0;
|
||||
}
|
||||
|
||||
function log_message(message) {
|
||||
system(command_from_args([ "logger", "-t", CONFIG_NAME, "[info] " + message ]) + " >/dev/null 2>&1");
|
||||
}
|
||||
|
||||
const CONFIG_NAME = env("TACHYON_CONFIG_NAME", "tachyon");
|
||||
const LIB_DIR = env("TACHYON_LIB", "/usr/lib/tachyon");
|
||||
const BIN_PATH = env("TACHYON_BIN", "/usr/bin/tachyon");
|
||||
const INIT_PATH = env("TACHYON_SERVICE_INIT", "/etc/init.d/" + CONFIG_NAME);
|
||||
const CONFIG_PATH = env("TACHYON_CONFIG_PATH", "/etc/config/" + CONFIG_NAME);
|
||||
const PERSISTENT_DIR = env("TACHYON_PERSISTENT_DIR", "/etc/" + CONFIG_NAME);
|
||||
const RUNTIME_STATE_DIR = env("TACHYON_RUNTIME_STATE_DIR", "/var/run/" + CONFIG_NAME);
|
||||
const SING_BOX_TMP_DIR = env("TACHYON_SING_BOX_TMP_DIR", "/tmp/sing-box");
|
||||
const SING_BOX_CONFIG_PATH = env("TACHYON_SING_BOX_CONFIG_PATH", "/etc/sing-box/config.json");
|
||||
|
||||
const PRESERVED_PERSISTENT_ENTRIES = {
|
||||
tailscale: 1
|
||||
};
|
||||
|
||||
function default_config_source() {
|
||||
for (let candidate in [
|
||||
"/rom/etc/config/" + CONFIG_NAME,
|
||||
LIB_DIR + "/defaults/" + CONFIG_NAME + ".conf",
|
||||
LIB_DIR + "/defaults/config"
|
||||
]) {
|
||||
candidate = as_string(candidate);
|
||||
if (fs.stat(candidate) != null)
|
||||
return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function remove_persistent_state() {
|
||||
if (fs.stat(PERSISTENT_DIR) == null)
|
||||
return true;
|
||||
|
||||
let entries = fs.lsdir(PERSISTENT_DIR);
|
||||
if (type(entries) != "array")
|
||||
return false;
|
||||
|
||||
for (let entry in entries) {
|
||||
entry = as_string(entry);
|
||||
if (PRESERVED_PERSISTENT_ENTRIES[entry])
|
||||
continue;
|
||||
if (!command_success_from_args([ "rm", "-rf", PERSISTENT_DIR + "/" + entry ]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function reset_settings(no_start) {
|
||||
command_success_from_args([ BIN_PATH, "stop" ]);
|
||||
command_success_from_args([ INIT_PATH, "stop" ]);
|
||||
|
||||
command_success_from_args([ "rm", "-rf",
|
||||
RUNTIME_STATE_DIR,
|
||||
SING_BOX_TMP_DIR,
|
||||
SING_BOX_CONFIG_PATH
|
||||
]);
|
||||
if (!remove_persistent_state()) {
|
||||
log_message("Reset settings failed: unable to clean " + PERSISTENT_DIR);
|
||||
print("{\"success\":false,\"message\":\"unable to clean persistent state\"}\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let source = default_config_source();
|
||||
if (source == null) {
|
||||
log_message("Reset settings failed: default config source not found");
|
||||
print("{\"success\":false,\"message\":\"default config source not found\"}\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!command_success_from_args([ "cp", "-f", source, CONFIG_PATH ])) {
|
||||
log_message("Reset settings failed: unable to write default config");
|
||||
print("{\"success\":false,\"message\":\"unable to write default config\"}\n");
|
||||
return 1;
|
||||
}
|
||||
command_success_from_args([ "chmod", "600", CONFIG_PATH ]);
|
||||
|
||||
log_message("Settings have been reset to defaults");
|
||||
print("{\"success\":true}\n");
|
||||
|
||||
if (no_start == "1")
|
||||
return 0;
|
||||
|
||||
let start_status = command_success_from_args([ INIT_PATH, "start" ]) ? 0 : 1;
|
||||
if (start_status != 0) {
|
||||
log_message("Reset settings: service failed to start after reset");
|
||||
print("{\"success\":true,\"started\":false}\n");
|
||||
}
|
||||
return start_status;
|
||||
}
|
||||
|
||||
let mode = as_string(ARGV[0]);
|
||||
let no_start = "0";
|
||||
for (let i = 1; i < length(ARGV); i++) {
|
||||
let arg = as_string(ARGV[i]);
|
||||
if (arg == "no-start" || arg == "--no-start")
|
||||
no_start = "1";
|
||||
}
|
||||
|
||||
if (mode == "reset-settings")
|
||||
exit(reset_settings(no_start));
|
||||
|
||||
warn("Usage: service/reset.uc reset-settings [no-start]\n");
|
||||
exit(1);
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
RESET_UC="$TACHYON_LIB/service/reset.uc"
|
||||
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -r "$RESET_UC" ] || fail "service/reset.uc must own reset-to-defaults logic"
|
||||
[ -r "$TACHYON_BIN" ] || fail "tachyon entrypoint is missing"
|
||||
|
||||
grep -Fq 'reset_settings: [ "service/reset.uc", "reset-settings", 1 ]' "$TACHYON_BIN" ||
|
||||
fail "tachyon entrypoint must dispatch reset_settings to service/reset.uc"
|
||||
grep -Fq 'reset_settings' "$TACHYON_BIN" ||
|
||||
fail "tachyon entrypoint must list reset_settings in the command table"
|
||||
|
||||
mkdir -p "$WORK_DIR/lib/defaults"
|
||||
cat >"$WORK_DIR/default-config" <<'EOF'
|
||||
config settings 'settings'
|
||||
option config_version '1.0.5'
|
||||
option dns_type 'udp'
|
||||
option shutdown_correctly '0'
|
||||
EOF
|
||||
cp "$WORK_DIR/default-config" "$WORK_DIR/lib/defaults/config"
|
||||
|
||||
mkdir -p \
|
||||
"$WORK_DIR/persistent/subscription-cache" \
|
||||
"$WORK_DIR/persistent/rulesets" \
|
||||
"$WORK_DIR/persistent/tailscale/server-main"
|
||||
printf '%s\n' 'stale-cache' >"$WORK_DIR/persistent/subscription-cache/cache.json"
|
||||
printf '%s\n' 'stale-ruleset' >"$WORK_DIR/persistent/rulesets/community.srs"
|
||||
printf '%s\n' 'tailscale-identity' >"$WORK_DIR/persistent/tailscale/server-main/node.key"
|
||||
mkdir -p "$WORK_DIR/runtime" "$WORK_DIR/sing-box-tmp"
|
||||
printf '%s\n' '{"log":{}}' >"$WORK_DIR/sing-box-tmp/config.json"
|
||||
mkdir -p "$(dirname "$WORK_DIR/sing-box-config")"
|
||||
printf '%s\n' '{"log":{}}' >"$WORK_DIR/sing-box-config"
|
||||
printf '%s\n' 'custom-user-config' >"$WORK_DIR/config"
|
||||
|
||||
: >"$WORK_DIR/bin.log"
|
||||
: >"$WORK_DIR/init.log"
|
||||
cat >"$WORK_DIR/fake-bin" <<'SH'
|
||||
#!/usr/bin/env sh
|
||||
printf '%s\n' "$*" >> "$TACHYON_RESET_BIN_LOG"
|
||||
exit 0
|
||||
SH
|
||||
chmod 0755 "$WORK_DIR/fake-bin"
|
||||
cat >"$WORK_DIR/fake-init" <<'SH'
|
||||
#!/usr/bin/env sh
|
||||
printf '%s\n' "$*" >> "$TACHYON_RESET_INIT_LOG"
|
||||
exit 0
|
||||
SH
|
||||
chmod 0755 "$WORK_DIR/fake-init"
|
||||
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" "$RESET_UC" reset-settings no-start >"$WORK_DIR/result.json"
|
||||
grep -Fq '"success":true' "$WORK_DIR/result.json" ||
|
||||
fail "reset-settings must report success"
|
||||
|
||||
grep -Fxq 'stop' "$WORK_DIR/bin.log" ||
|
||||
fail "reset must stop the service through the backend entrypoint"
|
||||
grep -Fxq 'stop' "$WORK_DIR/init.log" ||
|
||||
fail "reset must stop the service through init.d"
|
||||
if grep -Fxq 'start' "$WORK_DIR/init.log"; then
|
||||
fail "reset no-start must not start the service"
|
||||
fi
|
||||
|
||||
cmp -s "$WORK_DIR/config" "$WORK_DIR/default-config" ||
|
||||
fail "reset must restore the config file to factory defaults"
|
||||
[ "$(stat -c %a "$WORK_DIR/config")" = "600" ] ||
|
||||
fail "reset config must be chmod 600"
|
||||
|
||||
[ ! -e "$WORK_DIR/persistent/subscription-cache" ] ||
|
||||
fail "reset must remove stale subscription caches"
|
||||
[ ! -e "$WORK_DIR/persistent/rulesets" ] ||
|
||||
fail "reset must remove stale ruleset caches"
|
||||
[ "$(cat "$WORK_DIR/persistent/tailscale/server-main/node.key")" = 'tailscale-identity' ] ||
|
||||
fail "reset must preserve the Tailscale identity"
|
||||
[ ! -e "$WORK_DIR/runtime" ] ||
|
||||
fail "reset must remove the runtime state directory"
|
||||
[ ! -e "$WORK_DIR/sing-box-tmp" ] ||
|
||||
fail "reset must remove the sing-box temporary directory"
|
||||
[ ! -e "$WORK_DIR/sing-box-config" ] ||
|
||||
fail "reset must remove the sing-box config file"
|
||||
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" "$RESET_UC" reset-settings >"$WORK_DIR/result.json"
|
||||
grep -Fxq 'start' "$WORK_DIR/init.log" ||
|
||||
fail "reset without no-start must start the service"
|
||||
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" "$RESET_UC" bogus-mode 2>"$WORK_DIR/usage.err" &&
|
||||
fail "reset module must reject unknown modes"
|
||||
grep -Fq 'Usage' "$WORK_DIR/usage.err" ||
|
||||
fail "reset module must print usage for unknown modes"
|
||||
|
||||
grep -Fq 'reset_settings [no-start]' "$TACHYON_BIN" ||
|
||||
fail "tachyon help must document reset_settings"
|
||||
grep -Fq 'reset_settings' "$TACHYON_BIN" ||
|
||||
fail "tachyon failsafe dnsmasq restore must cover reset_settings"
|
||||
|
||||
# The installer must wipe leftover state on a fresh install but keep user
|
||||
# configuration on upgrades and legacy migrations.
|
||||
grep -Fq 'TACHYON_WAS_INSTALLED' "$INSTALLER" ||
|
||||
fail "installer must remember whether Tachyon was installed before"
|
||||
grep -Fq 'reset_settings", "no-start"' "$INSTALLER" ||
|
||||
fail "installer post-install must delegate fresh-install defaults to reset_settings"
|
||||
if grep -n -E 'reset_settings|TACHYON_WAS_INSTALLED' "$INSTALLER" | grep -qE 'rm -f |uci |sed -i '; then
|
||||
fail "installer must not own reset/state shell logic"
|
||||
fi
|
||||
|
||||
printf 'reset settings contract checks passed\n'
|
||||
Reference in New Issue
Block a user