3150 lines
114 KiB
Bash
Executable File
3150 lines
114 KiB
Bash
Executable File
#!/bin/sh
|
|
# shellcheck shell=dash
|
|
|
|
INSTALLER_VERSION="2.0.1"
|
|
|
|
REPO_OWNER="Dushnilin"
|
|
REPO_NAME="tachyon"
|
|
|
|
# ─── TUI helpers (self-contained, no external deps) ─────────────────────────
|
|
# POSIX-compatible, works with dash/bash/mksh on OpenWrt.
|
|
# Degrades gracefully when TERM is dumb or stdout is not a terminal.
|
|
|
|
# ─── Color detection ──────────────────────────────────────────────────────────
|
|
# We use printf to resolve ESC sequences into actual bytes.
|
|
# Busybox printf on OpenWrt does not interpret \033 inside %s arguments,
|
|
# so the variables must contain the real ESC character (0x1b) at assignment time.
|
|
ESC="$(printf '\033')"
|
|
|
|
_tui_colors=0
|
|
if [ -t 1 ] 2>/dev/null; then
|
|
case "${TERM:-dumb}" in
|
|
dumb) _tui_colors=0 ;;
|
|
*) _tui_colors=1 ;;
|
|
esac
|
|
fi
|
|
|
|
if [ "$_tui_colors" -eq 1 ]; then
|
|
_c_reset="${ESC}[0m"
|
|
_c_bold="${ESC}[1m"
|
|
_c_dim="${ESC}[2m"
|
|
_c_red="${ESC}[31;1m"
|
|
_c_green="${ESC}[32;1m"
|
|
_c_yellow="${ESC}[33;1m"
|
|
_c_blue="${ESC}[34;1m"
|
|
_c_cyan="${ESC}[36;1m"
|
|
_c_magenta="${ESC}[35;1m"
|
|
_c_white="${ESC}[37;1m"
|
|
_c_bg_blue="${ESC}[44m"
|
|
_c_bg_green="${ESC}[42m"
|
|
_c_bg_red="${ESC}[41m"
|
|
_c_bg_yellow="${ESC}[43m"
|
|
_c_underline="${ESC}[4m"
|
|
else
|
|
_c_reset=''
|
|
_c_bold=''
|
|
_c_dim=''
|
|
_c_red=''
|
|
_c_green=''
|
|
_c_yellow=''
|
|
_c_blue=''
|
|
_c_cyan=''
|
|
_c_magenta=''
|
|
_c_white=''
|
|
_c_bg_blue=''
|
|
_c_bg_green=''
|
|
_c_bg_red=''
|
|
_c_bg_yellow=''
|
|
_c_underline=''
|
|
fi
|
|
|
|
# ─── Terminal width ──────────────────────────────────────────────────────────
|
|
_tui_width() {
|
|
if [ -t 1 ] 2>/dev/null && command -v stty >/dev/null 2>&1; then
|
|
w="$(stty size 2>/dev/null | awk '{print $2}')"
|
|
[ -n "$w" ] && [ "$w" -gt 0 ] 2>/dev/null && printf '%s' "$w" && return 0
|
|
fi
|
|
printf '72'
|
|
}
|
|
|
|
# ─── Horizontal line ─────────────────────────────────────────────────────────
|
|
_tui_hline() {
|
|
_w="$(_tui_width)"
|
|
_char="${1:--}"
|
|
_i=0
|
|
while [ "$_i" -lt "$_w" ]; do
|
|
printf '%s' "$_char"
|
|
_i=$((_i + 1))
|
|
done
|
|
}
|
|
|
|
# ─── Center text ──────────────────────────────────────────────────────────────
|
|
_tui_center() {
|
|
_text="$1"
|
|
_w="$(_tui_width)"
|
|
_len=${#_text}
|
|
_pad=$((_w - _len))
|
|
[ "$_pad" -lt 0 ] 2>/dev/null && _pad=0
|
|
_left=$((_pad / 2))
|
|
_right=$((_pad - _left))
|
|
_i=0
|
|
while [ "$_i" -lt "$_left" ]; do
|
|
printf ' '
|
|
_i=$((_i + 1))
|
|
done
|
|
printf '%s' "$_text"
|
|
_i=0
|
|
while [ "$_i" -lt "$_right" ]; do
|
|
printf ' '
|
|
_i=$((_i + 1))
|
|
done
|
|
}
|
|
|
|
# ─── Pad/truncate to width ──────────────────────────────────────────────────
|
|
_tui_pad() {
|
|
_text="$1"
|
|
_target="${2:-0}"
|
|
_len=${#_text}
|
|
_i=$_len
|
|
while [ "$_i" -lt "$_target" ]; do
|
|
printf ' '
|
|
_i=$((_i + 1))
|
|
done
|
|
}
|
|
|
|
# ─── Print banner ────────────────────────────────────────────────────────────
|
|
tui_banner() {
|
|
printf '\n'
|
|
printf ' %s%sTachyon Installer%s v%s%s\n' "$_c_cyan" "$_c_bold" "$_c_reset" "$_c_bold" "$INSTALLER_VERSION"
|
|
printf '%s' "$_c_reset"
|
|
printf '%s\n' ""
|
|
printf '%s%s%s\n' "$_c_dim" "$(_tui_hline '─')" "$_c_reset"
|
|
}
|
|
|
|
# ─── Print step header ──────────────────────────────────────────────────────
|
|
tui_step() {
|
|
_step_no="$1"
|
|
_step_total="$2"
|
|
_step_text="$3"
|
|
_step_icon=""
|
|
|
|
printf '\n'
|
|
printf ' %s%s[ %s/%s ]%s %s%s%s\n' \
|
|
"$_c_blue" "$_c_bold" \
|
|
"$_step_no" "$_step_total" \
|
|
"$_c_reset" \
|
|
"$_c_bold" "$_step_text" "$_c_reset"
|
|
printf ' %s%s\n' "$_c_dim" "$(_tui_hline '·')"
|
|
printf '%s' "$_c_reset"
|
|
}
|
|
|
|
# ─── Success message ─────────────────────────────────────────────────────────
|
|
tui_ok() {
|
|
printf ' %s%s✓%s %s\n' "$_c_green" "$_c_bold" "$_c_reset" "$1"
|
|
}
|
|
|
|
# ─── Warning message ─────────────────────────────────────────────────────────
|
|
tui_warn() {
|
|
printf ' %s%s⚠%s %s%s\n' "$_c_yellow" "$_c_bold" "$_c_reset" "$1" "$_c_reset" >&2
|
|
}
|
|
|
|
# ─── Error message ───────────────────────────────────────────────────────────
|
|
tui_err() {
|
|
printf ' %s%s✗%s %s%s\n' "$_c_red" "$_c_bold" "$_c_reset" "$1" "$_c_reset" >&2
|
|
printf ' %s%s Log: %s%s\n' "$_c_dim" "$_c_reset" "$LOG_FILE" "$_c_reset" >&2
|
|
}
|
|
|
|
# ─── Info message ────────────────────────────────────────────────────────────
|
|
tui_info() {
|
|
printf ' %s▸%s %s\n' "$_c_cyan" "$_c_reset" "$1"
|
|
}
|
|
|
|
# ─── Download progress ───────────────────────────────────────────────────────
|
|
tui_download() {
|
|
_label="$1"
|
|
_attempt="${2:-1}"
|
|
_max="${3:-3}"
|
|
printf ' %s%s↓%s %s %s(%s/%s)%s\n' \
|
|
"$_c_cyan" "$_c_bold" "$_c_reset" \
|
|
"$_label" \
|
|
"$_c_dim" "$_attempt" "$_max" "$_c_reset"
|
|
}
|
|
|
|
# ─── Progress bar ────────────────────────────────────────────────────────────
|
|
tui_progress() {
|
|
_current="$1"
|
|
_total="$2"
|
|
_label="${3:-}"
|
|
_w="$(_tui_width)"
|
|
_w=$((_w - 8))
|
|
[ "$_w" -lt 10 ] 2>/dev/null && _w=10
|
|
|
|
_pct=0
|
|
[ "$_total" -gt 0 ] 2>/dev/null && _pct=$((_current * 100 / _total))
|
|
|
|
_filled=$((_pct * _w / 100))
|
|
[ "$_filled" -gt "$_w" ] 2>/dev/null && _filled=$_w
|
|
_empty=$((_w - _filled))
|
|
|
|
printf '\r %s' "$_c_dim"
|
|
_i=0
|
|
while [ "$_i" -lt "$_filled" ]; do
|
|
printf '━'
|
|
_i=$((_i + 1))
|
|
done
|
|
_i=0
|
|
while [ "$_i" -lt "$_empty" ]; do
|
|
printf '─'
|
|
_i=$((_i + 1))
|
|
done
|
|
printf '%s' "$_c_reset"
|
|
printf ' %s%3d%%%s' "$_c_bold" "$_pct" "$_c_reset"
|
|
[ -n "$_label" ] && printf ' %s' "$_label"
|
|
}
|
|
|
|
# ─── Spinner (call in background, kill when done) ───────────────────────────
|
|
tui_spinner_start() {
|
|
_spinner_text="${1:-Working...}"
|
|
_spinner_chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
_spinner_pid=""
|
|
|
|
# Only spin on real terminals
|
|
[ -t 1 ] 2>/dev/null || { printf ' %s\n' "$_spinner_text"; return 0; }
|
|
|
|
(
|
|
while true; do
|
|
for _ch in $_spinner_chars; do
|
|
printf '\r %s%s%s %s' "$_c_cyan" "$_ch" "$_c_reset" "$_spinner_text"
|
|
sleep 0.1 2>/dev/null || sleep 1
|
|
done
|
|
done
|
|
) &
|
|
_spinner_pid=$!
|
|
printf '%s' "$_spinner_pid"
|
|
}
|
|
|
|
tui_spinner_stop() {
|
|
_pid="$1"
|
|
_text="${2:-}"
|
|
if [ -n "$_pid" ] && kill -0 "$_pid" 2>/dev/null; then
|
|
kill "$_pid" 2>/dev/null
|
|
wait "$_pid" 2>/dev/null
|
|
fi
|
|
# Clear spinner line
|
|
printf '\r%s' "$(_tui_hline ' ')"
|
|
printf '\r'
|
|
[ -n "$_text" ] && tui_ok "$_text"
|
|
}
|
|
|
|
# ─── Boxed section ───────────────────────────────────────────────────────────
|
|
tui_box_start() {
|
|
_w="$(_tui_width)"
|
|
_w=$((_w - 4))
|
|
[ "$_w" -lt 10 ] 2>/dev/null && _w=10
|
|
printf '\n'
|
|
printf ' %s╔' "$_c_blue"
|
|
_i=0; while [ "$_i" -lt "$_w" ]; do printf '═'; _i=$((_i + 1)); done
|
|
printf '╗%s\n' "$_c_reset"
|
|
}
|
|
|
|
tui_box_line() {
|
|
_text="$1"
|
|
_w="$(_tui_width)"
|
|
_w=$((_w - 4))
|
|
_len=${#_text}
|
|
_pad=$((_w - _len - 2))
|
|
[ "$_pad" -lt 0 ] 2>/dev/null && _pad=0
|
|
printf ' %s║%s %s' "$_c_blue" "$_c_reset" "$_text"
|
|
_i=0; while [ "$_i" -lt "$_pad" ]; do printf ' '; _i=$((_i + 1)); done
|
|
printf '%s║%s\n' "$_c_blue" "$_c_reset"
|
|
}
|
|
|
|
tui_box_line_color() {
|
|
_text="$1"
|
|
_color="$2"
|
|
_w="$(_tui_width)"
|
|
_w=$((_w - 4))
|
|
_len=${#_text}
|
|
_pad=$((_w - _len - 2))
|
|
[ "$_pad" -lt 0 ] 2>/dev/null && _pad=0
|
|
printf ' %s║%s %s%s' "$_c_blue" "$_c_reset" "$_color" "$_text"
|
|
_i=0; while [ "$_i" -lt "$_pad" ]; do printf ' '; _i=$((_i + 1)); done
|
|
printf '%s║%s\n' "$_c_reset" "$_c_blue"
|
|
}
|
|
|
|
tui_box_end() {
|
|
_w="$(_tui_width)"
|
|
_w=$((_w - 4))
|
|
[ "$_w" -lt 10 ] 2>/dev/null && _w=10
|
|
printf ' %s╚' "$_c_blue"
|
|
_i=0; while [ "$_i" -lt "$_w" ]; do printf '═'; _i=$((_i + 1)); done
|
|
printf '╝%s\n' "$_c_reset"
|
|
}
|
|
|
|
# ─── Section divider ─────────────────────────────────────────────────────────
|
|
tui_divider() {
|
|
printf '\n %s%s%s\n' "$_c_dim" "$(_tui_hline '─')" "$_c_reset"
|
|
}
|
|
|
|
tui_box_divider() {
|
|
_w="$(_tui_width)"
|
|
_w=$((_w - 4))
|
|
[ "$_w" -lt 10 ] 2>/dev/null && _w=10
|
|
printf ' %s║%s' "$_c_blue" "$_c_reset"
|
|
_i=0; while [ "$_i" -lt "$_w" ]; do printf '─'; _i=$((_i + 1)); done
|
|
printf '%s║%s\n' "$_c_blue" "$_c_reset"
|
|
}
|
|
|
|
# ─── Interactive menu (returns choice) ──────────────────────────────────────
|
|
tui_menu() {
|
|
_prompt="$1"
|
|
shift
|
|
_options="$@"
|
|
|
|
printf '\n %s%s%s\n' "$_c_bold" "$_prompt" "$_c_reset"
|
|
_i=1
|
|
for _opt in $_options; do
|
|
printf ' %s%s)%s %s\n' "$_c_cyan" "$_c_bold" "$_c_reset" "$_opt"
|
|
_i=$((_i + 1))
|
|
done
|
|
printf '\n'
|
|
printf ' %s▸%s ' "$_c_cyan" "$_c_reset"
|
|
}
|
|
|
|
# ─── Confirmation prompt ────────────────────────────────────────────────────
|
|
tui_confirm() {
|
|
_prompt="$1"
|
|
printf ' %s%s?%s %s %s' "$_c_yellow" "$_c_bold" "$_c_reset" "$_prompt" "$_c_dim"
|
|
printf '(%s/%s)%s ' "y" "n" "$_c_reset"
|
|
}
|
|
|
|
REQUIRED_SPACE_KB=15360
|
|
CONNECT_TIMEOUT_SECONDS=15
|
|
METADATA_TIMEOUT_SECONDS=60
|
|
DOWNLOAD_TIMEOUT_SECONDS=600
|
|
|
|
PKG_IS_APK=0
|
|
FETCHER=""
|
|
TMP_DIR=""
|
|
LOCK_FILE="/tmp/tachyon.install.lock"
|
|
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
|
|
INSTALLER_LANG="en"
|
|
SING_BOX_INSTALL_VARIANT=""
|
|
|
|
# Runtime flags (see parse_args)
|
|
ASSUME_YES=0
|
|
DRY_RUN=0
|
|
VERBOSE=0
|
|
QUIET=0
|
|
SKIP_SING_BOX=0
|
|
ZRAM_INSTALL_REQUESTED=0
|
|
ZRAM_INSTALL_OVERRIDE=""
|
|
|
|
TACHYON_RELEASE_JSON=""
|
|
TACHYON_RELEASE_TAG=""
|
|
TACHYON_BACKEND_URL=""
|
|
TACHYON_BACKEND_NAME=""
|
|
TACHYON_BACKEND_FILE=""
|
|
TACHYON_APP_URL=""
|
|
TACHYON_APP_NAME=""
|
|
TACHYON_APP_FILE=""
|
|
TACHYON_I18N_URL=""
|
|
TACHYON_I18N_NAME=""
|
|
TACHYON_I18N_FILE=""
|
|
TACHYON_PACKAGE_VERSION=""
|
|
LEGACY_BRAND="$(printf '\160\157\144\153\157\160')"
|
|
LEGACY_BACKEND_PACKAGE="${LEGACY_BRAND}-plus"
|
|
LEGACY_CONFIG_PACKAGE_ALT="${LEGACY_BRAND}_plus"
|
|
LEGACY_CONFIG_BACKUP=""
|
|
|
|
command -v apk >/dev/null 2>&1 && PKG_IS_APK=1
|
|
|
|
log_line() {
|
|
# Append a timestamped line to the log file, best-effort (never fatal).
|
|
[ -n "$LOG_FILE" ] || return 0
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '----------------')" "$1" >>"$LOG_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
run_logged() {
|
|
_tag="$1"
|
|
shift
|
|
|
|
log_line "EXEC [$_tag] $*"
|
|
local _tmp_out
|
|
_tmp_out="$(mktemp)" 2>/dev/null || _tmp_out="/tmp/.cmd-out-$$.log"
|
|
|
|
"$@" >"$_tmp_out" 2>&1
|
|
local _rc=$?
|
|
|
|
if [ -s "$_tmp_out" ]; then
|
|
while IFS= read -r _line; do
|
|
log_line " [$_tag] $_line"
|
|
done <"$_tmp_out"
|
|
fi
|
|
|
|
if [ $_rc -ne 0 ]; then
|
|
cat "$_tmp_out" >&2 2>/dev/null || true
|
|
fi
|
|
|
|
rm -f "$_tmp_out" 2>/dev/null || true
|
|
return "$_rc"
|
|
}
|
|
|
|
run_logged_timeout() {
|
|
_tag="$1"
|
|
_secs="$2"
|
|
shift 2
|
|
|
|
log_line "EXEC [$_tag] (timeout ${_secs}s) $*"
|
|
local _tmp_out
|
|
_tmp_out="$(mktemp)" 2>/dev/null || _tmp_out="/tmp/.cmd-out-$$.log"
|
|
|
|
pkg_run_timeout "$_secs" "$@" >"$_tmp_out" 2>&1
|
|
local _rc=$?
|
|
|
|
if [ -s "$_tmp_out" ]; then
|
|
while IFS= read -r _line; do
|
|
log_line " [$_tag] $_line"
|
|
done <"$_tmp_out"
|
|
fi
|
|
|
|
if [ $_rc -ne 0 ]; then
|
|
cat "$_tmp_out" >&2 2>/dev/null || true
|
|
fi
|
|
|
|
rm -f "$_tmp_out" 2>/dev/null || true
|
|
return "$_rc"
|
|
}
|
|
|
|
# Clean up orphaned sing-box package references in /etc/apk/world that prevent apk from resolving dependencies
|
|
sanitize_apk_world() {
|
|
[ "$PKG_IS_APK" -eq 1 ] || return 0
|
|
[ -f /etc/apk/world ] || return 0
|
|
|
|
local _orphaned_pkg
|
|
for _orphaned_pkg in sing-box-extended sing-box sing-box-tiny sing-box-lx; do
|
|
if grep -E -q "^${_orphaned_pkg}([><= ].*)?$" /etc/apk/world 2>/dev/null; then
|
|
if ! apk info -e "$_orphaned_pkg" >/dev/null 2>&1; then
|
|
log_line "WARN removing orphaned $_orphaned_pkg from /etc/apk/world"
|
|
sed -i -E "/^${_orphaned_pkg}([><= ].*)?$/d" /etc/apk/world 2>/dev/null || true
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Retry an APK command up to 10 times if the database is locked (exit 227).
|
|
# Usage: apk_with_lock_retry <tag> <timeout> <apk args...>
|
|
apk_with_lock_retry() {
|
|
local _tag="$1" _secs="$2"
|
|
shift 2
|
|
local _attempt=0 _rc=227
|
|
sanitize_apk_world
|
|
while [ "$_attempt" -lt 10 ] && [ "$_rc" -eq 227 ]; do
|
|
if [ "$_attempt" -gt 0 ]; then
|
|
log_line "WARN [$_tag] APK database locked (exit 227), retrying in 3s (attempt $((_attempt+1))/10)..."
|
|
sleep 3
|
|
fi
|
|
run_logged_timeout "$_tag" "$_secs" "$@"
|
|
_rc=$?
|
|
_attempt=$((_attempt+1))
|
|
done
|
|
return "$_rc"
|
|
}
|
|
|
|
msg() {
|
|
log_line "INFO $1"
|
|
[ "$QUIET" -eq 1 ] && return 0
|
|
if command -v tui_info >/dev/null 2>&1; then
|
|
tui_info "$1"
|
|
else
|
|
printf '%s%s%s\n' "${_c_green}${_c_bold}" "$1" "$_c_reset"
|
|
fi
|
|
}
|
|
|
|
warn() {
|
|
log_line "WARN $1"
|
|
if command -v tui_warn >/dev/null 2>&1; then
|
|
tui_warn "$1"
|
|
else
|
|
printf '%s%s%s\n' "${_c_yellow}${_c_bold}" "$1" "$_c_reset" >&2
|
|
fi
|
|
}
|
|
|
|
fail() {
|
|
log_line "FAIL $1"
|
|
if [ "${TACHYON_WAS_RUNNING:-0}" -eq 1 ] && [ -x /etc/init.d/tachyon ]; then
|
|
log_line "WARN restoring Tachyon service after failed install"
|
|
/etc/init.d/tachyon enable 2>/dev/null || true
|
|
/etc/init.d/tachyon restart 2>/dev/null || true
|
|
fi
|
|
if command -v tui_err >/dev/null 2>&1; then
|
|
tui_err "$1"
|
|
else
|
|
printf '%s%s%s\n' "${_c_red}${_c_bold}" "$1" "$_c_reset" >&2
|
|
printf '%s%sLog: %s%s\n' "$_c_dim" "" "$LOG_FILE" "$_c_reset" >&2
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
debug() {
|
|
log_line "DEBUG $1"
|
|
[ "$VERBOSE" -eq 1 ] || return 0
|
|
printf '%s[debug] %s%s\n' "$_c_cyan" "$1" "$_c_reset"
|
|
}
|
|
|
|
step() {
|
|
step_no="$1"
|
|
step_total="$2"
|
|
step_text="$3"
|
|
log_line "STEP [$step_no/$step_total] $step_text"
|
|
[ "$QUIET" -eq 1 ] && return 0
|
|
if command -v tui_step >/dev/null 2>&1; then
|
|
tui_step "$step_no" "$step_total" "$step_text"
|
|
else
|
|
printf '%s[%s/%s]%s %s%s%s\n' "$_c_blue$_c_bold" "$step_no" "$step_total" "$_c_reset" "$_c_bold" "$step_text" "$_c_reset"
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Tachyon installer v${INSTALLER_VERSION}
|
|
|
|
Usage: $0 [options]
|
|
|
|
Installs or updates Tachyon packages:
|
|
- tachyon
|
|
- luci-app-tachyon
|
|
- luci-i18n-tachyon-ru when requested or when LuCI language is Russian
|
|
|
|
Can also install or switch sing-box variant:
|
|
- stable sing-box from OpenWrt feeds
|
|
- sing-box-extended from GitHub OpenWrt packages (for xHTTP support)
|
|
|
|
Options:
|
|
-y, --yes Assume "yes"/default answer for every interactive prompt
|
|
-n, --dry-run Resolve versions and show planned actions, but don't
|
|
download, install, remove, or modify anything on disk
|
|
-v, --verbose Print extra diagnostic detail while installing
|
|
-q, --quiet Suppress informational output (errors/warnings still show)
|
|
--skip-sing-box Skip sing-box installation
|
|
--zram Install and enable zram-swap (compressed RAM swap)
|
|
--no-zram Skip zram-swap prompt and installation
|
|
--version Print the installer version and exit
|
|
-h, --help Show this help text and exit
|
|
|
|
Log file: $LOG_FILE
|
|
EOF
|
|
}
|
|
|
|
parse_args() {
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--version)
|
|
printf 'tachyon-installer %s\n' "$INSTALLER_VERSION"
|
|
exit 0
|
|
;;
|
|
-y|--yes)
|
|
ASSUME_YES=1
|
|
;;
|
|
-n|--dry-run)
|
|
DRY_RUN=1
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=1
|
|
;;
|
|
-q|--quiet)
|
|
QUIET=1
|
|
;;
|
|
--skip-sing-box|--no-sing-box)
|
|
SKIP_SING_BOX=1
|
|
;;
|
|
--zram)
|
|
ZRAM_INSTALL_OVERRIDE="yes"
|
|
;;
|
|
--no-zram)
|
|
ZRAM_INSTALL_OVERRIDE="no"
|
|
;;
|
|
*)
|
|
fail "Unknown installer option: $1 (see --help)"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ "$VERBOSE" -eq 1 ] && [ "$QUIET" -eq 1 ]; then
|
|
fail "--verbose and --quiet cannot be used together"
|
|
fi
|
|
}
|
|
|
|
acquire_lock() {
|
|
# Guard against two copies of the installer running at once, which could
|
|
# corrupt package state or UCI config. Stale locks (dead PID) are reclaimed.
|
|
if [ -f "$LOCK_FILE" ]; then
|
|
existing_pid="$(cat "$LOCK_FILE" 2>/dev/null || true)"
|
|
if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
fail "Another instance of the installer appears to be running (pid $existing_pid). Remove $LOCK_FILE if this is incorrect."
|
|
fi
|
|
debug "Removing stale lock file from pid $existing_pid"
|
|
rm -f "$LOCK_FILE"
|
|
fi
|
|
printf '%s' "$$" >"$LOCK_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
release_lock() {
|
|
[ -f "$LOCK_FILE" ] || return 0
|
|
lock_pid="$(cat "$LOCK_FILE" 2>/dev/null || true)"
|
|
[ "$lock_pid" = "$$" ] && rm -f "$LOCK_FILE"
|
|
}
|
|
|
|
cleanup() {
|
|
[ -n "$TMP_DIR" ] && rm -rf "$TMP_DIR"
|
|
release_lock
|
|
}
|
|
|
|
read_openwrt_release_value() {
|
|
key="$1"
|
|
|
|
[ -f /etc/openwrt_release ] || return 0
|
|
sed -n "s/^${key}='\(.*\)'/\1/p" /etc/openwrt_release 2>/dev/null | head -n 1
|
|
}
|
|
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
init_tmp_dir() {
|
|
TMP_DIR="$(mktemp -d /tmp/tachyon.XXXXXX 2>/dev/null || true)"
|
|
|
|
if [ -z "$TMP_DIR" ]; then
|
|
TMP_DIR="/tmp/tachyon.$$"
|
|
mkdir -p "$TMP_DIR" || fail "Failed to create temporary directory: $TMP_DIR"
|
|
fi
|
|
}
|
|
|
|
detect_fetcher() {
|
|
if command_exists wget; then
|
|
FETCHER="wget"
|
|
return 0
|
|
fi
|
|
|
|
if command_exists curl; then
|
|
FETCHER="curl"
|
|
return 0
|
|
fi
|
|
|
|
fail "wget or curl is required to download Tachyon"
|
|
}
|
|
|
|
run_with_deadline() {
|
|
tachyon_deadline_seconds="$1"
|
|
shift
|
|
|
|
"$@" &
|
|
tachyon_deadline_command_pid=$!
|
|
|
|
# kill entire process group (all children of the command)
|
|
_deadline_kill_group() {
|
|
kill -- -"$tachyon_deadline_command_pid" 2>/dev/null || kill "$tachyon_deadline_command_pid" 2>/dev/null || true
|
|
}
|
|
|
|
(
|
|
trap 'kill "$tachyon_deadline_sleep_pid" 2>/dev/null || true; exit 0' TERM INT
|
|
sleep "$tachyon_deadline_seconds" &
|
|
tachyon_deadline_sleep_pid=$!
|
|
wait "$tachyon_deadline_sleep_pid"
|
|
_deadline_kill_group
|
|
) &
|
|
tachyon_deadline_watchdog_pid=$!
|
|
|
|
wait "$tachyon_deadline_command_pid"
|
|
tachyon_deadline_status=$?
|
|
kill "$tachyon_deadline_watchdog_pid" 2>/dev/null || true
|
|
wait "$tachyon_deadline_watchdog_pid" 2>/dev/null || true
|
|
return "$tachyon_deadline_status"
|
|
}
|
|
|
|
http_get() {
|
|
case "$FETCHER" in
|
|
wget)
|
|
run_with_deadline "$METADATA_TIMEOUT_SECONDS" wget -T "$CONNECT_TIMEOUT_SECONDS" -qO- "$1"
|
|
;;
|
|
curl)
|
|
curl --connect-timeout "$CONNECT_TIMEOUT_SECONDS" --speed-limit 1024 --speed-time 15 --max-time "$METADATA_TIMEOUT_SECONDS" -fsSL "$1"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_json_helper_path() {
|
|
helper_path="$TMP_DIR/install-json.uc"
|
|
|
|
cat > "$helper_path" <<'EOF'
|
|
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
|
|
function as_string(value) {
|
|
return value == null ? "" : "" + value;
|
|
}
|
|
|
|
function read_stdin() {
|
|
let input = fs.open("/dev/stdin", "r");
|
|
if (!input)
|
|
return "";
|
|
let data = input.read("all");
|
|
input.close();
|
|
return data == null ? "" : data;
|
|
}
|
|
|
|
function read_stdin_json() {
|
|
try {
|
|
return json(read_stdin());
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function starts_with(value, prefix) {
|
|
value = as_string(value);
|
|
prefix = as_string(prefix);
|
|
return substr(value, 0, length(prefix)) == prefix;
|
|
}
|
|
|
|
function ends_with(value, suffix) {
|
|
value = as_string(value);
|
|
suffix = as_string(suffix);
|
|
return length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix;
|
|
}
|
|
|
|
function file_exists(path) {
|
|
return fs.stat(as_string(path)) != null;
|
|
}
|
|
|
|
let uci_cursor_state = false;
|
|
|
|
function words(value) {
|
|
value = trim(as_string(value));
|
|
return value == "" ? [] : split(value, /[ \t\r\n]+/);
|
|
}
|
|
|
|
function truthy(value) {
|
|
value = lc(as_string(value));
|
|
return value == "1" || value == "true" || value == "yes" || value == "on";
|
|
}
|
|
|
|
function path_parts(path) {
|
|
path = as_string(path);
|
|
let first = index(path, ".");
|
|
if (first < 0)
|
|
return null;
|
|
|
|
let package_name = substr(path, 0, first);
|
|
let rest = substr(path, first + 1);
|
|
let second = index(rest, ".");
|
|
if (second < 0)
|
|
return { package: package_name, section: rest, option: "" };
|
|
|
|
return {
|
|
package: package_name,
|
|
section: substr(rest, 0, second),
|
|
option: substr(rest, second + 1)
|
|
};
|
|
}
|
|
|
|
function uci_cursor() {
|
|
if (uci_cursor_state !== false)
|
|
return uci_cursor_state;
|
|
|
|
try {
|
|
uci_cursor_state = require("uci").cursor();
|
|
}
|
|
catch (e) {
|
|
uci_cursor_state = null;
|
|
}
|
|
|
|
return uci_cursor_state;
|
|
}
|
|
|
|
function uci_available() {
|
|
return uci_cursor() != null;
|
|
}
|
|
|
|
function uci_load(package_name) {
|
|
let c = uci_cursor();
|
|
if (c == null)
|
|
return false;
|
|
|
|
try {
|
|
c.load(as_string(package_name));
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function uci_value_to_string(value) {
|
|
if (value == null)
|
|
return "";
|
|
if (type(value) == "array")
|
|
return join(" ", value);
|
|
return as_string(value);
|
|
}
|
|
|
|
function uci_value_to_list(value) {
|
|
if (value == null)
|
|
return [];
|
|
if (type(value) == "array")
|
|
return value;
|
|
return words(value);
|
|
}
|
|
|
|
function uci_get(path) {
|
|
let parts = path_parts(path);
|
|
let c = uci_cursor();
|
|
if (c == null || parts == null || parts.option == "")
|
|
return "";
|
|
if (!uci_load(parts.package))
|
|
return "";
|
|
|
|
return uci_value_to_string(c.get(parts.package, parts.section, parts.option));
|
|
}
|
|
|
|
function uci_exists(path) {
|
|
let parts = path_parts(path);
|
|
let c = uci_cursor();
|
|
if (c == null || parts == null)
|
|
return false;
|
|
if (!uci_load(parts.package))
|
|
return false;
|
|
|
|
if (parts.option == "")
|
|
return c.get_all(parts.package, parts.section) != null;
|
|
return c.get(parts.package, parts.section, parts.option) != null;
|
|
}
|
|
|
|
function uci_delete(path) {
|
|
let parts = path_parts(path);
|
|
let c = uci_cursor();
|
|
if (c == null || parts == null)
|
|
return false;
|
|
|
|
try {
|
|
if (parts.option == "")
|
|
c.delete(parts.package, parts.section);
|
|
else
|
|
c.delete(parts.package, parts.section, parts.option);
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function uci_set(path, value) {
|
|
let parts = path_parts(path);
|
|
let c = uci_cursor();
|
|
if (c == null || parts == null || parts.option == "")
|
|
return false;
|
|
|
|
try {
|
|
c.set(parts.package, parts.section, parts.option, type(value) == "array" ? value : as_string(value));
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function uci_add_list(path, value) {
|
|
let parts = path_parts(path);
|
|
let c = uci_cursor();
|
|
if (c == null || parts == null || parts.option == "")
|
|
return false;
|
|
|
|
try {
|
|
let values = uci_value_to_list(c.get(parts.package, parts.section, parts.option));
|
|
push(values, as_string(value));
|
|
c.set(parts.package, parts.section, parts.option, values);
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function uci_del_list(path, value) {
|
|
let parts = path_parts(path);
|
|
let c = uci_cursor();
|
|
if (c == null || parts == null || parts.option == "")
|
|
return false;
|
|
|
|
let values = [];
|
|
let removed = false;
|
|
for (let item in uci_value_to_list(c.get(parts.package, parts.section, parts.option))) {
|
|
if (item == value) {
|
|
removed = true;
|
|
continue;
|
|
}
|
|
push(values, item);
|
|
}
|
|
|
|
if (!removed)
|
|
return false;
|
|
|
|
try {
|
|
if (length(values) == 0)
|
|
c.delete(parts.package, parts.section, parts.option);
|
|
else
|
|
c.set(parts.package, parts.section, parts.option, values);
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function uci_commit(package_name) {
|
|
let c = uci_cursor();
|
|
if (c == null)
|
|
return false;
|
|
|
|
try {
|
|
return c.commit(package_name) != false;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function run(command) {
|
|
return system(command) == 0;
|
|
}
|
|
|
|
function command_success(command) {
|
|
return system(command) == 0;
|
|
}
|
|
|
|
function command_success_from_args(args) {
|
|
return run_args(args);
|
|
}
|
|
|
|
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);
|
|
return status > 255 ? int(status / 256) : status;
|
|
}
|
|
|
|
let timeout_prefix_cache = null;
|
|
function timeout_prefix() {
|
|
if (timeout_prefix_cache != null)
|
|
return timeout_prefix_cache;
|
|
|
|
if (normalize_status(system("timeout 1 /bin/true >/dev/null 2>&1")) == 0)
|
|
timeout_prefix_cache = [ "timeout" ];
|
|
else if (normalize_status(system("timeout -t 1 /bin/true >/dev/null 2>&1")) == 0)
|
|
timeout_prefix_cache = [ "timeout", "-t" ];
|
|
else
|
|
timeout_prefix_cache = [];
|
|
|
|
return timeout_prefix_cache;
|
|
}
|
|
|
|
function normalize_timeout_args(args) {
|
|
if (length(args) > 1 && args[0] == "timeout") {
|
|
let prefix = timeout_prefix();
|
|
let secs = args[1];
|
|
let rest = [];
|
|
for (let i = 2; i < length(args); i++)
|
|
push(rest, args[i]);
|
|
if (length(prefix) == 0)
|
|
return rest;
|
|
let result = [];
|
|
for (let p in prefix)
|
|
push(result, p);
|
|
push(result, secs);
|
|
for (let r in rest)
|
|
push(result, r);
|
|
return result;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function run_args(args) {
|
|
args = normalize_timeout_args(args);
|
|
return normalize_status(system(command_from_args(args) + " >/dev/null 2>&1")) == 0;
|
|
}
|
|
|
|
function command_output(args) {
|
|
args = normalize_timeout_args(args);
|
|
let pipe = fs.popen(command_from_args(args) + " 2>/dev/null", "r");
|
|
if (!pipe)
|
|
return "";
|
|
|
|
let data = pipe.read("all");
|
|
pipe.close();
|
|
return data == null ? "" : data;
|
|
}
|
|
|
|
function env(name, fallback) {
|
|
let value = getenv(name);
|
|
if (value == null || value == "")
|
|
return as_string(fallback);
|
|
return as_string(value);
|
|
}
|
|
|
|
const INSTALLER_TACHYON_INIT = env("TACHYON_INSTALLER_INIT", "/etc/init.d/tachyon");
|
|
const INSTALLER_TACHYON_BIN = env("TACHYON_INSTALLER_BIN", "/usr/bin/tachyon");
|
|
const INSTALLER_TACHYON_LIB = env("TACHYON_INSTALLER_LIB", "/usr/lib/tachyon");
|
|
const INSTALLER_TACHYON_PERSISTENT_DIR = env("TACHYON_INSTALLER_PERSISTENT_DIR", "/etc/tachyon");
|
|
const INSTALLER_TACHYON_UCI_DEFAULTS = env("TACHYON_INSTALLER_UCI_DEFAULTS", "/etc/uci-defaults/50_luci-tachyon");
|
|
const INSTALLER_TACHYON_LUCI_VIEW = env("TACHYON_INSTALLER_LUCI_VIEW", "/www/luci-static/resources/view/tachyon");
|
|
const INSTALLER_MENU_JSON = env("TACHYON_INSTALLER_MENU_JSON", "/usr/share/luci/menu.d/luci-app-tachyon.json");
|
|
const INSTALLER_ACL_JSON = env("TACHYON_INSTALLER_ACL_JSON", "/usr/share/rpcd/acl.d/luci-app-tachyon.json");
|
|
const INSTALLER_RU_LMO = env("TACHYON_INSTALLER_RU_LMO", "/usr/lib/lua/luci/i18n/tachyon.ru.lmo");
|
|
const INSTALLER_EN_LMO = env("TACHYON_INSTALLER_EN_LMO", "/usr/lib/lua/luci/i18n/tachyon.en.lmo");
|
|
const INSTALLER_RU_LUA = env("TACHYON_INSTALLER_RU_LUA", "/usr/lib/lua/luci/i18n/tachyon.ru.lua");
|
|
const INSTALLER_EN_LUA = env("TACHYON_INSTALLER_EN_LUA", "/usr/lib/lua/luci/i18n/tachyon.en.lua");
|
|
const INSTALLER_RPCD_INIT = env("TACHYON_INSTALLER_RPCD_INIT", "/etc/init.d/rpcd");
|
|
const LEGACY_BRAND = env("TACHYON_INSTALLER_LEGACY_BRAND", "");
|
|
const LEGACY_BACKEND_PACKAGE = env("TACHYON_INSTALLER_LEGACY_BACKEND", LEGACY_BRAND + "-plus");
|
|
const LEGACY_CONFIG_PACKAGE_ALT = env("TACHYON_INSTALLER_LEGACY_CONFIG_ALT", LEGACY_BRAND + "_plus");
|
|
const INSTALLER_LEGACY_INIT = env("TACHYON_INSTALLER_LEGACY_INIT", "/etc/init.d/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_BASE_INIT = env("TACHYON_INSTALLER_LEGACY_BASE_INIT", "/etc/init.d/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_BIN = env("TACHYON_INSTALLER_LEGACY_BASE_BIN", "/usr/bin/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_LIB = env("TACHYON_INSTALLER_LEGACY_BASE_LIB", "/usr/lib/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_UCI_DEFAULTS = env("TACHYON_INSTALLER_LEGACY_BASE_UCI_DEFAULTS", "/etc/uci-defaults/50_luci-" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_LUCI_VIEW = env("TACHYON_INSTALLER_LEGACY_BASE_LUCI_VIEW", "/www/luci-static/resources/view/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_MENU_JSON = env("TACHYON_INSTALLER_LEGACY_BASE_MENU_JSON", "/usr/share/luci/menu.d/luci-app-" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_ACL_JSON = env("TACHYON_INSTALLER_LEGACY_BASE_ACL_JSON", "/usr/share/rpcd/acl.d/luci-app-" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_I18N = env("TACHYON_INSTALLER_LEGACY_BASE_I18N", "/usr/lib/lua/luci/i18n/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_CONFIG = env("TACHYON_INSTALLER_LEGACY_BASE_CONFIG", "/etc/config/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_PERSISTENT_DIR = env("TACHYON_INSTALLER_LEGACY_BASE_PERSISTENT_DIR", "/etc/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_RUNTIME_DIR = env("TACHYON_INSTALLER_LEGACY_BASE_RUNTIME_DIR", "/var/run/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_BASE_TMP_DIR = env("TACHYON_INSTALLER_LEGACY_BASE_TMP_DIR", "/tmp/" + LEGACY_BRAND);
|
|
const INSTALLER_LEGACY_TMP_PACKAGE_GLOB = env("TACHYON_INSTALLER_LEGACY_TMP_PACKAGE_GLOB", "/tmp/*" + LEGACY_BRAND + "*");
|
|
const INSTALLER_LEGACY_SCAN_ROOTS = env("TACHYON_INSTALLER_LEGACY_SCAN_ROOTS", "/tmp /var/run /etc /usr/lib /usr/share/luci /usr/share/rpcd /www/luci-static/resources/view");
|
|
const INSTALLER_LEGACY_BIN = env("TACHYON_INSTALLER_LEGACY_BIN", "/usr/bin/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_LIB = env("TACHYON_INSTALLER_LEGACY_LIB", "/usr/lib/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_UCI_DEFAULTS = env("TACHYON_INSTALLER_LEGACY_UCI_DEFAULTS", "/etc/uci-defaults/50_luci-" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_LUCI_VIEW = env("TACHYON_INSTALLER_LEGACY_LUCI_VIEW", "/www/luci-static/resources/view/" + LEGACY_CONFIG_PACKAGE_ALT);
|
|
const INSTALLER_LEGACY_MENU_JSON = env("TACHYON_INSTALLER_LEGACY_MENU_JSON", "/usr/share/luci/menu.d/luci-app-" + LEGACY_BACKEND_PACKAGE + ".json");
|
|
const INSTALLER_LEGACY_ACL_JSON = env("TACHYON_INSTALLER_LEGACY_ACL_JSON", "/usr/share/rpcd/acl.d/luci-app-" + LEGACY_BACKEND_PACKAGE + ".json");
|
|
const INSTALLER_LEGACY_CONFIG = env("TACHYON_INSTALLER_LEGACY_CONFIG", "/etc/config/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_CONFIG_ALT = env("TACHYON_INSTALLER_LEGACY_CONFIG_FILE_ALT", "/etc/config/" + LEGACY_CONFIG_PACKAGE_ALT);
|
|
const INSTALLER_LEGACY_PERSISTENT_DIR = env("TACHYON_INSTALLER_LEGACY_PERSISTENT_DIR", "/etc/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_RUNTIME_DIR = env("TACHYON_INSTALLER_LEGACY_RUNTIME_DIR", "/var/run/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_TMP_DIR = env("TACHYON_INSTALLER_LEGACY_TMP_DIR", "/tmp/" + LEGACY_BACKEND_PACKAGE);
|
|
const INSTALLER_LEGACY_TMP_ALT_DIR = env("TACHYON_INSTALLER_LEGACY_TMP_ALT_DIR", "/tmp/" + LEGACY_CONFIG_PACKAGE_ALT);
|
|
|
|
let dns_owner_config = "tachyon";
|
|
let dns_owner_section = "tachyon";
|
|
let dns_owner_option_prefix = "tachyon_";
|
|
|
|
function path_exists(path) {
|
|
return fs.stat(as_string(path)) != null;
|
|
}
|
|
|
|
function path_executable(path) {
|
|
let stat = fs.stat(as_string(path));
|
|
return stat != null && stat.mode != null && (int(stat.mode) & 73) != 0;
|
|
}
|
|
|
|
function remove_path(path) {
|
|
if (as_string(path) == "" || !path_exists(path))
|
|
return true;
|
|
return run_args([ "rm", "-rf", path ]);
|
|
}
|
|
|
|
function remove_glob(pattern) {
|
|
pattern = as_string(pattern);
|
|
if (pattern == "")
|
|
return true;
|
|
let removed = true;
|
|
for (let path in fs.glob(pattern))
|
|
if (!remove_path(path))
|
|
removed = false;
|
|
return removed;
|
|
}
|
|
|
|
function remove_globs(patterns) {
|
|
let removed = true;
|
|
for (let pattern in words(patterns))
|
|
if (!remove_glob(pattern))
|
|
removed = false;
|
|
return removed;
|
|
}
|
|
|
|
function remove_legacy_named_children(root) {
|
|
root = as_string(root);
|
|
if (root == "" || LEGACY_BRAND == "")
|
|
return true;
|
|
|
|
let entries = fs.lsdir(root);
|
|
if (type(entries) != "array")
|
|
return true;
|
|
|
|
let removed = true;
|
|
let brand = lc(LEGACY_BRAND);
|
|
for (let entry in entries) {
|
|
entry = as_string(entry);
|
|
let path = root + "/" + entry;
|
|
if (index(lc(entry), brand) >= 0) {
|
|
if (!remove_path(path))
|
|
removed = false;
|
|
continue;
|
|
}
|
|
|
|
let stat = fs.stat(path);
|
|
if (stat != null && stat.type == "directory" && !remove_legacy_named_children(path))
|
|
removed = false;
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
function restart_dnsmasq() {
|
|
return run("[ -x /etc/init.d/dnsmasq ] && timeout 10 /etc/init.d/dnsmasq restart");
|
|
}
|
|
|
|
function installer_package_manager() {
|
|
return run_args([ "apk", "--version" ]) ? "apk" : "opkg";
|
|
}
|
|
|
|
function installer_installed_package_names() {
|
|
let manager = installer_package_manager();
|
|
let output = manager == "apk" ?
|
|
command_output([ "apk", "info" ]) :
|
|
command_output([ "opkg", "list-installed" ]);
|
|
let names = [];
|
|
|
|
for (let line in split(output, "\n")) {
|
|
line = trim(as_string(line));
|
|
if (line == "")
|
|
continue;
|
|
if (manager == "opkg") {
|
|
let parts = split(line, /[ \t]+/);
|
|
line = parts[0] || "";
|
|
}
|
|
if (line != "")
|
|
push(names, line);
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
function installer_package_installed(name) {
|
|
name = as_string(name);
|
|
if (name == "")
|
|
return false;
|
|
|
|
for (let installed in installer_installed_package_names())
|
|
if (installed == name)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function installer_remove_package(name) {
|
|
name = as_string(name);
|
|
if (name == "")
|
|
return true;
|
|
|
|
if (installer_package_manager() == "apk") {
|
|
if (file_exists("/etc/apk/world"))
|
|
command_success("sed -i -E " + shell_quote("/^" + name + "([><= ].*)?$/d") + " /etc/apk/world 2>/dev/null");
|
|
if (!installer_package_installed(name))
|
|
return true;
|
|
return run_args([ "timeout", "120", "apk", "del", name ]);
|
|
}
|
|
if (!installer_package_installed(name))
|
|
return true;
|
|
return run_args([ "timeout", "120", "opkg", "remove", "--force-depends", name ]);
|
|
}
|
|
|
|
function installer_remove_package_prefix(prefix) {
|
|
prefix = as_string(prefix);
|
|
if (prefix == "")
|
|
return true;
|
|
|
|
let removed = true;
|
|
for (let name in installer_installed_package_names())
|
|
if (starts_with(name, prefix) && !installer_remove_package(name))
|
|
removed = false;
|
|
return removed;
|
|
}
|
|
|
|
function installer_confirm_remove_https_dns_proxy() {
|
|
if (!installer_package_installed("https-dns-proxy"))
|
|
return true;
|
|
|
|
warn("Detected conflicting package: https-dns-proxy\n");
|
|
|
|
if (truthy(env("TACHYON_INSTALLER_ASSUME_YES", "0")) || run("[ ! -t 0 ]")) {
|
|
warn("Remove the conflicting https-dns-proxy package and continue?: 1 (yes, non-interactive)\n");
|
|
return true;
|
|
}
|
|
|
|
while (true) {
|
|
warn("\nRemove the conflicting https-dns-proxy package and continue?\n");
|
|
warn(" 1) yes\n");
|
|
warn(" 2) no\n");
|
|
warn("Select [2]: ");
|
|
|
|
let input = fs.open("/dev/stdin", "r");
|
|
let answer = input ? trim(as_string(input.read("line"))) : "";
|
|
if (input)
|
|
input.close();
|
|
|
|
if (answer == "1")
|
|
return true;
|
|
if (answer == "" || answer == "2")
|
|
return false;
|
|
warn("Invalid choice\n");
|
|
}
|
|
}
|
|
|
|
function installer_service_enabled(init_script) {
|
|
return path_executable(init_script) && run_args([ "timeout", "10", init_script, "enabled" ]);
|
|
}
|
|
|
|
function installer_service_running(init_script) {
|
|
if (!path_executable(init_script))
|
|
return false;
|
|
|
|
if (trim(command_output([ "timeout", "10", init_script, "status" ])) == "running")
|
|
return true;
|
|
return run_args([ "timeout", "10", init_script, "running" ]);
|
|
}
|
|
|
|
function installer_backend_status_running(bin_path) {
|
|
if (!path_executable(bin_path))
|
|
return false;
|
|
return index(command_output([ "timeout", "10", bin_path, "get_status" ]), "\"running\":1") >= 0;
|
|
}
|
|
|
|
function select_dns_owner(legacy) {
|
|
if (legacy == "forkop" || legacy == "forkop_plus") {
|
|
dns_owner_config = "forkop";
|
|
dns_owner_section = "forkop";
|
|
dns_owner_option_prefix = "forkop_";
|
|
}
|
|
else if (legacy == "netshift") {
|
|
dns_owner_config = "netshift";
|
|
dns_owner_section = "netshift";
|
|
dns_owner_option_prefix = "netshift_";
|
|
}
|
|
else if (legacy) {
|
|
dns_owner_config = LEGACY_BACKEND_PACKAGE;
|
|
dns_owner_section = LEGACY_CONFIG_PACKAGE_ALT;
|
|
dns_owner_option_prefix = LEGACY_BRAND + "_";
|
|
}
|
|
else {
|
|
dns_owner_config = "tachyon";
|
|
dns_owner_section = "tachyon";
|
|
dns_owner_option_prefix = "tachyon_";
|
|
}
|
|
}
|
|
|
|
let dnsmasq_failsafe_restore;
|
|
|
|
function installer_restore_dnsmasq(bin_path, legacy) {
|
|
if (path_executable(bin_path) && run_args([ bin_path, "restore_dnsmasq" ]))
|
|
return true;
|
|
|
|
select_dns_owner(legacy);
|
|
return dnsmasq_failsafe_restore();
|
|
}
|
|
|
|
function installer_deactivate_legacy_base() {
|
|
if (!path_executable(INSTALLER_LEGACY_BASE_INIT))
|
|
return false;
|
|
|
|
if (installer_service_running(INSTALLER_LEGACY_BASE_INIT)) {
|
|
warn("Detected a running legacy service. Stopping it before installing Tachyon.\n");
|
|
run_args([ "timeout", "10", INSTALLER_LEGACY_BASE_INIT, "stop" ]);
|
|
}
|
|
|
|
if (installer_service_enabled(INSTALLER_LEGACY_BASE_INIT)) {
|
|
warn("Detected an enabled legacy autostart. Disabling it before installing Tachyon.\n");
|
|
run_args([ "timeout", "10", INSTALLER_LEGACY_BASE_INIT, "disable" ]);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Every /etc/init.d/tachyon call goes through rc.common, which serializes on
|
|
// `flock -w 1000`. procd re-triggers retry_start_on_wan_up between a package's
|
|
// pre- and post-upgrade, so waiters keep appearing and a wedged one holds the
|
|
// lock for a quarter of an hour. That is long enough for the package manager to
|
|
// give up on the pre-upgrade hook and roll the whole transaction back, leaving
|
|
// the old build installed. Builds from 1.2.66 and earlier ship a prerm with no
|
|
// timeout of its own, and it is that OLD hook which runs during this upgrade —
|
|
// so the lock has to be free before the package manager is invoked at all.
|
|
function installer_release_init_lock() {
|
|
for (let pattern in [ "99-tachyon-wan|flock 1000", "/etc/init.d/tachyon" ])
|
|
system("ps 2>/dev/null | grep -E '" + pattern + "' | grep -v grep | awk '{print $1}' | " +
|
|
"while read _pid; do kill -9 \"$_pid\" 2>/dev/null; done; true");
|
|
|
|
// Killing by name is not enough. procd_lock() does `exec 1000>` on
|
|
// /var/lock/procd_tachyon.lock and then `flock 1000` with no -w, and every
|
|
// background spawn that inherits descriptor 1000 keeps the lock held long after
|
|
// the init script that took it exited. Builds up to 1.2.66 swept /proc/self/fd
|
|
// only as far as 999 and so missed exactly that descriptor — on a live router
|
|
// eleven unrelated processes (watchdog workers, logread, the zapret2 supervisors,
|
|
// nfqws2, a stray curl) were found pinning it. None of them match a tachyon name
|
|
// pattern, so the holders are cleared by descriptor. While any of them lives, the
|
|
// old pre-upgrade hook blocks in flock until the package manager kills it and
|
|
// rolls the whole transaction back.
|
|
//
|
|
// The installer's own process tree is skipped: this runs as a child of
|
|
// /usr/bin/tachyon on an upgrade, and killing an ancestor takes the install down.
|
|
system("__anc=\" \"; __p=$$; " +
|
|
"while [ -n \"$__p\" ] && [ \"$__p\" -gt 1 ] 2>/dev/null; do " +
|
|
"__anc=\"$__anc$__p \"; " +
|
|
"__p=$(awk '{ sub(/.*\\) /, \"\"); print $2 }' \"/proc/$__p/stat\" 2>/dev/null); done; " +
|
|
"for __f in /proc/[0-9]*; do [ -e \"$__f/fd/1000\" ] || continue; " +
|
|
"case \"$(readlink \"$__f/fd/1000\" 2>/dev/null)\" in *procd_tachyon*) " +
|
|
"__pid=${__f##*/}; case \"$__anc\" in *\" $__pid \"*) continue;; esac; " +
|
|
"kill -9 \"$__pid\" 2>/dev/null;; esac; done; true");
|
|
return true;
|
|
}
|
|
|
|
function installer_cleanup_legacy() {
|
|
let tachyon_installed = installer_package_installed("tachyon") ||
|
|
path_exists("/etc/config/tachyon") ||
|
|
path_executable(INSTALLER_TACHYON_BIN);
|
|
let legacy_installed = (LEGACY_BRAND != "" && (
|
|
installer_package_installed(LEGACY_BACKEND_PACKAGE) ||
|
|
installer_package_installed(LEGACY_BRAND) ||
|
|
installer_package_installed("luci-app-" + LEGACY_BRAND) ||
|
|
installer_package_installed("luci-app-" + LEGACY_BACKEND_PACKAGE) ||
|
|
path_exists("/etc/config/" + LEGACY_BRAND) ||
|
|
path_exists("/etc/config/" + LEGACY_BACKEND_PACKAGE) ||
|
|
path_exists("/etc/config/" + LEGACY_CONFIG_PACKAGE_ALT) ||
|
|
path_executable(INSTALLER_LEGACY_INIT) ||
|
|
path_executable(INSTALLER_LEGACY_BASE_INIT)
|
|
)) ||
|
|
installer_package_installed("forkop") ||
|
|
installer_package_installed("luci-app-forkop") ||
|
|
installer_package_installed("netshift") ||
|
|
installer_package_installed("luci-app-netshift") ||
|
|
path_exists("/etc/config/forkop") ||
|
|
path_exists("/etc/config/forkop_plus") ||
|
|
path_exists("/etc/config/netshift") ||
|
|
path_executable("/etc/init.d/forkop") ||
|
|
path_executable("/usr/bin/forkop") ||
|
|
path_executable("/etc/init.d/netshift") ||
|
|
path_executable("/usr/bin/netshift") ||
|
|
env("TACHYON_LEGACY_DETECTED", "0") == "1";
|
|
let active_init = legacy_installed ? INSTALLER_LEGACY_INIT : INSTALLER_TACHYON_INIT;
|
|
let active_bin = legacy_installed ? INSTALLER_LEGACY_BIN : INSTALLER_TACHYON_BIN;
|
|
let was_enabled = installer_service_enabled(active_init);
|
|
let was_running = installer_service_running(active_init) || installer_backend_status_running(active_bin);
|
|
|
|
for (let legacy_init in [
|
|
INSTALLER_LEGACY_BASE_INIT,
|
|
"/etc/init.d/forkop",
|
|
"/etc/init.d/netshift"
|
|
]) {
|
|
if (path_executable(legacy_init)) {
|
|
if (installer_service_enabled(legacy_init))
|
|
was_enabled = true;
|
|
if (installer_service_running(legacy_init))
|
|
was_running = true;
|
|
}
|
|
}
|
|
for (let legacy_bin in [
|
|
INSTALLER_LEGACY_BASE_BIN,
|
|
"/usr/bin/forkop",
|
|
"/usr/bin/netshift"
|
|
]) {
|
|
if (path_executable(legacy_bin) && installer_backend_status_running(legacy_bin))
|
|
was_running = true;
|
|
}
|
|
|
|
if (!installer_confirm_remove_https_dns_proxy())
|
|
return false;
|
|
|
|
if (legacy_installed)
|
|
installer_deactivate_legacy_base();
|
|
|
|
if (path_executable(active_init)) {
|
|
run_args([ "timeout", "10", active_init, "stop" ]);
|
|
installer_restore_dnsmasq(active_bin, legacy_installed);
|
|
run_args([ "timeout", "10", active_init, "disable" ]);
|
|
installer_release_init_lock();
|
|
}
|
|
|
|
if (path_executable("/etc/init.d/forkop")) {
|
|
run_args([ "timeout", "10", "/etc/init.d/forkop", "stop" ]);
|
|
installer_restore_dnsmasq("/usr/bin/forkop", "forkop");
|
|
run_args([ "timeout", "10", "/etc/init.d/forkop", "disable" ]);
|
|
installer_release_init_lock();
|
|
}
|
|
|
|
if (path_executable("/etc/init.d/netshift")) {
|
|
if (installer_service_running("/etc/init.d/netshift"))
|
|
run_args([ "timeout", "10", "/etc/init.d/netshift", "stop" ]);
|
|
installer_restore_dnsmasq("/usr/bin/netshift", "netshift");
|
|
run_args([ "timeout", "10", "/etc/init.d/netshift", "disable" ]);
|
|
installer_release_init_lock();
|
|
}
|
|
|
|
for (let legacy_tbl in [ "podkop", "PodkopTable", "forkop", "ForkopTable", "netshift", "NetShiftTable" ]) {
|
|
run_args([ "nft", "delete", "table", "inet", legacy_tbl ]);
|
|
}
|
|
system("rm -f /etc/rc.d/*podkop* /etc/rc.d/*forkop* /etc/rc.d/*netshift* 2>/dev/null || true");
|
|
|
|
let packages_removed = true;
|
|
for (let package_name in [ "luci-app-https-dns-proxy", "https-dns-proxy" ])
|
|
if (!installer_remove_package(package_name))
|
|
packages_removed = false;
|
|
if (!installer_remove_package_prefix("luci-i18n-https-dns-proxy"))
|
|
packages_removed = false;
|
|
|
|
if (legacy_installed) {
|
|
if (!installer_remove_package_prefix("luci-i18n-" + LEGACY_BACKEND_PACKAGE))
|
|
packages_removed = false;
|
|
if (!installer_remove_package("luci-app-" + LEGACY_BACKEND_PACKAGE))
|
|
packages_removed = false;
|
|
if (!installer_remove_package(LEGACY_BACKEND_PACKAGE))
|
|
packages_removed = false;
|
|
}
|
|
|
|
if (installer_package_installed("forkop") ||
|
|
installer_package_installed("luci-app-forkop") ||
|
|
path_exists("/etc/config/forkop") ||
|
|
path_exists("/etc/config/forkop_plus") ||
|
|
path_executable("/usr/bin/forkop") ||
|
|
path_executable("/etc/init.d/forkop")) {
|
|
if (!installer_remove_package_prefix("luci-i18n-forkop"))
|
|
packages_removed = false;
|
|
if (!installer_remove_package("luci-app-forkop"))
|
|
packages_removed = false;
|
|
if (!installer_remove_package("forkop"))
|
|
packages_removed = false;
|
|
remove_path("/usr/lib/forkop");
|
|
remove_path("/usr/bin/forkop");
|
|
remove_path("/etc/init.d/forkop");
|
|
remove_path("/etc/uci-defaults/50_luci-forkop");
|
|
remove_path("/www/luci-static/resources/view/forkop");
|
|
remove_path("/usr/share/luci/menu.d/luci-app-forkop.json");
|
|
remove_path("/usr/share/rpcd/acl.d/luci-app-forkop.json");
|
|
remove_path("/tmp/forkop");
|
|
remove_path("/var/run/forkop");
|
|
}
|
|
|
|
if (installer_package_installed("netshift") ||
|
|
installer_package_installed("luci-app-netshift") ||
|
|
path_exists("/etc/config/netshift") ||
|
|
path_executable("/usr/bin/netshift") ||
|
|
path_executable("/etc/init.d/netshift")) {
|
|
if (!installer_remove_package_prefix("luci-i18n-netshift"))
|
|
packages_removed = false;
|
|
if (!installer_remove_package("luci-app-netshift"))
|
|
packages_removed = false;
|
|
if (!installer_remove_package("netshift"))
|
|
packages_removed = false;
|
|
remove_path("/usr/lib/netshift");
|
|
remove_path("/usr/bin/netshift");
|
|
remove_path("/etc/init.d/netshift");
|
|
remove_path("/etc/uci-defaults/50_luci-netshift");
|
|
remove_path("/www/luci-static/resources/view/netshift");
|
|
remove_path("/usr/share/luci/menu.d/luci-app-netshift.json");
|
|
remove_path("/usr/share/rpcd/acl.d/luci-app-netshift.json");
|
|
remove_path("/tmp/netshift");
|
|
remove_path("/var/run/netshift");
|
|
remove_path("/etc/netshift");
|
|
}
|
|
|
|
if (installer_package_manager() == "apk" && file_exists("/etc/apk/world")) {
|
|
for (let orphaned_pkg in [ "sing-box-extended", "sing-box", "sing-box-tiny", "sing-box-lx" ]) {
|
|
if (!installer_package_installed(orphaned_pkg))
|
|
command_success("sed -i -E " + shell_quote("/^" + orphaned_pkg + "([><= ].*)?$/d") + " /etc/apk/world 2>/dev/null");
|
|
}
|
|
}
|
|
|
|
if (!installer_remove_package_prefix("luci-i18n-tachyon"))
|
|
packages_removed = false;
|
|
if (!installer_remove_package("luci-app-tachyon"))
|
|
packages_removed = false;
|
|
|
|
if (!packages_removed) {
|
|
warn("Notice: some legacy or target packages could not be uninstalled prior to upgrade; proceeding with package overwrite.\n");
|
|
}
|
|
|
|
if (legacy_installed) {
|
|
remove_path(INSTALLER_LEGACY_LIB);
|
|
remove_path(INSTALLER_LEGACY_INIT);
|
|
remove_path(INSTALLER_LEGACY_BIN);
|
|
for (let path in [
|
|
INSTALLER_LEGACY_LUCI_VIEW,
|
|
INSTALLER_LEGACY_MENU_JSON,
|
|
INSTALLER_LEGACY_ACL_JSON,
|
|
INSTALLER_LEGACY_UCI_DEFAULTS
|
|
])
|
|
remove_path(path);
|
|
}
|
|
|
|
if (!tachyon_installed) {
|
|
remove_path(INSTALLER_TACHYON_LIB);
|
|
remove_path(INSTALLER_TACHYON_INIT);
|
|
remove_path(INSTALLER_TACHYON_BIN);
|
|
}
|
|
|
|
for (let path in [
|
|
INSTALLER_TACHYON_LUCI_VIEW,
|
|
INSTALLER_MENU_JSON,
|
|
INSTALLER_ACL_JSON,
|
|
INSTALLER_TACHYON_UCI_DEFAULTS,
|
|
INSTALLER_RU_LMO,
|
|
INSTALLER_EN_LMO,
|
|
INSTALLER_RU_LUA,
|
|
INSTALLER_EN_LUA
|
|
])
|
|
remove_path(path);
|
|
|
|
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;
|
|
}
|
|
|
|
function installer_finalize_legacy() {
|
|
if (LEGACY_BRAND == "")
|
|
return false;
|
|
|
|
for (let legacy_ts_dir in [
|
|
INSTALLER_LEGACY_PERSISTENT_DIR + "/tailscale",
|
|
"/etc/forkop/tailscale",
|
|
"/etc/netshift/tailscale"
|
|
]) {
|
|
if (path_exists(legacy_ts_dir)) {
|
|
let entries = fs.lsdir(legacy_ts_dir);
|
|
let tachyon_tailscale_dir = INSTALLER_TACHYON_PERSISTENT_DIR + "/tailscale";
|
|
if (type(entries) != "array" || !run_args([ "mkdir", "-p", tachyon_tailscale_dir ])) {
|
|
warn("Failed to prepare legacy Tailscale state migration; the legacy directory was preserved.\n");
|
|
return false;
|
|
}
|
|
|
|
for (let entry in entries) {
|
|
entry = as_string(entry);
|
|
let source = legacy_ts_dir + "/" + entry;
|
|
let target = tachyon_tailscale_dir + "/" + entry;
|
|
if (path_exists(target))
|
|
continue;
|
|
|
|
let temporary = tachyon_tailscale_dir + "/." + entry + ".tachyon-migrate";
|
|
if (!remove_path(temporary) ||
|
|
!run_args([ "cp", "-a", source, temporary ]) ||
|
|
!run_args([ "mv", temporary, target ])) {
|
|
remove_path(temporary);
|
|
warn("Failed to migrate legacy Tailscale state; the legacy directory was preserved.\n");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let cleaned = true;
|
|
for (let path in [
|
|
INSTALLER_LEGACY_CONFIG,
|
|
INSTALLER_LEGACY_CONFIG_ALT,
|
|
INSTALLER_LEGACY_PERSISTENT_DIR,
|
|
INSTALLER_LEGACY_RUNTIME_DIR,
|
|
INSTALLER_LEGACY_TMP_DIR,
|
|
INSTALLER_LEGACY_TMP_ALT_DIR,
|
|
"/etc/config/forkop",
|
|
"/etc/config/forkop_plus",
|
|
"/etc/forkop",
|
|
"/var/run/forkop",
|
|
"/tmp/forkop",
|
|
"/etc/config/netshift",
|
|
"/etc/netshift",
|
|
"/var/run/netshift",
|
|
"/tmp/netshift"
|
|
])
|
|
if (!remove_path(path))
|
|
cleaned = false;
|
|
|
|
for (let prefix in [
|
|
INSTALLER_LEGACY_CONFIG,
|
|
INSTALLER_LEGACY_CONFIG_ALT,
|
|
INSTALLER_LEGACY_PERSISTENT_DIR,
|
|
INSTALLER_LEGACY_RUNTIME_DIR,
|
|
INSTALLER_LEGACY_TMP_DIR,
|
|
INSTALLER_LEGACY_TMP_ALT_DIR,
|
|
INSTALLER_LEGACY_INIT,
|
|
INSTALLER_LEGACY_BIN,
|
|
INSTALLER_LEGACY_LIB,
|
|
INSTALLER_LEGACY_UCI_DEFAULTS,
|
|
INSTALLER_LEGACY_LUCI_VIEW,
|
|
INSTALLER_LEGACY_MENU_JSON,
|
|
INSTALLER_LEGACY_ACL_JSON,
|
|
INSTALLER_LEGACY_BASE_CONFIG,
|
|
INSTALLER_LEGACY_BASE_PERSISTENT_DIR,
|
|
INSTALLER_LEGACY_BASE_RUNTIME_DIR,
|
|
INSTALLER_LEGACY_BASE_TMP_DIR,
|
|
INSTALLER_LEGACY_BASE_INIT,
|
|
INSTALLER_LEGACY_BASE_BIN,
|
|
INSTALLER_LEGACY_BASE_LIB,
|
|
INSTALLER_LEGACY_BASE_UCI_DEFAULTS,
|
|
INSTALLER_LEGACY_BASE_LUCI_VIEW,
|
|
INSTALLER_LEGACY_BASE_MENU_JSON,
|
|
INSTALLER_LEGACY_BASE_ACL_JSON,
|
|
INSTALLER_LEGACY_BASE_I18N,
|
|
"/etc/config/forkop",
|
|
"/etc/config/forkop_plus",
|
|
"/etc/forkop",
|
|
"/var/run/forkop",
|
|
"/tmp/forkop",
|
|
"/etc/config/netshift",
|
|
"/etc/netshift",
|
|
"/var/run/netshift",
|
|
"/tmp/netshift"
|
|
])
|
|
if (!remove_glob(prefix + "*"))
|
|
cleaned = false;
|
|
|
|
if (!remove_glob(INSTALLER_LEGACY_TMP_PACKAGE_GLOB))
|
|
cleaned = false;
|
|
|
|
for (let root in words(INSTALLER_LEGACY_SCAN_ROOTS))
|
|
if (!remove_legacy_named_children(root))
|
|
cleaned = false;
|
|
|
|
return cleaned;
|
|
}
|
|
|
|
function installer_post_install() {
|
|
remove_globs(env("TACHYON_INSTALLER_LUCI_CACHE_GLOBS", "/var/luci-indexcache* /tmp/luci-indexcache* /var/luci-modulecache* /tmp/luci-modulecache*"));
|
|
for (let path in [
|
|
env("TACHYON_INSTALLER_LATEST_VERSION_CACHE", "/tmp/tachyon.latest-version.cache"),
|
|
env("TACHYON_INSTALLER_SYSTEM_INFO_CACHE", "/var/run/tachyon/system-info.json"),
|
|
env("TACHYON_INSTALLER_SERVER_COUNTRY_CACHE", "/var/run/tachyon/server-country-cache.json"),
|
|
env("TACHYON_INSTALLER_SING_BOX_VERSION_CACHE", "/var/run/tachyon/ui-state/sing-box-version"),
|
|
env("TACHYON_INSTALLER_TMP_SYSTEM_INFO_CACHE", "/tmp/tachyon/system-info.json")
|
|
])
|
|
remove_path(path);
|
|
|
|
if (path_executable(INSTALLER_RPCD_INIT)) {
|
|
run_args([ "timeout", "10", INSTALLER_RPCD_INIT, "restart" ]);
|
|
run_args([ "timeout", "10", "/etc/init.d/uhttpd", "restart" ]);
|
|
}
|
|
if (path_executable(INSTALLER_TACHYON_BIN))
|
|
run_args([ "timeout", "15", INSTALLER_TACHYON_BIN, "luci_postinst" ]);
|
|
|
|
if (env("TACHYON_WAS_ENABLED", "0") == "1" && path_executable(INSTALLER_TACHYON_INIT))
|
|
run_args([ "timeout", "10", INSTALLER_TACHYON_INIT, "enable" ]);
|
|
|
|
if (env("TACHYON_WAS_RUNNING", "0") == "1" && path_executable(INSTALLER_TACHYON_INIT)) {
|
|
if (!run_args([ "timeout", "30", INSTALLER_TACHYON_INIT, "start" ]) &&
|
|
!run_args([ "timeout", "30", INSTALLER_TACHYON_INIT, "restart" ]))
|
|
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;
|
|
}
|
|
|
|
function list_has(values, needle) {
|
|
for (let value in words(values))
|
|
if (value == needle)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function dnsmasq_managed_instance_exists() {
|
|
return uci_exists("dhcp." + dns_owner_section);
|
|
}
|
|
|
|
function dnsmasq_default_servers() {
|
|
return uci_get("dhcp.@dnsmasq[0].server");
|
|
}
|
|
|
|
function dnsmasq_default_has_managed_dns() {
|
|
return list_has(dnsmasq_default_servers(), "127.0.0.42");
|
|
}
|
|
|
|
function dnsmasq_has_managed_dns() {
|
|
return dnsmasq_default_has_managed_dns() || dnsmasq_managed_instance_exists();
|
|
}
|
|
|
|
function dnsmasq_has_managed_state() {
|
|
return uci_get("dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "server") != "" ||
|
|
uci_get("dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "noresolv") != "" ||
|
|
uci_get("dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "cachesize") != "" ||
|
|
uci_get("dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "notinterface") != "" ||
|
|
dnsmasq_managed_instance_exists();
|
|
}
|
|
|
|
function dnsmasq_management_disabled() {
|
|
return truthy(uci_get(dns_owner_config + ".settings.dont_touch_dhcp"));
|
|
}
|
|
|
|
function dnsmasq_managed_interfaces() {
|
|
let interfaces = uci_get("dhcp." + dns_owner_section + ".interface");
|
|
if (interfaces == "")
|
|
interfaces = uci_get(dns_owner_config + ".settings.source_network_interfaces");
|
|
if (interfaces == "")
|
|
interfaces = "br-lan";
|
|
|
|
return interfaces;
|
|
}
|
|
|
|
function dnsmasq_cleanup_managed_instance() {
|
|
let managed_instance_present = dnsmasq_managed_instance_exists();
|
|
let managed_interfaces = managed_instance_present ? dnsmasq_managed_interfaces() : "";
|
|
|
|
uci_delete("dhcp." + dns_owner_section);
|
|
|
|
let backup_option = "dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "notinterface";
|
|
let backup_notinterfaces = uci_get(backup_option);
|
|
if (backup_notinterfaces != "") {
|
|
uci_delete("dhcp.@dnsmasq[0].notinterface");
|
|
for (let value in words(backup_notinterfaces))
|
|
uci_add_list("dhcp.@dnsmasq[0].notinterface", value);
|
|
uci_delete(backup_option);
|
|
return;
|
|
}
|
|
|
|
if (managed_instance_present) {
|
|
for (let value in words(managed_interfaces))
|
|
uci_del_list("dhcp.@dnsmasq[0].notinterface", value);
|
|
}
|
|
|
|
uci_delete(backup_option);
|
|
}
|
|
|
|
function dnsmasq_restore_default_instance() {
|
|
let server_list = dnsmasq_default_servers();
|
|
let server_backup_option = "dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "server";
|
|
let backup_servers = uci_get(server_backup_option);
|
|
let managed_global_dns = list_has(server_list, "127.0.0.42");
|
|
|
|
uci_delete("dhcp.@dnsmasq[0].server");
|
|
if (backup_servers != "") {
|
|
for (let value in words(backup_servers))
|
|
uci_add_list("dhcp.@dnsmasq[0].server", value);
|
|
uci_delete(server_backup_option);
|
|
}
|
|
else {
|
|
for (let value in words(server_list)) {
|
|
if (value != "127.0.0.42")
|
|
uci_add_list("dhcp.@dnsmasq[0].server", value);
|
|
}
|
|
}
|
|
uci_delete(server_backup_option);
|
|
|
|
let noresolv_backup_option = "dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "noresolv";
|
|
let noresolv = uci_get(noresolv_backup_option);
|
|
if (noresolv != "") {
|
|
uci_set("dhcp.@dnsmasq[0].noresolv", noresolv);
|
|
uci_delete(noresolv_backup_option);
|
|
}
|
|
else if (managed_global_dns) {
|
|
uci_set("dhcp.@dnsmasq[0].noresolv", "0");
|
|
}
|
|
|
|
let cachesize_backup_option = "dhcp.@dnsmasq[0]." + dns_owner_option_prefix + "cachesize";
|
|
let cachesize = uci_get(cachesize_backup_option);
|
|
if (cachesize != "") {
|
|
uci_set("dhcp.@dnsmasq[0].cachesize", cachesize);
|
|
uci_delete(cachesize_backup_option);
|
|
}
|
|
else if (managed_global_dns) {
|
|
uci_set("dhcp.@dnsmasq[0].cachesize", "150");
|
|
}
|
|
}
|
|
|
|
dnsmasq_failsafe_restore = function() {
|
|
if (!uci_available())
|
|
return true;
|
|
|
|
if (dnsmasq_management_disabled() && !dnsmasq_has_managed_state())
|
|
return true;
|
|
|
|
if (!dnsmasq_has_managed_dns() && !dnsmasq_has_managed_state())
|
|
return true;
|
|
|
|
dnsmasq_cleanup_managed_instance();
|
|
dnsmasq_restore_default_instance();
|
|
uci_commit("dhcp");
|
|
restart_dnsmasq();
|
|
return true;
|
|
};
|
|
|
|
function release_version_valid(value) {
|
|
return match(as_string(value), /^[0-9]+[.][0-9]+[.][0-9]+$/) != null;
|
|
}
|
|
|
|
function asset_matches(name, kind, ext, version) {
|
|
if (kind == "sha256sums")
|
|
return name == "sha256sums.txt";
|
|
|
|
if (!release_version_valid(version))
|
|
return false;
|
|
|
|
if (kind == "backend")
|
|
return name == "tachyon_" + version + "." + ext;
|
|
if (kind == "app")
|
|
return name == "luci-app-tachyon_" + version + "." + ext;
|
|
if (kind == "i18n")
|
|
return name == "luci-i18n-tachyon-ru_" + version + "." + ext;
|
|
return false;
|
|
}
|
|
|
|
function github_message() {
|
|
let value = read_stdin_json();
|
|
if (type(value) == "array")
|
|
value = value[0];
|
|
if (value == null)
|
|
exit(2);
|
|
if (type(value) == "object" && value.message != null)
|
|
print(as_string(value.message), "\n");
|
|
}
|
|
|
|
function release_tag() {
|
|
let release = read_stdin_json();
|
|
if (type(release) == "array")
|
|
release = release[0];
|
|
if (type(release) == "object" && release.tag_name != null)
|
|
print(as_string(release.tag_name), "\n");
|
|
}
|
|
|
|
function release_asset_url(kind, ext) {
|
|
let release = read_stdin_json();
|
|
if (type(release) == "array")
|
|
release = release[0];
|
|
if (type(release) != "object" || type(release.assets) != "array")
|
|
return;
|
|
let version = as_string(release.tag_name || "");
|
|
if (!release_version_valid(version))
|
|
return;
|
|
for (let asset in release.assets) {
|
|
if (type(asset) == "object" && asset_matches(asset.name, kind, ext, version)) {
|
|
print(as_string(asset.browser_download_url || ""), "\n");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "github-message")
|
|
github_message();
|
|
else if (mode == "release-tag")
|
|
release_tag();
|
|
else if (mode == "release-asset-url")
|
|
release_asset_url(ARGV[1], ARGV[2]);
|
|
else if (mode == "uci-get") {
|
|
let value = uci_get(ARGV[1]);
|
|
if (value != "")
|
|
print(value, "\n");
|
|
}
|
|
else if (mode == "dnsmasq-failsafe-restore")
|
|
exit(dnsmasq_failsafe_restore() ? 0 : 1);
|
|
else if (mode == "installer-cleanup-legacy")
|
|
exit(installer_cleanup_legacy() ? 0 : 1);
|
|
else if (mode == "installer-finalize-legacy")
|
|
exit(installer_finalize_legacy() ? 0 : 1);
|
|
else if (mode == "installer-post-install")
|
|
exit(installer_post_install() ? 0 : 1);
|
|
else
|
|
exit(1);
|
|
EOF
|
|
|
|
printf '%s\n' "$helper_path"
|
|
}
|
|
|
|
|
|
install_json_ucode() {
|
|
TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \
|
|
TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND_PACKAGE" \
|
|
TACHYON_INSTALLER_LEGACY_CONFIG_ALT="$LEGACY_CONFIG_PACKAGE_ALT" \
|
|
TACHYON_INSTALLER_ASSUME_YES="$ASSUME_YES" \
|
|
TACHYON_LEGACY_DETECTED="$TACHYON_LEGACY_DETECTED" \
|
|
TACHYON_FORKOP_MIGRATION="$TACHYON_FORKOP_MIGRATION" \
|
|
TACHYON_NETSHIFT_MIGRATION="$TACHYON_NETSHIFT_MIGRATION" \
|
|
ucode "$(install_json_helper_path)" "$@"
|
|
}
|
|
|
|
download_file_once() {
|
|
case "$FETCHER" in
|
|
wget)
|
|
run_with_deadline "$DOWNLOAD_TIMEOUT_SECONDS" wget -T "$CONNECT_TIMEOUT_SECONDS" -q -O "$2" "$1"
|
|
;;
|
|
curl)
|
|
curl --connect-timeout "$CONNECT_TIMEOUT_SECONDS" --speed-limit 1024 --speed-time 15 --max-time "$DOWNLOAD_TIMEOUT_SECONDS" -fsSL "$1" -o "$2"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
github_to_jsdelivr() {
|
|
_url="$1"
|
|
case "$_url" in
|
|
*github.com*)
|
|
_path="${_url#*github.com/}"
|
|
case "$_path" in
|
|
*/releases/download/*)
|
|
_repo_owner="$(printf '%s' "$_path" | cut -d'/' -f1)"
|
|
_repo_name="$(printf '%s' "$_path" | cut -d'/' -f2)"
|
|
_tag="$(printf '%s' "$_path" | cut -d'/' -f4)"
|
|
_file="$(printf '%s' "$_path" | cut -d'/' -f5-)"
|
|
printf 'https://cdn.jsdelivr.net/gh/%s/%s@%s/%s' "$_repo_owner" "$_repo_name" "$_tag" "$_file"
|
|
return 0
|
|
;;
|
|
*/raw/*)
|
|
_repo_owner="$(printf '%s' "$_path" | cut -d'/' -f1)"
|
|
_repo_name="$(printf '%s' "$_path" | cut -d'/' -f2)"
|
|
_branch="$(printf '%s' "$_path" | cut -d'/' -f4)"
|
|
_file="$(printf '%s' "$_path" | cut -d'/' -f5-)"
|
|
printf 'https://cdn.jsdelivr.net/gh/%s/%s@%s/%s' "$_repo_owner" "$_repo_name" "$_branch" "$_file"
|
|
return 0
|
|
;;
|
|
*/*)
|
|
_repo_owner="$(printf '%s' "$_path" | cut -d'/' -f1)"
|
|
_repo_name="$(printf '%s' "$_path" | cut -d'/' -f2)"
|
|
_ref="$(printf '%s' "$_path" | cut -d'/' -f4)"
|
|
_file="$(printf '%s' "$_path" | cut -d'/' -f5-)"
|
|
[ -n "$_file" ] && {
|
|
printf 'https://cdn.jsdelivr.net/gh/%s/%s@%s/%s' "$_repo_owner" "$_repo_name" "$_ref" "$_file"
|
|
return 0
|
|
}
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
printf '%s' "$_url"
|
|
}
|
|
|
|
download_with_retry() {
|
|
url="$1"
|
|
output_path="$2"
|
|
label="$3"
|
|
dl_num="${4:-}"
|
|
dl_total="${5:-}"
|
|
attempt=1
|
|
max_attempts=4
|
|
jsdelivr_url="$(github_to_jsdelivr "$url")"
|
|
|
|
while [ "$attempt" -le "$max_attempts" ]; do
|
|
current_url="$url"
|
|
if [ "$attempt" -eq 2 ] && [ "$jsdelivr_url" != "$url" ]; then
|
|
warn "Retrying $label via jsdelivr.net CDN..."
|
|
current_url="$jsdelivr_url"
|
|
elif [ "$attempt" -eq 2 ] || ([ "$attempt" -eq 3 ] && [ "$jsdelivr_url" = "$url" ]); then
|
|
warn "Retrying $label via gh-proxy.com mirror..."
|
|
current_url="https://gh-proxy.com/$url"
|
|
elif [ "$attempt" -eq 3 ] && [ "$jsdelivr_url" != "$url" ]; then
|
|
warn "Retrying $label via gh-proxy.com mirror..."
|
|
current_url="https://gh-proxy.com/$url"
|
|
elif [ "$attempt" -eq 4 ]; then
|
|
warn "Retrying $label via ghproxy.net mirror..."
|
|
current_url="https://ghproxy.net/$url"
|
|
fi
|
|
|
|
if [ -n "$dl_num" ] && [ -n "$dl_total" ]; then
|
|
msg "[$dl_num/$dl_total] Downloading $label"
|
|
else
|
|
msg "Downloading $label"
|
|
fi
|
|
|
|
if download_file_once "$current_url" "$output_path" && [ -s "$output_path" ]; then
|
|
return 0
|
|
fi
|
|
|
|
rm -f "$output_path"
|
|
warn "Retrying $label"
|
|
attempt=$((attempt + 1))
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
pkg_run_timeout() {
|
|
_secs="$1"
|
|
shift
|
|
if command_exists timeout; then
|
|
timeout "$_secs" "$@"
|
|
else
|
|
run_with_deadline "$_secs" "$@"
|
|
fi
|
|
}
|
|
|
|
pkg_is_installed() {
|
|
pkg_name="$1"
|
|
|
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
|
apk info 2>/dev/null | grep -qx "$pkg_name"
|
|
else
|
|
opkg list-installed 2>/dev/null | awk -v pkg="$pkg_name" '$1 == pkg { found = 1 } END { exit(found ? 0 : 1) }'
|
|
fi
|
|
}
|
|
|
|
pkg_list_update() {
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would update package lists ($([ "$PKG_IS_APK" -eq 1 ] && echo apk || echo opkg))"
|
|
return 0
|
|
fi
|
|
|
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
|
apk_with_lock_retry "apk" 120 apk update
|
|
else
|
|
run_logged_timeout "opkg" 120 opkg update
|
|
fi
|
|
local rc=$?
|
|
|
|
if [ $rc -ne 0 ]; then
|
|
log_line "FAIL package update failed (exit $rc)"
|
|
printf '%s%s--- package manager error ---%s\n' "$_c_red" "" "$_c_reset" >&2
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
pkg_install_name() {
|
|
pkg_name="$1"
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would install package: $pkg_name"
|
|
return 0
|
|
fi
|
|
|
|
# 300s: named packages carry postinst hooks too (sing-box, kmods pulling deps
|
|
# from the network), and a kill mid-transaction is rolled back on apk.
|
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
|
apk_with_lock_retry "apk" 300 apk add "$pkg_name"
|
|
else
|
|
run_logged_timeout "opkg" 300 opkg install "$pkg_name"
|
|
fi
|
|
local rc=$?
|
|
|
|
if [ $rc -ne 0 ]; then
|
|
log_line "FAIL package install failed for $pkg_name (exit $rc)"
|
|
printf '%s%s--- package install error for %s ---%s\n' "$_c_red" "" "$pkg_name" "$_c_reset" >&2
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Commit SHA baked into the currently installed backend, read straight from the
|
|
# installed constants module. Empty when nothing is installed or the placeholder
|
|
# was never substituted. This is what tells a rebuild of the same tag apart from
|
|
# the build already on disk — the version string cannot.
|
|
installed_backend_sha() {
|
|
_sha_file="/usr/lib/tachyon/core/constants.uc"
|
|
[ -r "$_sha_file" ] || return 0
|
|
# "unknown" is what the Makefile substitutes when git is unavailable at build
|
|
# time, and the placeholder survives an unprocessed tree; neither identifies a
|
|
# build, so both must read as "no SHA" rather than as a SHA that never moves.
|
|
sed -n 's/.*TACHYON_COMMIT_SHA", *"\([^"]*\)".*/\1/p' "$_sha_file" 2>/dev/null |
|
|
head -1 | grep -v -e COMPILED -e '^unknown$' || true
|
|
}
|
|
|
|
pkg_install_files() {
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would install downloaded package file(s): $*"
|
|
return 0
|
|
fi
|
|
|
|
# Recorded before the transaction so a rolled-back rebuild of the same tag is
|
|
# detectable: the version stays identical, only the SHA moves.
|
|
local sha_before
|
|
sha_before="$(installed_backend_sha)"
|
|
|
|
if [ "$PKG_IS_APK" -eq 1 ]; then
|
|
# 300s, not 120s: the package hooks stop and restart the service, and on a
|
|
# loaded router that legitimately outlives two minutes. Being killed here
|
|
# makes apk roll the whole transaction back, which silently leaves the old
|
|
# build installed — the exact failure this ceiling used to cause.
|
|
apk_with_lock_retry "apk" 300 apk add --allow-untrusted "$@"
|
|
local rc=$?
|
|
if [ $rc -ne 0 ]; then
|
|
# apk may report warnings as errors even though the package installed successfully
|
|
# (e.g., ip-full conflict with ip, missing optional deps, etc.)
|
|
# Verify the CORRECT version is now installed, not just any version.
|
|
local pkg_file
|
|
for pkg_file in "$@"; do
|
|
local pkg_name pkg_expected_ver pkg_actual_ver
|
|
pkg_name="$(basename "$pkg_file" | sed 's/\.[^.]*$//; s/_[0-9].*$//')"
|
|
pkg_expected_ver="$(basename "$pkg_file" | sed 's/^[^_]*_//; s/\.apk$//')"
|
|
pkg_actual_ver="$(apk info "$pkg_name" 2>/dev/null | head -1 | sed "s/${pkg_name}-//")"
|
|
if [ "$pkg_actual_ver" = "$pkg_expected_ver" ]; then
|
|
# A matching version proves nothing for a rebuilt tag: it also
|
|
# matches when apk rolled back to the identical old version. If
|
|
# the backend SHA did not move either, nothing was written.
|
|
if [ "$pkg_name" = "tachyon" ] && [ -n "$sha_before" ] &&
|
|
[ "$(installed_backend_sha)" = "$sha_before" ]; then
|
|
log_line "FAIL apk install failed (exit $rc); backend still at $sha_before, transaction was rolled back"
|
|
return $rc
|
|
fi
|
|
warn "Package manager reported non-critical errors during installation (package is installed OK)"
|
|
return 0
|
|
fi
|
|
done
|
|
log_line "FAIL apk install failed (exit $rc); package was rolled back"
|
|
return $rc
|
|
fi
|
|
else
|
|
# --force-reinstall: a release tag can be rebuilt, so the .ipk version may
|
|
# equal the installed one. Without it opkg treats that as "already
|
|
# installed" and the rebuild is never written.
|
|
# 300s for the same reason as the apk branch: the hooks restart the service.
|
|
run_logged_timeout "opkg" 300 opkg install --force-overwrite --force-downgrade --force-reinstall "$@"
|
|
local rc=$?
|
|
if [ $rc -ne 0 ]; then
|
|
log_line "FAIL opkg install failed (exit $rc)"
|
|
return $rc
|
|
fi
|
|
fi
|
|
}
|
|
|
|
ensure_bootstrap_tool() {
|
|
tool_name="$1"
|
|
package_name="$2"
|
|
|
|
if command_exists "$tool_name"; then
|
|
return 0
|
|
fi
|
|
|
|
msg "$(installer_text bootstrap_dep): $package_name"
|
|
pkg_install_name "$package_name" || fail "Failed to install $package_name"
|
|
}
|
|
|
|
ensure_bootstrap_package() {
|
|
package_name="$1"
|
|
|
|
if pkg_is_installed "$package_name"; then
|
|
return 0
|
|
fi
|
|
|
|
msg "$(installer_text bootstrap_dep): $package_name"
|
|
pkg_install_name "$package_name" || fail "Failed to install $package_name"
|
|
}
|
|
|
|
ensure_bootstrap_ucode_runtime() {
|
|
ensure_bootstrap_tool "ucode" "ucode"
|
|
ensure_bootstrap_package "ucode-mod-fs"
|
|
ensure_bootstrap_package "ucode-mod-uci"
|
|
}
|
|
|
|
sync_time() {
|
|
current_year=""
|
|
|
|
if ! command_exists ntpd; then
|
|
return 0
|
|
fi
|
|
|
|
current_year="$(date +%Y 2>/dev/null || true)"
|
|
case "$current_year" in
|
|
''|*[!0-9]*) current_year=0 ;;
|
|
esac
|
|
|
|
if [ "$current_year" -ge 2024 ]; then
|
|
return 0
|
|
fi
|
|
|
|
ntpd -q \
|
|
-p 194.190.168.1 \
|
|
-p 216.239.35.0 \
|
|
-p 216.239.35.4 \
|
|
-p 162.159.200.1 \
|
|
-p 162.159.200.123 >/dev/null 2>&1 || true
|
|
}
|
|
|
|
check_root() {
|
|
if command_exists id && [ "$(id -u)" != "0" ]; then
|
|
fail "Please run this installer as root"
|
|
fi
|
|
}
|
|
|
|
log_system_audit() {
|
|
log_line "================================================================================"
|
|
log_line "Tachyon Installer v${INSTALLER_VERSION} Audit Log Started"
|
|
log_line "Timestamp : $(date 2>/dev/null || echo '----------------')"
|
|
log_line "Kernel : $(uname -a 2>/dev/null || echo 'N/A')"
|
|
log_line "Router Model : $(cat /tmp/sysinfo/model 2>/dev/null || echo 'N/A')"
|
|
log_line "OpenWrt Release : $(read_openwrt_release_value "DISTRIB_RELEASE" 2>/dev/null || echo 'N/A') ($(read_openwrt_release_value "DISTRIB_TARGET" 2>/dev/null || echo 'N/A'))"
|
|
log_line "Package Manager : $([ "$PKG_IS_APK" -eq 1 ] && echo 'apk' || echo 'opkg')"
|
|
log_line "Free Overlay : $(df -h /overlay 2>/dev/null | awk 'NR==2 {print $4}' || df -h / 2>/dev/null | awk 'NR==2 {print $4}')"
|
|
log_line "LuCI Main Lang : $(get_luci_main_lang 2>/dev/null || echo 'auto')"
|
|
log_line "================================================================================"
|
|
}
|
|
|
|
check_system() {
|
|
release=""
|
|
major=""
|
|
model=""
|
|
available_space=""
|
|
|
|
[ -f /etc/openwrt_release ] || fail "This installer supports OpenWrt only"
|
|
|
|
log_system_audit
|
|
|
|
model="$(cat /tmp/sysinfo/model 2>/dev/null || true)"
|
|
[ -n "$model" ] && msg "$(installer_text router_model): $model"
|
|
|
|
release="$(read_openwrt_release_value "DISTRIB_RELEASE")"
|
|
major="$(printf '%s' "$release" | sed 's/[^0-9].*$//' | cut -d. -f1)"
|
|
|
|
if [ -n "$major" ] && [ "$major" -lt 23 ]; then
|
|
fail "Tachyon requires OpenWrt 23.05 or newer"
|
|
fi
|
|
|
|
available_space="$(df /overlay 2>/dev/null | awk 'NR==2 {print $4}')"
|
|
[ -n "$available_space" ] || available_space="$(df / 2>/dev/null | awk 'NR==2 {print $4}')"
|
|
|
|
if [ -n "$available_space" ] && [ "$available_space" -lt "$REQUIRED_SPACE_KB" ]; then
|
|
fail "Not enough free flash space. Available: $((available_space / 1024)) MB, required: $((REQUIRED_SPACE_KB / 1024)) MB"
|
|
fi
|
|
}
|
|
|
|
installer_is_ru() {
|
|
[ "$INSTALLER_LANG" = "ru" ]
|
|
}
|
|
|
|
installer_text() {
|
|
key="$1"
|
|
|
|
if installer_is_ru; then
|
|
case "$key" in
|
|
yes) printf '%s\n' "Да" ;;
|
|
no) printf '%s\n' "Нет" ;;
|
|
select) printf '%s\n' "Выберите номер" ;;
|
|
invalid_choice) printf '%s\n' "Введите номер из списка." ;;
|
|
router_model) printf '%s\n' "Модель роутера" ;;
|
|
bootstrap_dep) printf '%s\n' "Установка базовой зависимости" ;;
|
|
i18n_installed) printf '%s\n' "Русский пакет интерфейса уже установлен и будет обновлен." ;;
|
|
i18n_prompt) printf '%s\n' "Установить русский пакет интерфейса?" ;;
|
|
i18n_skip) printf '%s\n' "Продолжаю без русского пакета интерфейса." ;;
|
|
luci_ru) printf '%s\n' "Русский пакет интерфейса будет установлен автоматически." ;;
|
|
sing_box_prompt) printf '%s\n' "Какую сборку singbox ставить?" ;;
|
|
sing_box_stable) printf '%s\n' "singbox stable (без xhttp)" ;;
|
|
sing_box_extended) printf '%s\n' "singbox extended (если нужен xhttp)" ;;
|
|
sing_box_extended_compressed) printf '%s\n' "singbox extended (сжатая UPX версия)" ;;
|
|
sing_box_tiny) printf '%s\n' "singbox tiny (минимум функций)" ;;
|
|
sing_box_lx) printf '%s\n' "singbox Leadaxe (lx)" ;;
|
|
sing_box_skip) printf '%s\n' "Не устанавливать sing-box (пропустить)" ;;
|
|
sing_box_skip_msg) printf '%s\n' "Пропускаю установку sing-box." ;;
|
|
installing_singbox_backend) printf '%s\n' "Установка выбранного варианта sing-box..." ;;
|
|
install_start) printf '%s\n' "=== Начало установки Tachyon ===" ;;
|
|
pkg_list_update) printf '%s\n' "Обновление списков пакетов..." ;;
|
|
resolving_release) printf '%s\n' "Определение последней версии релиза Tachyon..." ;;
|
|
downloading_packages) printf '%s\n' "Скачивание пакетов релиза Tachyon..." ;;
|
|
cleaning_legacy) printf '%s\n' "Удаление старых или конфликтующих пакетов..." ;;
|
|
installing_backend) printf '%s\n' "Установка основного пакета Tachyon..." ;;
|
|
migrating_config) printf '%s\n' "Перенос существующей конфигурации..." ;;
|
|
migrating_config_detail) printf '%s\n' "Перенос конфигурации предыдущей версии в Tachyon..." ;;
|
|
installing_ui) printf '%s\n' "Установка пакетов интерфейса LuCI..." ;;
|
|
installing_singbox) printf '%s\n' "Установка выбранной версии sing-box..." ;;
|
|
running_postinstall) printf '%s\n' "Применение финальных настроек (post-install)..." ;;
|
|
step_done) printf '%s\n' "Завершено" ;;
|
|
dry_run_warn) printf '%s\n' "Тестовый запуск: изменения в пакеты, файлы и конфигурацию вноситься не будут." ;;
|
|
legacy_detected) printf '%s\n' "Обнаружена предыдущая версия; старые пакеты будут удалены, а конфигурация обновлена." ;;
|
|
verifying_checksums) printf '%s\n' "Проверка контрольных сумм пакетов..." ;;
|
|
checksums_ok) printf '%s\n' "Контрольные суммы успешно проверены." ;;
|
|
kmod_check) printf '%s\n' "Проверка и установка необходимых модулей ядра..." ;;
|
|
summary_dry_run) printf '%s\n' "Тестовый запуск завершён за %ss — изменения не вносились." ;;
|
|
summary_success) printf '%s\n' "Tachyon %s успешно установлен (%ss)" ;;
|
|
summary_release) printf '%s\n' "Релиз" ;;
|
|
summary_legacy_removed) printf '%s\n' "Предыдущая версия перенесена и удалена." ;;
|
|
summary_log) printf '%s\n' "Лог" ;;
|
|
summary_luci_notice) printf '%s\n' "Откройте LuCI и проверьте правила перед включением Tachyon. Если меню не появилось сразу, обновите страницу (Ctrl+F5) или перезайдите в LuCI." ;;
|
|
zram_low_mem_warning) printf '%s\n' "ВНИМАНИЕ: Обнаружен малый объем оперативной памяти (%s МБ). При работе sing-box возможен сбой ядра Out-Of-Memory (OOM-killer)." ;;
|
|
zram_prompt) printf '%s\n' "Настоятельно рекомендуется установить zram-swap (сжатый swap в RAM) для стабильной работы. Установить zram-swap?" ;;
|
|
zram_auto_enable) printf '%s\n' "Автоматически выбираю установку zram-swap для стабильной работы при малом объеме RAM." ;;
|
|
zram_installing) printf '%s\n' "Установка и активация zram-swap..." ;;
|
|
zram_success) printf '%s\n' "zram-swap успешно установлен и запущен." ;;
|
|
zram_skipped) printf '%s\n' "Установка zram-swap пропущена. Внимание: возможны сбои OOM-killer при нехватке памяти." ;;
|
|
zram_requested) printf '%s\n' "Выбрана установка zram-swap." ;;
|
|
zram_status) printf '%s\n' "RAM Swap (zram)" ;;
|
|
*) printf '%s\n' "$key" ;;
|
|
esac
|
|
return 0
|
|
fi
|
|
|
|
case "$key" in
|
|
yes) printf '%s\n' "Yes" ;;
|
|
no) printf '%s\n' "No" ;;
|
|
select) printf '%s\n' "Select a number" ;;
|
|
invalid_choice) printf '%s\n' "Enter a number from the list." ;;
|
|
router_model) printf '%s\n' "Router model" ;;
|
|
bootstrap_dep) printf '%s\n' "Installing bootstrap dependency" ;;
|
|
i18n_installed) printf '%s\n' "The Russian interface package is already installed and will be updated." ;;
|
|
i18n_prompt) printf '%s\n' "Install the Russian interface language package?" ;;
|
|
i18n_skip) printf '%s\n' "Continuing without the Russian interface language package." ;;
|
|
luci_ru) printf '%s\n' "The Russian interface package will be installed automatically." ;;
|
|
sing_box_prompt) printf '%s\n' "Which singbox build should be installed?" ;;
|
|
sing_box_stable) printf '%s\n' "singbox stable (without xhttp)" ;;
|
|
sing_box_extended) printf '%s\n' "singbox extended (if xhttp is needed)" ;;
|
|
sing_box_extended_compressed) printf '%s\n' "singbox extended (UPX compressed)" ;;
|
|
sing_box_tiny) printf '%s\n' "singbox tiny (minimal features)" ;;
|
|
sing_box_lx) printf '%s\n' "singbox Leadaxe (lx)" ;;
|
|
sing_box_skip) printf '%s\n' "Do not install sing-box (skip)" ;;
|
|
sing_box_skip_msg) printf '%s\n' "Skipping sing-box installation." ;;
|
|
installing_singbox_backend) printf '%s\n' "Installing selected sing-box variant..." ;;
|
|
install_start) printf '%s\n' "=== Starting Tachyon Installation ===" ;;
|
|
pkg_list_update) printf '%s\n' "Updating package lists..." ;;
|
|
resolving_release) printf '%s\n' "Resolving latest Tachyon release version..." ;;
|
|
downloading_packages) printf '%s\n' "Downloading Tachyon release packages..." ;;
|
|
cleaning_legacy) printf '%s\n' "Cleaning up legacy or conflicting packages..." ;;
|
|
installing_backend) printf '%s\n' "Installing Tachyon backend package..." ;;
|
|
migrating_config) printf '%s\n' "Migrating legacy configuration..." ;;
|
|
migrating_config_detail) printf '%s\n' "Migrating legacy configuration to Tachyon..." ;;
|
|
installing_ui) printf '%s\n' "Installing Tachyon LuCI web interface packages..." ;;
|
|
installing_singbox) printf '%s\n' "Installing selected sing-box variant..." ;;
|
|
running_postinstall) printf '%s\n' "Running post-install configuration..." ;;
|
|
step_done) printf '%s\n' "Done" ;;
|
|
dry_run_warn) printf '%s\n' "Dry-run mode: no packages, files, or configuration will be changed." ;;
|
|
legacy_detected) printf '%s\n' "Legacy installation detected; its packages will be removed and its configuration will be upgraded" ;;
|
|
verifying_checksums) printf '%s\n' "Verifying package checksums..." ;;
|
|
checksums_ok) printf '%s\n' "Checksums verified successfully." ;;
|
|
kmod_check) printf '%s\n' "Ensuring optional kernel module dependencies (best effort)..." ;;
|
|
summary_dry_run) printf '%s\n' "Dry run complete in %ss — no changes were made." ;;
|
|
summary_success) printf '%s\n' "Tachyon %s installed successfully (%ss)" ;;
|
|
summary_release) printf '%s\n' "Release" ;;
|
|
summary_legacy_removed) printf '%s\n' "Legacy installation migrated and removed." ;;
|
|
summary_log) printf '%s\n' "Log" ;;
|
|
summary_luci_notice) printf '%s\n' "Open LuCI and review your rules before enabling Tachyon. If the menu doesn't appear immediately, hard-refresh the page (Ctrl+F5) or re-login." ;;
|
|
zram_low_mem_warning) printf '%s\n' "WARNING: Low RAM detected (%s MB). Running sing-box may trigger kernel Out-Of-Memory (OOM-killer) under load." ;;
|
|
zram_prompt) printf '%s\n' "It is strongly recommended to install zram-swap (compressed RAM swap) for stability. Install zram-swap?" ;;
|
|
zram_auto_enable) printf '%s\n' "Auto-selecting zram-swap for stability on low-RAM device." ;;
|
|
zram_installing) printf '%s\n' "Installing and activating zram-swap..." ;;
|
|
zram_success) printf '%s\n' "zram-swap installed and activated successfully." ;;
|
|
zram_skipped) printf '%s\n' "Skipped zram-swap installation. Warning: Out-Of-Memory crashes may occur under load." ;;
|
|
zram_requested) printf '%s\n' "zram-swap installation requested." ;;
|
|
zram_status) printf '%s\n' "RAM Swap (zram)" ;;
|
|
*) printf '%s\n' "$key" ;;
|
|
esac
|
|
}
|
|
|
|
detect_installer_language() {
|
|
luci_lang="$(get_luci_main_lang)"
|
|
|
|
INSTALLER_LANG="en"
|
|
if pkg_is_installed "luci-i18n-tachyon-ru"; then
|
|
INSTALLER_LANG="ru"
|
|
return 0
|
|
fi
|
|
|
|
case "$luci_lang" in
|
|
ru|ru_*|ru-*) INSTALLER_LANG="ru" ;;
|
|
esac
|
|
}
|
|
|
|
numbered_yes_no_prompt() {
|
|
prompt_text="$1"
|
|
answer=""
|
|
|
|
if [ "$ASSUME_YES" -eq 1 ] || [ ! -t 0 ]; then
|
|
msg "$prompt_text: 1 ($(installer_text yes), non-interactive)"
|
|
return 0
|
|
fi
|
|
|
|
while :; do
|
|
printf '\n%s\n' "$prompt_text"
|
|
printf ' 1) %s\n' "$(installer_text yes)"
|
|
printf ' 2) %s\n' "$(installer_text no)"
|
|
printf '%s [2]: ' "$(installer_text select)"
|
|
read -r answer || return 1
|
|
|
|
case "$answer" in
|
|
1)
|
|
return 0
|
|
;;
|
|
2|"")
|
|
return 1
|
|
;;
|
|
*)
|
|
warn "$(installer_text invalid_choice)"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
confirm_prompt() {
|
|
prompt_text="$1"
|
|
numbered_yes_no_prompt "$prompt_text"
|
|
}
|
|
|
|
get_luci_main_lang() {
|
|
if [ -r /etc/config/luci ]; then
|
|
_cfg_lang="$(sed -n "s/.*option lang ['\"]\?\([a-zA-Z_]\+\)['\"]\?/\1/p" /etc/config/luci 2>/dev/null | head -n 1)"
|
|
[ -n "$_cfg_lang" ] && [ "$_cfg_lang" != "auto" ] && printf '%s\n' "$_cfg_lang" && return 0
|
|
fi
|
|
command_exists ucode || return 0
|
|
ucode -e 'require("fs"); require("uci");' >/dev/null 2>&1 || return 0
|
|
install_json_ucode uci-get luci.main.lang 2>/dev/null || true
|
|
}
|
|
|
|
extract_package_version() {
|
|
package_name="$1"
|
|
|
|
case "$package_name" in
|
|
tachyon_*.ipk|tachyon_*.apk)
|
|
printf '%s\n' "$package_name" | sed 's/^tachyon_//;s/\.ipk$//;s/\.apk$//'
|
|
;;
|
|
luci-app-tachyon_*.ipk|luci-app-tachyon_*.apk)
|
|
printf '%s\n' "$package_name" | sed 's/^luci-app-tachyon_//;s/\.ipk$//;s/\.apk$//'
|
|
;;
|
|
luci-i18n-tachyon-ru_*.ipk|luci-i18n-tachyon-ru_*.apk)
|
|
printf '%s\n' "$package_name" | sed 's/^luci-i18n-tachyon-ru_//;s/\.ipk$//;s/\.apk$//'
|
|
;;
|
|
*)
|
|
printf '%s\n' "$package_name"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
fetch_github_latest_release_json() {
|
|
owner="$1"
|
|
repo="$2"
|
|
response=""
|
|
message=""
|
|
url="https://api.github.com/repos/${owner}/${repo}/releases/latest"
|
|
|
|
response="$(http_get "$url" 2>/dev/null || true)"
|
|
if [ -z "$response" ] || printf '%s' "$response" | grep -q '"message": "Not Found"'; then
|
|
warn "Latest release query failed; trying via gh-proxy.com mirror..."
|
|
response="$(http_get "https://gh-proxy.com/$url" 2>/dev/null || true)"
|
|
fi
|
|
if [ -z "$response" ] || printf '%s' "$response" | grep -q '"message": "Not Found"'; then
|
|
warn "Latest release query failed; trying release list..."
|
|
url="https://api.github.com/repos/${owner}/${repo}/releases"
|
|
response="$(http_get "$url" 2>/dev/null || true)"
|
|
if [ -z "$response" ] || printf '%s' "$response" | grep -q '"message": "Not Found"'; then
|
|
warn "Release list query failed; trying via gh-proxy.com mirror..."
|
|
response="$(http_get "https://gh-proxy.com/$url" 2>/dev/null || true)"
|
|
fi
|
|
fi
|
|
[ -n "$response" ] || fail "Failed to query GitHub latest release metadata for ${owner}/${repo}"
|
|
|
|
message="$(printf '%s' "$response" | install_json_ucode github-message 2>/dev/null)" ||
|
|
fail "GitHub returned an invalid latest release response for ${owner}/${repo}"
|
|
case "$message" in
|
|
*"API rate limit"*|*"rate limit exceeded"*)
|
|
fail "GitHub API rate limit reached. Try again later."
|
|
;;
|
|
"Not Found")
|
|
fail "No published latest release found for ${owner}/${repo}"
|
|
;;
|
|
esac
|
|
|
|
printf '%s' "$response"
|
|
}
|
|
|
|
resolve_tachyon_release() {
|
|
asset_ext="ipk"
|
|
|
|
[ "$PKG_IS_APK" -eq 1 ] && asset_ext="apk"
|
|
|
|
TACHYON_RELEASE_JSON="$(fetch_github_latest_release_json "$REPO_OWNER" "$REPO_NAME")"
|
|
TACHYON_RELEASE_TAG="$(printf '%s' "$TACHYON_RELEASE_JSON" | install_json_ucode release-tag 2>/dev/null)"
|
|
[ -n "$TACHYON_RELEASE_TAG" ] || fail "Failed to detect the Tachyon release tag"
|
|
|
|
TACHYON_SHA256_URL="$(printf '%s' "$TACHYON_RELEASE_JSON" | install_json_ucode release-asset-url sha256sums txt 2>/dev/null)"
|
|
[ -n "$TACHYON_SHA256_URL" ] || fail "The Tachyon release does not contain a sha256sums.txt file"
|
|
|
|
TACHYON_BACKEND_URL="$(printf '%s' "$TACHYON_RELEASE_JSON" | install_json_ucode release-asset-url backend "$asset_ext" 2>/dev/null)"
|
|
[ -n "$TACHYON_BACKEND_URL" ] || fail "The Tachyon release does not contain a tachyon .$asset_ext package"
|
|
|
|
TACHYON_APP_URL="$(printf '%s' "$TACHYON_RELEASE_JSON" | install_json_ucode release-asset-url app "$asset_ext" 2>/dev/null)"
|
|
[ -n "$TACHYON_APP_URL" ] || fail "The Tachyon release does not contain a luci-app-tachyon .$asset_ext package"
|
|
|
|
TACHYON_BACKEND_NAME="$(basename "$TACHYON_BACKEND_URL")"
|
|
TACHYON_APP_NAME="$(basename "$TACHYON_APP_URL")"
|
|
TACHYON_PACKAGE_VERSION="$(extract_package_version "$TACHYON_BACKEND_NAME")"
|
|
|
|
TACHYON_I18N_URL=""
|
|
TACHYON_I18N_NAME=""
|
|
|
|
if [ "$TACHYON_I18N_REQUESTED" -eq 1 ]; then
|
|
TACHYON_I18N_URL="$(printf '%s' "$TACHYON_RELEASE_JSON" | install_json_ucode release-asset-url i18n "$asset_ext" 2>/dev/null)"
|
|
[ -n "$TACHYON_I18N_URL" ] || fail "The Tachyon release does not contain a luci-i18n-tachyon-ru .$asset_ext package"
|
|
TACHYON_I18N_NAME="$(basename "$TACHYON_I18N_URL")"
|
|
fi
|
|
}
|
|
|
|
sing_box_is_present() {
|
|
command_exists sing-box ||
|
|
pkg_is_installed "sing-box" ||
|
|
pkg_is_installed "sing-box-tiny" ||
|
|
pkg_is_installed "sing-box-extended"
|
|
}
|
|
|
|
select_sing_box_installation() {
|
|
answer=""
|
|
default_choice=1
|
|
|
|
# Removed legacy sing-box overwrite to respect Leadaxe sing-box
|
|
|
|
if sing_box_is_present; then
|
|
SING_BOX_INSTALL_VARIANT=""
|
|
return 0
|
|
fi
|
|
|
|
if [ "$SKIP_SING_BOX" -eq 1 ]; then
|
|
SING_BOX_INSTALL_VARIANT=""
|
|
return 0
|
|
fi
|
|
|
|
if [ "$ASSUME_YES" -eq 1 ] || [ ! -t 0 ]; then
|
|
SING_BOX_INSTALL_VARIANT="stable"
|
|
msg "$(installer_text sing_box_prompt): $default_choice ($(installer_text sing_box_stable), non-interactive)"
|
|
return 0
|
|
fi
|
|
|
|
while :; do
|
|
printf '\n%s\n' "$(installer_text sing_box_prompt)"
|
|
printf ' 1) %s\n' "$(installer_text sing_box_stable)"
|
|
printf ' 2) %s\n' "$(installer_text sing_box_extended)"
|
|
printf ' 3) %s\n' "$(installer_text sing_box_extended_compressed)"
|
|
printf ' 4) %s\n' "$(installer_text sing_box_lx)"
|
|
printf ' 5) %s\n' "$(installer_text sing_box_tiny)"
|
|
printf ' 6) %s\n' "$(installer_text sing_box_skip)"
|
|
printf '%s [%s]: ' "$(installer_text select)" "$default_choice"
|
|
read -r answer || return 1
|
|
[ -n "$answer" ] || answer="$default_choice"
|
|
|
|
if [ "$answer" = "1" ]; then
|
|
SING_BOX_INSTALL_VARIANT="stable"
|
|
return 0
|
|
fi
|
|
if [ "$answer" = "2" ]; then
|
|
SING_BOX_INSTALL_VARIANT="extended"
|
|
return 0
|
|
fi
|
|
if [ "$answer" = "3" ]; then
|
|
SING_BOX_INSTALL_VARIANT="extended-compressed"
|
|
return 0
|
|
fi
|
|
if [ "$answer" = "4" ]; then
|
|
SING_BOX_INSTALL_VARIANT="lx"
|
|
return 0
|
|
fi
|
|
if [ "$answer" = "5" ]; then
|
|
SING_BOX_INSTALL_VARIANT="tiny"
|
|
return 0
|
|
fi
|
|
if [ "$answer" = "6" ]; then
|
|
SING_BOX_INSTALL_VARIANT=""
|
|
return 0
|
|
fi
|
|
|
|
warn "$(installer_text invalid_choice)"
|
|
done
|
|
}
|
|
|
|
install_selected_sing_box() {
|
|
action=""
|
|
output_file="$TMP_DIR/sing-box-component-action.json"
|
|
|
|
case "$SING_BOX_INSTALL_VARIANT" in
|
|
"")
|
|
msg "$(installer_text sing_box_skip_msg)"
|
|
return 0
|
|
;;
|
|
stable)
|
|
action="install_stable"
|
|
;;
|
|
extended)
|
|
action="install_extended"
|
|
;;
|
|
extended-compressed)
|
|
action="install_extended_compressed"
|
|
;;
|
|
lx)
|
|
action="install_lx"
|
|
;;
|
|
tiny)
|
|
action="install_tiny"
|
|
;;
|
|
*)
|
|
fail "Unknown sing-box installation variant: $SING_BOX_INSTALL_VARIANT"
|
|
;;
|
|
esac
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would install sing-box variant: $SING_BOX_INSTALL_VARIANT"
|
|
return 0
|
|
fi
|
|
|
|
[ -x /usr/bin/tachyon ] || fail "tachyon backend must be installed before sing-box component action"
|
|
for kmod in inet_diag netlink_diag tun nft_tproxy nft_nat; do
|
|
modprobe "$kmod" 2>/dev/null || true
|
|
done
|
|
[ -d /dev/net ] || mkdir -p /dev/net
|
|
[ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 2>/dev/null || true
|
|
msg "$(installer_text installing_singbox_backend)"
|
|
if ! run_logged "sing-box" /usr/bin/tachyon component_action sing_box "$action"; then
|
|
fail "Failed to install selected sing-box variant"
|
|
fi
|
|
}
|
|
|
|
get_total_ram_kb() {
|
|
awk '/MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0
|
|
}
|
|
|
|
zram_is_active() {
|
|
# Skip the header line — the old grep matched "file" inside "Filename" (the header),
|
|
# so this used to return true even when no swap was active at all.
|
|
if [ -f /proc/swaps ] && awk 'NR>1 { found=1 } END { exit !found }' /proc/swaps 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
if command_exists zramctl && zramctl 2>/dev/null | grep -q '.'; then
|
|
return 0
|
|
fi
|
|
if pkg_is_installed "zram-swap"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
decide_zram_installation() {
|
|
if [ "$ZRAM_INSTALL_OVERRIDE" = "no" ]; then
|
|
ZRAM_INSTALL_REQUESTED=0
|
|
return 0
|
|
fi
|
|
|
|
if [ "$ZRAM_INSTALL_OVERRIDE" = "yes" ]; then
|
|
ZRAM_INSTALL_REQUESTED=1
|
|
msg "$(installer_text zram_requested)"
|
|
return 0
|
|
fi
|
|
|
|
if zram_is_active; then
|
|
ZRAM_INSTALL_REQUESTED=0
|
|
return 0
|
|
fi
|
|
|
|
total_ram_kb="$(get_total_ram_kb)"
|
|
total_ram_mb=$((total_ram_kb / 1024))
|
|
|
|
if [ "$total_ram_kb" -gt 0 ] && [ "$total_ram_mb" -le 256 ]; then
|
|
warn "$(printf "$(installer_text zram_low_mem_warning)" "$total_ram_mb")"
|
|
|
|
if [ "$ASSUME_YES" -eq 1 ] || [ ! -t 0 ]; then
|
|
ZRAM_INSTALL_REQUESTED=1
|
|
msg "$(installer_text zram_auto_enable)"
|
|
return 0
|
|
fi
|
|
|
|
if confirm_prompt "$(installer_text zram_prompt)"; then
|
|
ZRAM_INSTALL_REQUESTED=1
|
|
else
|
|
ZRAM_INSTALL_REQUESTED=0
|
|
warn "$(installer_text zram_skipped)"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
install_zram_if_requested() {
|
|
[ "$ZRAM_INSTALL_REQUESTED" -eq 1 ] || return 0
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would install and enable zram-swap"
|
|
return 0
|
|
fi
|
|
|
|
msg "$(installer_text zram_installing)"
|
|
if pkg_install_name "zram-swap"; then
|
|
if [ -x /etc/init.d/zram ]; then
|
|
/etc/init.d/zram enable 2>/dev/null || true
|
|
/etc/init.d/zram start 2>/dev/null || true
|
|
fi
|
|
msg "$(installer_text zram_success)"
|
|
else
|
|
warn "Failed to install zram-swap package. Continuing installation without zram."
|
|
fi
|
|
}
|
|
|
|
backup_existing_config() {
|
|
[ "$DRY_RUN" -eq 1 ] && return 0
|
|
if [ -s /etc/config/tachyon ]; then
|
|
_ts="$(date +%Y%m%d_%H%M%S 2>/dev/null || date +%s 2>/dev/null || echo "0")"
|
|
_backup_file="/etc/config/tachyon.backup-${_ts}"
|
|
if cp -af /etc/config/tachyon "$_backup_file" 2>/dev/null; then
|
|
chmod 0600 "$_backup_file" 2>/dev/null || true
|
|
cp -af /etc/config/tachyon /etc/config/tachyon.bak 2>/dev/null || true
|
|
chmod 0600 /etc/config/tachyon.bak 2>/dev/null || true
|
|
log_line "INFO Safety backup of /etc/config/tachyon created at $_backup_file"
|
|
else
|
|
warn "Failed to create safety backup of /etc/config/tachyon"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cleanup_legacy_installation() {
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would stop/disable and remove legacy or conflicting packages"
|
|
return 0
|
|
fi
|
|
|
|
local legacy_detected_before="$TACHYON_LEGACY_DETECTED"
|
|
state_file="$TMP_DIR/install-state.env"
|
|
|
|
install_json_ucode installer-cleanup-legacy >"$state_file" ||
|
|
fail "Failed to prepare the system before Tachyon package installation"
|
|
|
|
# shellcheck disable=SC1090
|
|
. "$state_file"
|
|
|
|
if [ "$legacy_detected_before" -eq 1 ]; then
|
|
TACHYON_LEGACY_DETECTED=1
|
|
fi
|
|
}
|
|
|
|
detect_legacy_installation() {
|
|
TACHYON_LEGACY_DETECTED=0
|
|
TACHYON_FORKOP_MIGRATION=0
|
|
TACHYON_NETSHIFT_MIGRATION=0
|
|
LEGACY_CONFIG_BACKUP=""
|
|
|
|
if pkg_is_installed "forkop" || pkg_is_installed "luci-app-forkop" || \
|
|
[ -x "/etc/init.d/forkop" ] || [ -x "/usr/bin/forkop" ] || [ -d "/usr/lib/forkop" ]; then
|
|
TACHYON_LEGACY_DETECTED=1
|
|
TACHYON_FORKOP_MIGRATION=1
|
|
fi
|
|
|
|
if pkg_is_installed "netshift" || pkg_is_installed "luci-app-netshift" || \
|
|
[ -x "/etc/init.d/netshift" ] || [ -x "/usr/bin/netshift" ] || [ -d "/usr/lib/netshift" ]; then
|
|
TACHYON_LEGACY_DETECTED=1
|
|
TACHYON_NETSHIFT_MIGRATION=1
|
|
fi
|
|
|
|
for legacy_config_path in \
|
|
"/etc/config/netshift" \
|
|
"/etc/config/forkop" \
|
|
"/etc/config/forkop_plus" \
|
|
"/etc/config/$LEGACY_BACKEND_PACKAGE" \
|
|
"/etc/config/$LEGACY_CONFIG_PACKAGE_ALT" \
|
|
"/etc/config/$LEGACY_BRAND"; do
|
|
if [ -r "$legacy_config_path" ]; then
|
|
LEGACY_CONFIG_BACKUP="$TMP_DIR/legacy-config.backup"
|
|
cp "$legacy_config_path" "$LEGACY_CONFIG_BACKUP" ||
|
|
fail "Failed to back up the legacy configuration"
|
|
TACHYON_LEGACY_DETECTED=1
|
|
if [ "$legacy_config_path" = "/etc/config/netshift" ]; then
|
|
TACHYON_NETSHIFT_MIGRATION=1
|
|
elif [ "$legacy_config_path" = "/etc/config/forkop" ] || [ "$legacy_config_path" = "/etc/config/forkop_plus" ]; then
|
|
TACHYON_FORKOP_MIGRATION=1
|
|
fi
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$TACHYON_LEGACY_DETECTED" -eq 0 ]; then
|
|
if pkg_is_installed "$LEGACY_BACKEND_PACKAGE" || \
|
|
pkg_is_installed "$LEGACY_BRAND" || \
|
|
pkg_is_installed "luci-app-$LEGACY_BRAND" || \
|
|
pkg_is_installed "luci-app-$LEGACY_BACKEND_PACKAGE" || \
|
|
[ -x "/etc/init.d/$LEGACY_BACKEND_PACKAGE" ] || \
|
|
[ -x "/etc/init.d/$LEGACY_BRAND" ] || \
|
|
[ -x "/usr/bin/$LEGACY_BACKEND_PACKAGE" ] || \
|
|
[ -x "/usr/bin/$LEGACY_BRAND" ] || \
|
|
[ -d "/usr/lib/$LEGACY_BACKEND_PACKAGE" ] || \
|
|
[ -d "/usr/lib/$LEGACY_BRAND" ]; then
|
|
TACHYON_LEGACY_DETECTED=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$TACHYON_LEGACY_DETECTED" -eq 1 ]; then
|
|
msg "$(installer_text legacy_detected)"
|
|
fi
|
|
}
|
|
|
|
decide_i18n_installation() {
|
|
luci_lang="$(get_luci_main_lang)"
|
|
|
|
detect_installer_language
|
|
|
|
if pkg_is_installed "luci-i18n-tachyon-ru"; then
|
|
TACHYON_I18N_REQUESTED=1
|
|
msg "$(installer_text i18n_installed)"
|
|
return 0
|
|
fi
|
|
|
|
if [ "$TACHYON_LEGACY_DETECTED" -eq 1 ]; then
|
|
if pkg_is_installed "luci-i18n-${LEGACY_BACKEND_PACKAGE}-ru" || \
|
|
pkg_is_installed "luci-i18n-${LEGACY_BRAND}-ru" || \
|
|
pkg_is_installed "luci-i18n-forkop-ru" || \
|
|
pkg_is_installed "luci-i18n-forkop" || \
|
|
pkg_is_installed "luci-i18n-netshift-ru" || \
|
|
pkg_is_installed "luci-i18n-netshift"; then
|
|
TACHYON_I18N_REQUESTED=1
|
|
msg "$(installer_text i18n_installed)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
case "$luci_lang" in
|
|
ru|ru_*|ru-*)
|
|
TACHYON_I18N_REQUESTED=1
|
|
INSTALLER_LANG="ru"
|
|
msg "$(installer_text luci_ru)"
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
if confirm_prompt "$(installer_text i18n_prompt)"; then
|
|
TACHYON_I18N_REQUESTED=1
|
|
INSTALLER_LANG="ru"
|
|
return 0
|
|
fi
|
|
|
|
warn "$(installer_text i18n_skip)"
|
|
}
|
|
|
|
download_tachyon_packages() {
|
|
TACHYON_BACKEND_FILE="$TMP_DIR/$TACHYON_BACKEND_NAME"
|
|
TACHYON_APP_FILE="$TMP_DIR/$TACHYON_APP_NAME"
|
|
TACHYON_I18N_FILE=""
|
|
TACHYON_SHA256_FILE="$TMP_DIR/sha256sums.txt"
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would download and verify: $TACHYON_BACKEND_NAME, $TACHYON_APP_NAME${TACHYON_I18N_REQUESTED:+$([ "$TACHYON_I18N_REQUESTED" -eq 1 ] && printf ', luci-i18n-tachyon-ru package')}"
|
|
return 0
|
|
fi
|
|
|
|
local _total=3
|
|
[ -n "$TACHYON_I18N_URL" ] && _total=4
|
|
|
|
download_with_retry "$TACHYON_SHA256_URL" "$TACHYON_SHA256_FILE" "sha256sums.txt" 1 "$_total" || fail "Failed to download sha256sums.txt"
|
|
|
|
download_with_retry "$TACHYON_BACKEND_URL" "$TACHYON_BACKEND_FILE" "$TACHYON_BACKEND_NAME" 2 "$_total" || fail "Failed to download $TACHYON_BACKEND_NAME"
|
|
download_with_retry "$TACHYON_APP_URL" "$TACHYON_APP_FILE" "$TACHYON_APP_NAME" 3 "$_total" || fail "Failed to download $TACHYON_APP_NAME"
|
|
|
|
if [ -n "$TACHYON_I18N_URL" ]; then
|
|
TACHYON_I18N_FILE="$TMP_DIR/$TACHYON_I18N_NAME"
|
|
download_with_retry "$TACHYON_I18N_URL" "$TACHYON_I18N_FILE" "$TACHYON_I18N_NAME" 4 "$_total" || fail "Failed to download $TACHYON_I18N_NAME"
|
|
fi
|
|
|
|
msg "$(installer_text verifying_checksums)"
|
|
(
|
|
cd "$TMP_DIR" || fail "Failed to change directory to temporary path"
|
|
local pattern
|
|
pattern="$(basename "$TACHYON_BACKEND_FILE")|$(basename "$TACHYON_APP_FILE")"
|
|
if [ -n "$TACHYON_I18N_FILE" ]; then
|
|
pattern="$pattern|$(basename "$TACHYON_I18N_FILE")"
|
|
fi
|
|
grep -E "$pattern" sha256sums.txt | sha256sum -c - || fail "Checksum verification failed! The downloaded packages may be corrupted."
|
|
)
|
|
msg "$(installer_text checksums_ok)"
|
|
}
|
|
|
|
install_backend_package() {
|
|
sanitize_apk_world
|
|
msg "$(installer_text kmod_check)"
|
|
for kmod in kmod-inet-diag kmod-netlink-diag kmod-tun kmod-nft-tproxy kmod-nft-nat; do
|
|
if ! pkg_is_installed "$kmod"; then
|
|
pkg_install_name "$kmod" || warn "Could not install $kmod (this is normal if built-in or using custom firmware)"
|
|
fi
|
|
done
|
|
|
|
for kmod in inet_diag netlink_diag tun nft_tproxy nft_nat; do
|
|
modprobe "$kmod" 2>/dev/null || true
|
|
done
|
|
[ -d /dev/net ] || mkdir -p /dev/net
|
|
[ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 2>/dev/null || true
|
|
|
|
# Protect user configuration from being overwritten by apk.
|
|
# apk add --allow-untrusted may not respect conffiles_static on all
|
|
# OpenWrt builds, silently replacing /etc/config/tachyon with defaults.
|
|
local cfg_backup="/etc/.tachyon/.cfg-backup.pre-install"
|
|
local cfg_hash_before=""
|
|
mkdir -p /etc/.tachyon 2>/dev/null || true
|
|
if [ -r /etc/config/tachyon ]; then
|
|
cp -a /etc/config/tachyon "$cfg_backup" 2>/dev/null ||
|
|
warn "Failed to back up Tachyon configuration before package install"
|
|
local _sha_out
|
|
_sha_out="$(sha256sum /etc/config/tachyon 2>/dev/null)"
|
|
cfg_hash_before="${_sha_out%% *}"
|
|
fi
|
|
|
|
local backend_sha_before backend_present_before=0
|
|
backend_sha_before="$(installed_backend_sha)"
|
|
pkg_is_installed "tachyon" && backend_present_before=1
|
|
|
|
if ! pkg_install_files "$TACHYON_BACKEND_FILE"; then
|
|
# "tachyon is installed" is not evidence of success on a rebuilt tag: apk
|
|
# rolls the transaction back to the identical old version, so both the
|
|
# package name and its version still check out. Only a moved commit SHA
|
|
# proves new files landed.
|
|
#
|
|
# An empty backend_sha_before does NOT mean "fresh install". installed_backend_sha
|
|
# rejects the unsubstituted __COMPILED_COMMIT_SHA__ placeholder and the literal
|
|
# "unknown", and a stale build on disk is exactly where both show up — so the
|
|
# upgrade this check exists to catch used to land in the presence branch and
|
|
# print a success warning over a rolled-back transaction. Presence before the
|
|
# install is what distinguishes an upgrade from a first install.
|
|
local backend_sha_after
|
|
backend_sha_after="$(installed_backend_sha)"
|
|
if [ -n "$backend_sha_before" ] && [ "$backend_sha_before" = "$backend_sha_after" ]; then
|
|
fail "tachyon installation failed: still running build $backend_sha_before, the package manager rolled the upgrade back (see $LOG_FILE)"
|
|
elif [ "$backend_present_before" = 1 ] && [ -z "$backend_sha_after" ]; then
|
|
fail "tachyon installation failed: the installed build still reports no commit SHA, so the package manager rolled the upgrade back (see $LOG_FILE)"
|
|
elif pkg_is_installed "tachyon"; then
|
|
warn "Package manager reported errors during installation, but tachyon is installed — continuing"
|
|
else
|
|
fail "tachyon installation failed"
|
|
fi
|
|
fi
|
|
|
|
# Restore if apk overwrote the config with defaults
|
|
if [ -n "$cfg_hash_before" ] && [ -r /etc/config/tachyon ] && [ -r "$cfg_backup" ] && [ -s "$cfg_backup" ]; then
|
|
local cfg_hash_after _sha_out2
|
|
_sha_out2="$(sha256sum /etc/config/tachyon 2>/dev/null)"
|
|
cfg_hash_after="${_sha_out2%% *}"
|
|
if [ "$cfg_hash_before" != "$cfg_hash_after" ]; then
|
|
warn "Package manager overwrote /etc/config/tachyon; restoring user configuration"
|
|
cp -a "$cfg_backup" /etc/config/tachyon
|
|
chmod 0600 /etc/config/tachyon
|
|
fi
|
|
fi
|
|
rm -f "$cfg_backup" 2>/dev/null
|
|
}
|
|
|
|
migrate_legacy_configuration() {
|
|
[ "$TACHYON_LEGACY_DETECTED" -eq 1 ] || return 0
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would migrate the legacy configuration to Tachyon"
|
|
return 0
|
|
fi
|
|
|
|
if [ -z "$LEGACY_CONFIG_BACKUP" ] || [ ! -f "$LEGACY_CONFIG_BACKUP" ]; then
|
|
for cand in \
|
|
"/etc/config/netshift" \
|
|
"/etc/config/forkop" \
|
|
"/etc/config/forkop_plus" \
|
|
"/etc/config/$LEGACY_BACKEND_PACKAGE" \
|
|
"/etc/config/$LEGACY_CONFIG_PACKAGE_ALT" \
|
|
"/etc/config/$LEGACY_BRAND"; do
|
|
if [ -s "$cand" ]; then
|
|
LEGACY_CONFIG_BACKUP="$cand"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -n "$LEGACY_CONFIG_BACKUP" ] && [ -f "$LEGACY_CONFIG_BACKUP" ]; then
|
|
cp "$LEGACY_CONFIG_BACKUP" /etc/config/tachyon ||
|
|
fail "Failed to restore the legacy configuration for migration"
|
|
chmod 0644 /etc/config/tachyon ||
|
|
fail "Failed to set permissions on the Tachyon configuration"
|
|
|
|
msg "$(installer_text migrating_config_detail)"
|
|
local migration_mode="migrate-podkop"
|
|
|
|
if ! TACHYON_CONFIG_NAME="tachyon" \
|
|
TACHYON_LIB="/usr/lib/tachyon" \
|
|
run_logged "migration" ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc "$migration_mode"; then
|
|
cp "$LEGACY_CONFIG_BACKUP" /etc/config/tachyon 2>/dev/null || true
|
|
fail "Legacy configuration migration failed; the original configuration was restored"
|
|
fi
|
|
else
|
|
warn "The legacy package had no readable configuration; Tachyon defaults will be used"
|
|
fi
|
|
|
|
install_json_ucode installer-finalize-legacy ||
|
|
fail "Failed to remove legacy configuration and cache files after migration"
|
|
}
|
|
|
|
install_ui_packages() {
|
|
if ! pkg_install_files "$TACHYON_APP_FILE"; then
|
|
if pkg_is_installed "luci-app-tachyon" && [ -f "/usr/share/luci/menu.d/luci-app-tachyon.json" ]; then
|
|
warn "Package manager reported non-critical errors during luci-app-tachyon installation, but files are verified"
|
|
else
|
|
fail "luci-app-tachyon installation failed"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -f "/usr/share/luci/menu.d/luci-app-tachyon.json" ]; then
|
|
fail "luci-app-tachyon installation failed: menu.d/luci-app-tachyon.json missing"
|
|
fi
|
|
|
|
if [ -n "$TACHYON_I18N_FILE" ]; then
|
|
if ! pkg_install_files "$TACHYON_I18N_FILE"; then
|
|
if pkg_is_installed "luci-i18n-tachyon-ru"; then
|
|
warn "Package manager reported errors during luci-i18n-tachyon-ru installation, but the package is installed — continuing"
|
|
else
|
|
fail "luci-i18n-tachyon-ru installation failed"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
post_install() {
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "[dry-run] would run post-install cleanup and (re)start the Tachyon service"
|
|
return 0
|
|
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"
|
|
|
|
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /var/luci-modulecache* /tmp/luci-modulecache* 2>/dev/null || true
|
|
if [ -x "/etc/init.d/rpcd" ]; then
|
|
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
|
|
fi
|
|
if [ -x "/etc/init.d/uhttpd" ]; then
|
|
/etc/init.d/uhttpd restart >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
TOTAL_STEPS=10
|
|
|
|
main() {
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
parse_args "$@"
|
|
START_TIME="$(date +%s 2>/dev/null || echo 0)"
|
|
: >"$LOG_FILE" 2>/dev/null || true
|
|
log_line "Tachyon installer v${INSTALLER_VERSION} starting (args: $*)"
|
|
|
|
acquire_lock
|
|
check_root
|
|
init_tmp_dir
|
|
detect_fetcher
|
|
detect_installer_language
|
|
check_system
|
|
backup_existing_config
|
|
|
|
# Print TUI banner
|
|
if [ "$QUIET" -eq 0 ] && command -v tui_banner >/dev/null 2>&1; then
|
|
tui_banner
|
|
fi
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
warn "$(installer_text dry_run_warn)"
|
|
fi
|
|
|
|
step 1 "$TOTAL_STEPS" "$(installer_text pkg_list_update)"
|
|
pkg_list_update || fail "Failed to update package lists"
|
|
ensure_bootstrap_ucode_runtime
|
|
|
|
sync_time
|
|
detect_legacy_installation
|
|
decide_i18n_installation
|
|
select_sing_box_installation
|
|
decide_zram_installation
|
|
|
|
msg "$(installer_text install_start)"
|
|
|
|
step 2 "$TOTAL_STEPS" "$(installer_text resolving_release)"
|
|
resolve_tachyon_release
|
|
debug "Resolved release tag: $TACHYON_RELEASE_TAG"
|
|
|
|
step 3 "$TOTAL_STEPS" "$(installer_text downloading_packages)"
|
|
download_tachyon_packages
|
|
|
|
install_zram_if_requested
|
|
|
|
step 4 "$TOTAL_STEPS" "$(installer_text cleaning_legacy)"
|
|
cleanup_legacy_installation
|
|
|
|
step 5 "$TOTAL_STEPS" "$(installer_text installing_backend)"
|
|
install_backend_package
|
|
|
|
step 6 "$TOTAL_STEPS" "$(installer_text migrating_config)"
|
|
migrate_legacy_configuration
|
|
|
|
step 7 "$TOTAL_STEPS" "$(installer_text installing_ui)"
|
|
install_ui_packages
|
|
|
|
step 8 "$TOTAL_STEPS" "$(installer_text installing_singbox)"
|
|
install_selected_sing_box
|
|
|
|
step 9 "$TOTAL_STEPS" "$(installer_text running_postinstall)"
|
|
post_install
|
|
|
|
step 10 "$TOTAL_STEPS" "$(installer_text step_done)"
|
|
print_summary
|
|
}
|
|
|
|
print_summary() {
|
|
end_time="$(date +%s 2>/dev/null || echo 0)"
|
|
elapsed=$((end_time - START_TIME))
|
|
[ "$elapsed" -ge 0 ] 2>/dev/null || elapsed=0
|
|
|
|
local zram_stat="not active"
|
|
if [ -f /proc/swaps ] && grep -E -q '/dev/zram|partition|file' /proc/swaps 2>/dev/null; then
|
|
zram_stat="active (zram-swap)"
|
|
elif pkg_is_installed "zram-swap"; then
|
|
zram_stat="installed"
|
|
fi
|
|
|
|
if command -v tui_box_start >/dev/null 2>&1; then
|
|
tui_box_start
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
tui_box_line "$(printf "$(installer_text summary_dry_run)" "$elapsed")"
|
|
else
|
|
tui_box_line_color "$(printf "$(installer_text summary_success)" "$TACHYON_PACKAGE_VERSION" "$elapsed")" "$_c_green"
|
|
fi
|
|
tui_box_line "$(installer_text summary_release): ${REPO_OWNER}/${REPO_NAME}@${TACHYON_RELEASE_TAG}"
|
|
[ "$TACHYON_LEGACY_DETECTED" -eq 1 ] && tui_box_line "$(installer_text summary_legacy_removed)"
|
|
tui_box_line "$(installer_text zram_status): $zram_stat"
|
|
tui_box_line "$(installer_text summary_log): $LOG_FILE"
|
|
if command -v tui_box_divider >/dev/null 2>&1; then
|
|
tui_box_divider
|
|
else
|
|
tui_divider
|
|
fi
|
|
tui_box_line_color "$(installer_text summary_luci_notice)" "$_c_yellow"
|
|
tui_box_end
|
|
else
|
|
printf '\n'
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
msg "$(printf "$(installer_text summary_dry_run)" "$elapsed")"
|
|
else
|
|
msg "$(printf "$(installer_text summary_success)" "$TACHYON_PACKAGE_VERSION" "$elapsed")"
|
|
fi
|
|
msg "$(installer_text summary_release): ${REPO_OWNER}/${REPO_NAME}@${TACHYON_RELEASE_TAG}"
|
|
[ "$TACHYON_LEGACY_DETECTED" -eq 1 ] && msg "$(installer_text summary_legacy_removed)"
|
|
msg "$(installer_text zram_status): $zram_stat"
|
|
msg "$(installer_text summary_log): $LOG_FILE"
|
|
warn "$(installer_text summary_luci_notice)"
|
|
fi
|
|
}
|
|
|
|
main "$@" |