feat(reset): кнопка сброса настроек в LuCI и дефолтное состояние при чистой установке

- новый модуль service/reset.uc (режим reset-settings, флаг no-start): остановка сервиса, чистка runtime/кэшей, сохранение tailscale, запись дефолтного конфига
This commit is contained in:
Dushnilin
2026-08-13 21:06:39 +03:00
parent 9b4767a5af
commit 305e14d44e
16 changed files with 16626 additions and 570 deletions
+2
View File
@@ -20,6 +20,8 @@ function stripIllegalReturn(code) {
const files = await glob([ const files = await glob([
'src/**/*.ts', 'src/**/*.ts',
'src/**/*.tsx', '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, cwd: __dirname,
ignore: [ ignore: [
File diff suppressed because it is too large Load Diff
+15 -4
View File
@@ -60,15 +60,13 @@ function parsePo(content) {
try { try {
msgid = JSON.parse(line.slice(6)); msgid = JSON.parse(line.slice(6));
} catch (e) { } catch (e) {
console.error('Failed to parse msgid on line:', line); msgid = parseUnquotedPoValue(line, 'msgid');
throw e;
} }
} else if (line.startsWith('msgstr ') && msgid !== null) { } else if (line.startsWith('msgstr ') && msgid !== null) {
try { try {
msgstr = JSON.parse(line.slice(7)); msgstr = JSON.parse(line.slice(7));
} catch (e) { } catch (e) {
console.error('Failed to parse msgstr on line:', line); msgstr = parseUnquotedPoValue(line, 'msgstr');
throw e;
} }
translations.set(msgid, msgstr); translations.set(msgid, msgstr);
msgid = null; msgid = null;
@@ -78,6 +76,19 @@ function parsePo(content) {
return translations; 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) { function escapePoString(str) {
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
} }
File diff suppressed because it is too large Load Diff
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', 'ChatGPT / OpenAI': 'ChatGPT / OpenAI',
'Gemini / Google AI': 'Gemini / Google AI', 'Gemini / Google AI': 'Gemini / Google AI',
'X (Twitter)': 'Twitter (X)', 'X (Twitter)': 'Twitter (X)',
'UDP / QUIC': 'UDP / QUIC протоколы', 'UDP / QUIC': 'UDP / QUIC протоколы',
'Telegram': 'Telegram', Telegram: 'Telegram',
'Instagram': 'Instagram', Instagram: 'Instagram',
'YouTube': 'YouTube', YouTube: 'YouTube',
'TikTok': 'TikTok', TikTok: 'TikTok',
'Discord': 'Discord', Discord: 'Discord',
'Facebook': 'Facebook', Facebook: 'Facebook',
'WhatsApp': 'WhatsApp', WhatsApp: 'WhatsApp',
'Netflix': 'Netflix', Netflix: 'Netflix',
'Spotify': 'Spotify', Spotify: 'Spotify',
'Twitch': 'Twitch', Twitch: 'Twitch',
'Steam': 'Steam', Steam: 'Steam',
'GitHub': 'GitHub', GitHub: 'GitHub',
}; };
return sectionMap[clean] || clean; return sectionMap[clean] || clean;
@@ -375,11 +375,7 @@ export function renderServiceCheckModal() {
'padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;', 'padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;',
}, },
[ [
E( E('option', { value: 'ALL' }, `Все сервисы (${targets.length})`),
'option',
{ value: 'ALL' },
`Все сервисы (${targets.length})`,
),
...sectionNames.map((sec) => { ...sectionNames.map((sec) => {
const count = targets.filter((t) => t.section === sec).length; const count = targets.filter((t) => t.section === sec).length;
return E( return E(
@@ -524,19 +520,24 @@ export function renderServiceCheckModal() {
}[] = []; }[] = [];
targets.forEach((item, index) => { 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 badge = renderStatusBadge('', false, true);
const tr = E('tr', { class: `tr ${rowClass}` }, [ const tr = E('tr', { class: `tr ${rowClass}` }, [
E('td', { class: 'td', style: 'word-break: break-all; padding: 8px;' }, [ E(
E( 'td',
'span', { class: 'td', style: 'word-break: break-all; padding: 8px;' },
{ style: 'font-weight: 600; font-size: 12px;' }, [
formatSectionName(item.section), E(
), 'span',
E('br'), { style: 'font-weight: 600; font-size: 12px;' },
E('small', { style: 'opacity: 0.8;' }, item.domain), formatSectionName(item.section),
]), ),
E('br'),
E('small', { style: 'opacity: 0.8;' }, item.domain),
],
),
E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [ E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [
E( E(
'span', 'span',
@@ -554,20 +555,33 @@ export function renderServiceCheckModal() {
]), ]),
E( E(
'td', '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( E(
'td', '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( E(
'td', '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 }); rowMap.push({ target: item, tr });
@@ -582,7 +596,9 @@ export function renderServiceCheckModal() {
!searchQuery || !searchQuery ||
target.domain.toLowerCase().includes(searchQuery) || target.domain.toLowerCase().includes(searchQuery) ||
target.section.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'; tr.style.display = matchesSection && matchesSearch ? '' : 'none';
}); });
}; };
@@ -663,7 +679,10 @@ export function renderServiceCheckModal() {
const customTr = E('tr', { class: 'tr cbi-rowstyle-1' }, [ const customTr = E('tr', { class: 'tr cbi-rowstyle-1' }, [
E( E(
'td', 'td',
{ class: 'td', style: 'word-break: break-all; padding: 8px;' }, {
class: 'td',
style: 'word-break: break-all; padding: 8px;',
},
[ [
E( E(
'span', 'span',
@@ -674,39 +693,65 @@ export function renderServiceCheckModal() {
E('small', {}, cItem.domain), E('small', {}, cItem.domain),
], ],
), ),
E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [
E(
'span',
{
class: 'badge cbi-value-title',
style: 'font-size: 11px; padding: 2px 6px;',
},
formatRouteType(cItem.route_type),
),
]),
E('td', { class: 'td', style: 'font-size: 12px; padding: 8px;' }, [
cItem.ip || '?',
E('br'),
E('small', {}, `${cItem.dns_ms}ms`),
]),
E( E(
'td', 'td',
{ class: 'td', style: 'text-align: right; padding: 8px 4px;' }, {
class: 'td',
style: 'font-size: 12px; padding: 8px;',
},
[
E(
'span',
{
class: 'badge cbi-value-title',
style: 'font-size: 11px; padding: 2px 6px;',
},
formatRouteType(cItem.route_type),
),
],
),
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;',
},
cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : '-', cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : '-',
), ),
E( E(
'td', '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}` : '-', cItem.tls_ms > 0 ? `${cItem.tls_ms}` : '-',
), ),
E( E(
'td', '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}` : '-', cItem.http_ms > 0 ? `${cItem.http_ms}` : '-',
), ),
E( E(
'td', 'td',
{ class: 'td', style: 'text-align: center; padding: 8px 6px;' }, {
class: 'td',
style: 'text-align: center; padding: 8px 6px;',
},
cBadge, cBadge,
), ),
]); ]);
+13
View File
@@ -331,6 +331,7 @@ LOG_FILE="/tmp/tachyon-install.log"
START_TIME="" START_TIME=""
TACHYON_WAS_ENABLED=0 TACHYON_WAS_ENABLED=0
TACHYON_WAS_RUNNING=0 TACHYON_WAS_RUNNING=0
TACHYON_WAS_INSTALLED=0
TACHYON_LEGACY_DETECTED=0 TACHYON_LEGACY_DETECTED=0
TACHYON_FORKOP_MIGRATION=0 TACHYON_FORKOP_MIGRATION=0
TACHYON_I18N_REQUESTED=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_ENABLED=", was_enabled ? "1" : "0", "\n");
print("TACHYON_WAS_RUNNING=", was_running ? "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"); print("TACHYON_LEGACY_DETECTED=", legacy_installed ? "1" : "0", "\n");
return true; return true;
} }
@@ -1447,6 +1449,16 @@ function installer_post_install() {
warn("Failed to start Tachyon after upgrade.\n"); 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; return true;
} }
@@ -2576,6 +2588,7 @@ post_install() {
fi fi
TACHYON_WAS_ENABLED="$TACHYON_WAS_ENABLED" TACHYON_WAS_RUNNING="$TACHYON_WAS_RUNNING" \ 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 || install_json_ucode installer-post-install ||
fail "Failed to complete Tachyon post-install actions" fail "Failed to complete Tachyon post-install actions"
} }
@@ -7937,13 +7937,10 @@ var styles = `
.tachyon_dashboard-page__subscription-meta__title { .tachyon_dashboard-page__subscription-meta__title {
flex: 0 1 auto; flex: 0 1 auto;
width: max-content;
max-width: min(28ch, 30%);
min-width: min-content;
color: var(--text-color-high); color: var(--text-color-high);
font-weight: 700; font-weight: 700;
line-height: 1.25; line-height: 1.25;
overflow-wrap: anywhere; word-break: break-word;
} }
.tachyon_dashboard-page__subscription-meta__facts { .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", "\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", "\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", "\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", "ChatGPT / OpenAI": "ChatGPT / OpenAI",
"Gemini / Google AI": "Gemini / Google AI", "Gemini / Google AI": "Gemini / Google AI",
"X (Twitter)": "Twitter (X)", "X (Twitter)": "Twitter (X)",
"UDP / QUIC": "UDP / QUIC \u043F\u0440\u043E\u0442\u043E\u043A\u043E\u043B\u044B", "UDP / QUIC": "UDP / QUIC \u043F\u0440\u043E\u0442\u043E\u043A\u043E\u043B\u044B",
"Telegram": "Telegram", Telegram: "Telegram",
"Instagram": "Instagram", Instagram: "Instagram",
"YouTube": "YouTube", YouTube: "YouTube",
"TikTok": "TikTok", TikTok: "TikTok",
"Discord": "Discord", Discord: "Discord",
"Facebook": "Facebook", Facebook: "Facebook",
"WhatsApp": "WhatsApp", WhatsApp: "WhatsApp",
"Netflix": "Netflix", Netflix: "Netflix",
"Spotify": "Spotify", Spotify: "Spotify",
"Twitch": "Twitch", Twitch: "Twitch",
"Steam": "Steam", Steam: "Steam",
"GitHub": "GitHub" GitHub: "GitHub"
}; };
return sectionMap[clean] || clean; return sectionMap[clean] || clean;
} }
@@ -10328,11 +10325,7 @@ function renderServiceCheckModal() {
style: "padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;" style: "padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;"
}, },
[ [
E( E("option", { value: "ALL" }, `\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044B (${targets.length})`),
"option",
{ value: "ALL" },
`\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044B (${targets.length})`
),
...sectionNames.map((sec) => { ...sectionNames.map((sec) => {
const count = targets.filter((t) => t.section === sec).length; const count = targets.filter((t) => t.section === sec).length;
return E( return E(
@@ -10460,15 +10453,19 @@ function renderServiceCheckModal() {
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 badge = renderStatusBadge("", false, true);
const tr = E("tr", { class: `tr ${rowClass}` }, [ const tr = E("tr", { class: `tr ${rowClass}` }, [
E("td", { class: "td", style: "word-break: break-all; padding: 8px;" }, [ E(
E( "td",
"span", { class: "td", style: "word-break: break-all; padding: 8px;" },
{ style: "font-weight: 600; font-size: 12px;" }, [
formatSectionName(item.section) E(
), "span",
E("br"), { style: "font-weight: 600; font-size: 12px;" },
E("small", { style: "opacity: 0.8;" }, item.domain) formatSectionName(item.section)
]), ),
E("br"),
E("small", { style: "opacity: 0.8;" }, item.domain)
]
),
E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [ E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [
E( E(
"span", "span",
@@ -10486,20 +10483,33 @@ function renderServiceCheckModal() {
]), ]),
E( E(
"td", "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( E(
"td", "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( E(
"td", "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 }); rowMap.push({ target: item, tr });
tableBody.appendChild(tr); tableBody.appendChild(tr);
@@ -10567,7 +10577,10 @@ function renderServiceCheckModal() {
const customTr = E("tr", { class: "tr cbi-rowstyle-1" }, [ const customTr = E("tr", { class: "tr cbi-rowstyle-1" }, [
E( E(
"td", "td",
{ class: "td", style: "word-break: break-all; padding: 8px;" }, {
class: "td",
style: "word-break: break-all; padding: 8px;"
},
[ [
E( E(
"span", "span",
@@ -10578,39 +10591,65 @@ function renderServiceCheckModal() {
E("small", {}, cItem.domain) E("small", {}, cItem.domain)
] ]
), ),
E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [
E(
"span",
{
class: "badge cbi-value-title",
style: "font-size: 11px; padding: 2px 6px;"
},
formatRouteType(cItem.route_type)
)
]),
E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [
cItem.ip || "?",
E("br"),
E("small", {}, `${cItem.dns_ms}ms`)
]),
E( E(
"td", "td",
{ class: "td", style: "text-align: right; padding: 8px 4px;" }, {
class: "td",
style: "font-size: 12px; padding: 8px;"
},
[
E(
"span",
{
class: "badge cbi-value-title",
style: "font-size: 11px; padding: 2px 6px;"
},
formatRouteType(cItem.route_type)
)
]
),
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;"
},
cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : "-" cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : "-"
), ),
E( E(
"td", "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}` : "-" cItem.tls_ms > 0 ? `${cItem.tls_ms}` : "-"
), ),
E( E(
"td", "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}` : "-" cItem.http_ms > 0 ? `${cItem.http_ms}` : "-"
), ),
E( E(
"td", "td",
{ class: "td", style: "text-align: center; padding: 8px 6px;" }, {
class: "td",
style: "text-align: center; padding: 8px 6px;"
},
cBadge cBadge
) )
]); ]);
@@ -376,6 +376,82 @@ function createWatchdogStatusWidget() {
return wrapper; 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) { function createSmartDetectSectionsWidget(section_id) {
const TESTABLE_ACTIONS = ["connection", "proxy", "outbound", "vpn", "zapret", "zapret2", "byedpi"]; const TESTABLE_ACTIONS = ["connection", "proxy", "outbound", "vpn", "zapret", "zapret2", "byedpi"];
const allSections = (uci.sections(UCI_PACKAGE, "section") || []) const allSections = (uci.sections(UCI_PACKAGE, "section") || [])
@@ -1217,6 +1293,20 @@ function createSettingsContent(section, capabilities) {
o.default = "0"; o.default = "0";
o.rmempty = false; 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( o = section.taboption(
"ai", "ai",
form.Flag, form.Flag,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -148,6 +148,9 @@ define Package/tachyon/install
$(INSTALL_DIR) $(1)/usr/lib/tachyon $(INSTALL_DIR) $(1)/usr/lib/tachyon
$(CP) ./files/usr/lib/. $(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_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 $(INSTALL_BIN) ./files/etc/hotplug.d/iface/99-tachyon-wan-monitor $(1)/etc/hotplug.d/iface/99-tachyon-wan-monitor
+4
View File
@@ -51,6 +51,7 @@ function restore_dnsmasq_failsafe_on_loader_failure(command) {
command = as_string(command); command = as_string(command);
if (command == "start" || command == "stop" || command == "restart" || if (command == "start" || command == "stop" || command == "restart" ||
command == "reload" || command == "uninstall" || command == "reload" || command == "uninstall" ||
command == "reset_settings" ||
command == "dnsmasq_restore" || command == "restore_dnsmasq") command == "dnsmasq_restore" || command == "restore_dnsmasq")
restore_dnsmasq_failsafe(); restore_dnsmasq_failsafe();
} }
@@ -64,6 +65,8 @@ function show_help() {
print(" restart Restart Tachyon service\n"); print(" restart Restart Tachyon service\n");
print(" enable Enable Tachyon autostart\n"); print(" enable Enable Tachyon autostart\n");
print(" disable Disable 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(" uninstall Remove Tachyon files installed outside opkg/apk\n");
print(" main Run main Tachyon process\n"); print(" main Run main Tachyon process\n");
print(" list_update Update remote lists\n"); print(" list_update Update remote lists\n");
@@ -160,6 +163,7 @@ function command_spec(command) {
enable: [ "service/lifecycle.uc", "enable", 0 ], enable: [ "service/lifecycle.uc", "enable", 0 ],
disable: [ "service/lifecycle.uc", "disable", 0 ], disable: [ "service/lifecycle.uc", "disable", 0 ],
uninstall: [ "service/uninstall.uc", "uninstall", 1 ], uninstall: [ "service/uninstall.uc", "uninstall", 1 ],
reset_settings: [ "service/reset.uc", "reset-settings", 1 ],
dnsmasq_restore: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ], dnsmasq_restore: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
restore_dnsmasq: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ], restore_dnsmasq: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
main: [ "service/lifecycle.uc", "main", 0 ], main: [ "service/lifecycle.uc", "main", 0 ],
+143
View File
@@ -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);
+150
View File
@@ -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'