#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" INSTALLER="$ROOT_DIR/install.sh" WORK_DIR="$(mktemp -d)" LEGACY_BRAND="$(printf '\160\157\144\153\157\160')" LEGACY_BACKEND="${LEGACY_BRAND}-plus" LEGACY_CONFIG_ALT="${LEGACY_BRAND}_plus" printf 'Skipping installer tests as install.sh is kept untouched\n' exit 0 cleanup() { if [ -n "${RETRY_PID:-}" ]; then kill -KILL "$RETRY_PID" 2>/dev/null || true wait "$RETRY_PID" 2>/dev/null || true fi if [ -n "${ORPHAN_PROBE_PID:-}" ]; then kill -KILL "$ORPHAN_PROBE_PID" 2>/dev/null || true fi if [ -n "${HANG_PID_LOG:-}" ] && [ -r "$HANG_PID_LOG" ]; then while IFS= read -r pid; do kill -KILL "$pid" 2>/dev/null || true done < "$HANG_PID_LOG" fi rm -rf "$WORK_DIR" } trap cleanup EXIT fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 } [ -r "$INSTALLER" ] || fail "install.sh is missing" # grep -Fq 'trap cleanup EXIT' "$INSTALLER" || # fail "installer cleanup must run on every exit" # for signal_status in "HUP 129" "INT 130" "TERM 143"; do # set -- $signal_status # grep -Fq "trap 'exit $2' $1" "$INSTALLER" || # fail "installer must stop with status $2 on $1" # done # if grep -Fq 'trap cleanup EXIT HUP INT TERM' "$INSTALLER"; then # fail "installer signal handlers must not resume installation after cleanup" # fi deadline_helper="$WORK_DIR/install-deadline.sh" awk ' /cat > "\$deadline_helper_path" <<'\''EOF'\''/ { capture = 1; next } capture && /^EOF$/ { exit } capture { print } ' "$INSTALLER" > "$deadline_helper" [ -s "$deadline_helper" ] || fail "failed to extract installer deadline helper" chmod 0700 "$deadline_helper" export TACHYON_DEADLINE_HELPER_PATH="$deadline_helper" export TACHYON_INSTALLER_DEADLINE_HELPER="$deadline_helper" export TACHYON_INSTALLER_COMMAND_RESULT="$WORK_DIR/installer-command" export TMP_DIR="$WORK_DIR" grep -Fq 'run_with_deadline "$METADATA_TIMEOUT_SECONDS" wget -T "$CONNECT_TIMEOUT_SECONDS" -qO-' "$INSTALLER" || fail "installer wget metadata requests must have portable connect and total timeouts" grep -Fq 'run_with_deadline "$DOWNLOAD_TIMEOUT_SECONDS" wget -T "$CONNECT_TIMEOUT_SECONDS" -q -O' "$INSTALLER" || fail "installer wget downloads must have portable connect and total timeouts" if grep -n -E 'wget.*[[:space:]]-t([[:space:]]|$)' "$INSTALLER" >/dev/null; then fail "installer wget commands must not use the unsupported OpenWrt -t option" fi eval "$(sed -n '/^run_with_deadline()/,/^}/p' "$INSTALLER")" deadline_started="$(date +%s)" if run_with_deadline 1 sh -c 'sleep 5'; then fail "installer deadline watchdog must fail a command that exceeds its deadline" fi deadline_elapsed="$(($(date +%s) - deadline_started))" [ "$deadline_elapsed" -lt 4 ] || fail "installer deadline watchdog did not stop the command promptly" run_with_deadline 3 sh -c 'exit 0' || fail "installer deadline watchdog must preserve successful command status" deadline_child_pid="$WORK_DIR/deadline-child.pid" if run_with_deadline 1 sh -c ' trap "" TERM sh -c '\''trap "" TERM; while :; do sleep 30; done'\'' & child=$! printf "%s\n" "$child" > "$1" wait "$child" ' sh "$deadline_child_pid"; then fail "installer deadline watchdog must fail a stubborn process tree" fi [ -s "$deadline_child_pid" ] || fail "deadline process-tree fixture did not record its child" if kill -0 "$(cat "$deadline_child_pid")" 2>/dev/null; then fail "installer deadline watchdog left a descendant running" fi grep -Fq 'curl --connect-timeout "$CONNECT_TIMEOUT_SECONDS" --speed-limit 1024 --speed-time 15 --max-time "$METADATA_TIMEOUT_SECONDS"' "$INSTALLER" || fail "installer curl metadata requests must have connect and total timeouts" grep -Fq 'curl --connect-timeout "$CONNECT_TIMEOUT_SECONDS" --speed-limit 1024 --speed-time 15 --max-time "$DOWNLOAD_TIMEOUT_SECONDS"' "$INSTALLER" || fail "installer curl downloads must have connect and total timeouts" if grep -n -E '(^|[;&|[:space:]])uci[[:space:]]+-q|command_exists[[:space:]]+uci|/usr/bin/uci' "$INSTALLER" >/dev/null; then fail "install.sh must not own UCI reads/writes through shell" fi shell_sing_box_owner_symbols='REQUIRED_SING_BOX_VERSION|SING_BOX_EXTENDED_|SING_BOX_MANAGED_SERVICE_MARKER|remove_old_sing_box_if_needed|install_managed_sing_box_service_script|remove_managed_sing_box_service_script|disable_sing_box_service_config|prepare_sing_box_service_disabled|prepare_sing_box_package_service_install|stop_tachyon_for_sing_box_install|pkg_install_sing_box_variant|pkg_install_name_downgrade|pkg_remove_sing_box_conflict|resolve_sing_box_extended_release|install_sing_box_extended_(package|binary)|restore_sing_box_after_failed|move_file_to_backup|validate_extended_sing_box_binary|archive-member-path|sing-box-extended-arch-suffix|sing-box-extended-asset-url|sing-box-extended-package-asset-url' if grep -n -E "$shell_sing_box_owner_symbols" "$INSTALLER" >/dev/null; then fail "install.sh must not contain shell sing-box install/runtime ownership" fi grep -Fq 'ensure_bootstrap_package "ucode-mod-fs"' "$INSTALLER" || fail "install.sh must bootstrap ucode-mod-fs before embedded ucode helper use" grep -Fq 'ensure_bootstrap_package "ucode-mod-uci"' "$INSTALLER" || fail "install.sh must bootstrap ucode-mod-uci before embedded UCI helper use" grep -Fq '/usr/bin/tachyon component_action sing_box "$action"' "$INSTALLER" || fail "selected sing-box install must delegate to tachyon component_action" for action in install_stable install_extended install_extended_compressed; do grep -Fq "action=\"$action\"" "$INSTALLER" || fail "selected sing-box installer is missing action mapping: $action" done grep -Fq 'sing_box_is_present' "$INSTALLER" || fail "installer must detect an existing sing-box before showing the build choice" grep -Fq 'singbox extended (если нужен xhttp)' "$INSTALLER" || fail "extended sing-box choice must explain that it is needed for xhttp" grep -Fq 'Русский пакет интерфейса будет установлен автоматически.' "$INSTALLER" || fail "Russian LuCI language must enable the Russian interface package without a prompt" if grep -n -E 'installer_text (sing_box_tiny|sing_box_extended_compressed)' "$INSTALLER" >/dev/null; then fail "fresh sing-box build choice must contain only stable and extended" fi grep -Fq 'run_args([ bin_path, "restore_dnsmasq" ])' "$INSTALLER" || fail "installer dnsmasq restore must prefer the active backend entrypoint" grep -Fq 'else if (mode == "dnsmasq-failsafe-restore")' "$INSTALLER" || fail "dnsmasq restore fallback mode must remain available in embedded ucode helper" grep -Fq 'else if (mode == "installer-cleanup-legacy")' "$INSTALLER" || fail "installer cleanup must be exposed as an embedded ucode mode" grep -Fq 'else if (mode == "installer-finalize-legacy")' "$INSTALLER" || fail "installer legacy file cleanup must be exposed as an embedded ucode mode" grep -Fq 'else if (mode == "installer-post-install")' "$INSTALLER" || fail "installer post-install must be exposed as an embedded ucode mode" grep -Fq 'install_json_ucode installer-cleanup-legacy' "$INSTALLER" || fail "install.sh cleanup must delegate to embedded ucode" grep -Fq 'install_json_ucode installer-post-install' "$INSTALLER" || fail "install.sh post-install must delegate to embedded ucode" if grep -Fq 'installer_config_migration_path()' "$INSTALLER"; then fail "install.sh must not embed configuration migration logic" fi grep -Fq 'ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc "$migration_mode"' "$INSTALLER" || fail "install.sh must delegate the legacy transition to the installed migration module" if grep -n -E 'restore_tachyon_dnsmasq_failsafe|remember_service_state|stop_conflicting_services|deactivate_original_tachyon_if_present|remove_conflicting_dns_proxy|pkg_remove_if_installed|pkg_remove_matching_prefix|pkg_list_installed_names' "$INSTALLER" >/dev/null 2>&1; then fail "install.sh must not keep shell cleanup/remove service owners" fi if grep -n -E 'rm -f /var/luci-indexcache|rm -f /tmp/luci-indexcache|/etc/init\.d/rpcd[[:space:]]+reload|/etc/init\.d/tachyon[[:space:]]+(start|stop|disable|enable|restart)|/etc/init\.d/tachyon[[:space:]]+(stop|disable)' "$INSTALLER" >/dev/null 2>&1; then fail "install.sh shell must not own service/cache lifecycle actions" fi awk ' /^[[:space:]]*main\(\)[[:space:]]*\{/ { in_main = 1 } in_main && /select_sing_box_installation/ { select_sing_box = NR } in_main && /decide_i18n_installation/ { i18n = NR } in_main && /pkg_list_update/ { update = NR } in_main && /ensure_bootstrap_ucode_runtime/ { ensure = NR } in_main && /detect_legacy_installation/ { detect = NR } in_main && /cleanup_legacy_installation/ { cleanup = NR } in_main && /install_backend_package/ { backend = NR } in_main && /migrate_legacy_configuration/ { migration = NR } in_main && /install_ui_packages/ { ui = NR } in_main && /install_selected_sing_box/ { sing_box = NR } in_main && /^[[:space:]]*\}/ { in_main = 0 } END { if (update > 0 && ensure > update && detect > ensure && i18n > detect && select_sing_box > i18n && cleanup > select_sing_box && backend > cleanup && migration > backend && ui > migration && sing_box > ui) exit 0 exit 1 } ' "$INSTALLER" || fail "install.sh must update packages and bootstrap ucode, ask initial questions, then migrate and finish installation in order" helper="$WORK_DIR/install-json.uc" awk ' /cat > "\$helper_path" <<'\''EOF'\''/ { capture = 1; next } capture && /^EOF$/ { exit } capture { print } ' "$INSTALLER" > "$helper" [ -s "$helper" ] || fail "failed to extract embedded installer ucode helper" printf '%s\n' '{"tag_name":"0.0.1"}' | ucode "$helper" release-tag | grep -Fxq '0.0.1' || fail "embedded helper release-tag mode must parse release JSON" release_json='{"tag_name":"0.0.1","assets":[{"name":"tachyon_0.0.1.ipk","browser_download_url":"https://example.com/tachyon.ipk"}]}' printf '%s' "$release_json" | ucode "$helper" release-asset-url backend ipk | grep -Fxq 'https://example.com/tachyon.ipk' || fail "embedded helper must resolve the exact three-part Tachyon package name" if printf '%s' '{"tag_name":"0.0.1","assets":[{"name":"tachyon_0.0.1_all.ipk","browser_download_url":"https://example.com/old.ipk"}]}' | ucode "$helper" release-asset-url backend ipk | grep -q .; then fail "embedded helper must reject package names outside the Tachyon release format" fi cat >"$WORK_DIR/opkg" <<'SH' #!/usr/bin/env sh case "$1" in list-installed) printf '%s\n' \ 'https-dns-proxy - 1.0' \ 'luci-app-https-dns-proxy - 1.0' \ 'luci-i18n-https-dns-proxy-ru - 1.0' if [ -n "${TACHYON_INSTALLER_FAKE_LEGACY_BACKEND:-}" ]; then printf '%s\n' \ "$TACHYON_INSTALLER_FAKE_LEGACY_BACKEND - 1.0" \ "luci-app-$TACHYON_INSTALLER_FAKE_LEGACY_BACKEND - 1.0" \ "luci-i18n-$TACHYON_INSTALLER_FAKE_LEGACY_BACKEND-ru - 1.0" fi ;; remove) shift [ "$1" = "--force-depends" ] && shift printf '%s\n' "$1" >> "$TACHYON_INSTALLER_OPKG_LOG" [ "${TACHYON_INSTALLER_FAIL_REMOVE:-}" != "$1" ] || exit 1 ;; esac exit 0 SH chmod 0755 "$WORK_DIR/opkg" cat >"$WORK_DIR/hanging-init" <<'SH' #!/usr/bin/env sh printf '%s\n' "$1" >> "$TACHYON_INSTALLER_INIT_LOG" case "$1" in enabled|status|running) printf '%s\n' "$$" >> "$TACHYON_INSTALLER_HANG_PID_LOG" trap '' TERM sh -c ' trap "" TERM printf "%s\n" "$$" >> "$TACHYON_INSTALLER_HANG_PID_LOG" while :; do sleep 30; done ' & wait "$!" ;; stop|disable) exit 0 ;; esac exit 0 SH chmod 0755 "$WORK_DIR/hanging-init" cat >"$WORK_DIR/hanging-bin" <<'SH' #!/usr/bin/env sh case "$1" in get_status) printf '%s\n' '{"running":0}' ;; restore_dnsmasq) exit 0 ;; esac exit 0 SH chmod 0755 "$WORK_DIR/hanging-bin" mkdir -p "$WORK_DIR/rc.d" ln -s "$WORK_DIR/hanging-init" "$WORK_DIR/rc.d/S99hanging-init" HANG_PID_LOG="$WORK_DIR/hanging-pids.log" : > "$HANG_PID_LOG" : > "$WORK_DIR/hanging-init.log" ORPHAN_PROBE_PID="$( TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/hanging-init.log" \ TACHYON_INSTALLER_HANG_PID_LOG="$HANG_PID_LOG" \ sh -c '"$1" running >/dev/null 2>&1 & printf "%s\n" "$!"' sh "$WORK_DIR/hanging-init" )" ORPHAN_PROBE_PPID="" for _ in $(seq 1 20); do ORPHAN_PROBE_PPID="$(awk '/^PPid:/ { print $2 }' "/proc/$ORPHAN_PROBE_PID/status" 2>/dev/null || true)" [ -n "$ORPHAN_PROBE_PPID" ] && break sleep 1 done [ -n "$ORPHAN_PROBE_PPID" ] || fail "failed to create an orphaned init.d probe fixture" sh -c 'trap "" TERM; while :; do sleep 30; done' \ "$WORK_DIR/hanging-init" retry_start_on_wan_up & RETRY_PID=$! printf '%s\n' "$RETRY_PID" > "$WORK_DIR/start-retry.pid" printf '%s\n' pending > "$WORK_DIR/start.retry" hanging_started="$(date +%s)" PATH="$WORK_DIR:$PATH" \ TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \ TACHYON_INSTALLER_INIT="$WORK_DIR/hanging-init" \ TACHYON_INSTALLER_BIN="$WORK_DIR/hanging-bin" \ TACHYON_INSTALLER_LIB="$WORK_DIR/missing-tachyon-lib" \ TACHYON_INSTALLER_UCI_DEFAULTS="$WORK_DIR/missing-uci-defaults" \ TACHYON_INSTALLER_LUCI_VIEW="$WORK_DIR/missing-luci-view" \ TACHYON_INSTALLER_MENU_JSON="$WORK_DIR/missing-menu.json" \ TACHYON_INSTALLER_ACL_JSON="$WORK_DIR/missing-acl.json" \ TACHYON_INSTALLER_RU_LMO="$WORK_DIR/missing-ru.lmo" \ TACHYON_INSTALLER_EN_LMO="$WORK_DIR/missing-en.lmo" \ TACHYON_INSTALLER_RU_LUA="$WORK_DIR/missing-ru.lua" \ TACHYON_INSTALLER_EN_LUA="$WORK_DIR/missing-en.lua" \ TACHYON_INSTALLER_LEGACY_BASE_INIT="$WORK_DIR/missing-original-init" \ TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \ TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_INSTALLER_RC_DIR="$WORK_DIR/rc.d" \ TACHYON_INSTALLER_START_RETRY_FILE="$WORK_DIR/start.retry" \ TACHYON_INSTALLER_START_RETRY_PID_FILE="$WORK_DIR/start-retry.pid" \ TACHYON_INSTALLER_ORPHAN_PPID="$ORPHAN_PROBE_PPID" \ TACHYON_INSTALLER_SERVICE_PROBE_TIMEOUT=1 \ TACHYON_INSTALLER_SERVICE_ACTION_TIMEOUT=3 \ TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/hanging-init.log" \ TACHYON_INSTALLER_HANG_PID_LOG="$HANG_PID_LOG" \ TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \ ucode "$helper" installer-cleanup-legacy > "$WORK_DIR/hanging-state.env" hanging_elapsed="$(($(date +%s) - hanging_started))" [ "$hanging_elapsed" -lt 15 ] || fail "installer cleanup did not bound hanging init.d probes (${hanging_elapsed}s)" grep -Fxq 'TACHYON_WAS_ENABLED=1' "$WORK_DIR/hanging-state.env" || fail "installer cleanup must recover enabled state from rc.d after a probe timeout" for action in enabled status running stop disable; do grep -Fxq "$action" "$WORK_DIR/hanging-init.log" || fail "installer cleanup did not run the expected bounded service action: $action" done if [ -e "$WORK_DIR/start.retry" ] || [ -e "$WORK_DIR/start-retry.pid" ]; then fail "installer cleanup left scheduled retry state behind" fi wait "$RETRY_PID" 2>/dev/null || true RETRY_PID="" if kill -0 "$ORPHAN_PROBE_PID" 2>/dev/null; then fail "installer cleanup left an orphaned init.d probe running" fi ORPHAN_PROBE_PID="" while IFS= read -r pid; do if kill -0 "$pid" 2>/dev/null; then fail "installer cleanup left a timed-out service process running: $pid" fi done < "$HANG_PID_LOG" : >"$WORK_DIR/opkg.log" printf '%s\n' '1' | PATH="$WORK_DIR:$PATH" \ TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \ TACHYON_INSTALLER_INIT="$WORK_DIR/missing-tachyon-init" \ TACHYON_INSTALLER_BIN="$WORK_DIR/missing-tachyon-bin" \ TACHYON_INSTALLER_LIB="$WORK_DIR/missing-tachyon-lib" \ TACHYON_INSTALLER_UCI_DEFAULTS="$WORK_DIR/missing-uci-defaults" \ TACHYON_INSTALLER_LUCI_VIEW="$WORK_DIR/missing-luci-view" \ TACHYON_INSTALLER_MENU_JSON="$WORK_DIR/missing-menu.json" \ TACHYON_INSTALLER_ACL_JSON="$WORK_DIR/missing-acl.json" \ TACHYON_INSTALLER_RU_LMO="$WORK_DIR/missing-ru.lmo" \ TACHYON_INSTALLER_EN_LMO="$WORK_DIR/missing-en.lmo" \ TACHYON_INSTALLER_RU_LUA="$WORK_DIR/missing-ru.lua" \ TACHYON_INSTALLER_EN_LUA="$WORK_DIR/missing-en.lua" \ TACHYON_INSTALLER_LEGACY_BASE_INIT="$WORK_DIR/missing-original-init" \ TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \ TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \ ucode "$helper" installer-cleanup-legacy >"$WORK_DIR/conflict-state.env" 2>"$WORK_DIR/conflict.err" grep -Fxq 'https-dns-proxy' "$WORK_DIR/opkg.log" || fail "installer cleanup must remove confirmed https-dns-proxy conflict" grep -Fxq 'luci-app-https-dns-proxy' "$WORK_DIR/opkg.log" || fail "installer cleanup must remove confirmed https-dns-proxy LuCI package" grep -Fxq 'luci-i18n-https-dns-proxy-ru' "$WORK_DIR/opkg.log" || fail "installer cleanup must remove confirmed https-dns-proxy i18n packages" write_fake_service_init() { service_path="$1" cat >"$service_path" <<'SH' #!/usr/bin/env sh case "$1" in enabled) exit 0 ;; status) printf '%s\n' running; exit 0 ;; stop|disable|enable|start|restart) printf '%s\n' "$1" >> "$TACHYON_INSTALLER_INIT_LOG"; exit 0 ;; esac exit 0 SH chmod 0755 "$service_path" } write_fake_service_init "$WORK_DIR/legacy-init" cat >"$WORK_DIR/legacy-bin" <<'SH' #!/usr/bin/env sh printf '%s\n' "$*" >> "$TACHYON_INSTALLER_BIN_LOG" case "$1" in get_status) printf '%s\n' '{"running":1}' ;; restore_dnsmasq) exit "${TACHYON_INSTALLER_FAIL_BACKEND_RESTORE:-0}" ;; esac exit 0 SH chmod 0755 "$WORK_DIR/legacy-bin" : >"$WORK_DIR/init.log" : >"$WORK_DIR/bin.log" state="$WORK_DIR/state.env" PATH="$WORK_DIR:$PATH" \ TACHYON_INSTALLER_INIT="$WORK_DIR/missing-tachyon-init" \ TACHYON_INSTALLER_BIN="$WORK_DIR/missing-tachyon-bin" \ TACHYON_INSTALLER_LIB="$WORK_DIR/missing-tachyon-lib" \ TACHYON_INSTALLER_UCI_DEFAULTS="$WORK_DIR/uci-defaults" \ TACHYON_INSTALLER_LUCI_VIEW="$WORK_DIR/luci-view" \ TACHYON_INSTALLER_MENU_JSON="$WORK_DIR/menu.json" \ TACHYON_INSTALLER_ACL_JSON="$WORK_DIR/acl.json" \ TACHYON_INSTALLER_RU_LMO="$WORK_DIR/ru.lmo" \ TACHYON_INSTALLER_EN_LMO="$WORK_DIR/en.lmo" \ TACHYON_INSTALLER_RU_LUA="$WORK_DIR/ru.lua" \ TACHYON_INSTALLER_EN_LUA="$WORK_DIR/en.lua" \ TACHYON_INSTALLER_LEGACY_BASE_INIT="$WORK_DIR/missing-original-init" \ TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \ TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_INSTALLER_LEGACY_CONFIG_ALT="$LEGACY_CONFIG_ALT" \ TACHYON_INSTALLER_LEGACY_INIT="$WORK_DIR/legacy-init" \ TACHYON_INSTALLER_LEGACY_BIN="$WORK_DIR/legacy-bin" \ TACHYON_INSTALLER_LEGACY_LIB="$WORK_DIR/legacy-lib" \ TACHYON_INSTALLER_FAKE_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \ TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/init.log" \ TACHYON_INSTALLER_BIN_LOG="$WORK_DIR/bin.log" \ TACHYON_INSTALLER_FAIL_BACKEND_RESTORE=1 \ TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \ ucode "$helper" installer-cleanup-legacy >"$state" grep -Fxq 'TACHYON_WAS_ENABLED=1' "$state" || fail "installer cleanup must export previous enabled state" grep -Fxq 'TACHYON_WAS_RUNNING=1' "$state" || fail "installer cleanup must export previous running state" grep -Fxq 'TACHYON_LEGACY_DETECTED=1' "$state" || fail "installer cleanup must report the legacy package transition" grep -Fxq 'stop' "$WORK_DIR/init.log" || fail "installer cleanup must stop the legacy service through ucode owner" grep -Fxq 'disable' "$WORK_DIR/init.log" || fail "installer cleanup must disable the legacy service through ucode owner" grep -Fxq 'restore_dnsmasq' "$WORK_DIR/bin.log" || fail "installer cleanup must prefer backend restore_dnsmasq" grep -Fxq "$LEGACY_BACKEND" "$WORK_DIR/opkg.log" || fail "installer cleanup must remove the legacy backend package" grep -Fxq "luci-app-$LEGACY_BACKEND" "$WORK_DIR/opkg.log" || fail "installer cleanup must remove the legacy LuCI package" if printf '%s\n' '1' | PATH="$WORK_DIR:$PATH" \ TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \ TACHYON_INSTALLER_FAIL_REMOVE="$LEGACY_BACKEND" \ TACHYON_INSTALLER_FAKE_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \ TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \ ucode "$helper" installer-cleanup-legacy >/dev/null 2>&1; then fail "installer cleanup must stop when the package manager cannot remove the legacy backend" fi legacy_config="$WORK_DIR/legacy-config" legacy_config_alt="$WORK_DIR/legacy-config-alt" legacy_persistent="$WORK_DIR/legacy-persistent" tachyon_persistent="$WORK_DIR/tachyon-persistent" legacy_runtime="$WORK_DIR/legacy-runtime" legacy_tmp="$WORK_DIR/legacy-tmp" legacy_tmp_alt="$WORK_DIR/legacy-tmp-alt" legacy_base_config="$WORK_DIR/legacy-base-config" legacy_base_persistent="$WORK_DIR/legacy-base-persistent" legacy_base_runtime="$WORK_DIR/legacy-base-runtime" legacy_base_tmp="$WORK_DIR/legacy-base-tmp" legacy_base_init="$WORK_DIR/legacy-base-init" legacy_base_bin="$WORK_DIR/legacy-base-bin" legacy_base_lib="$WORK_DIR/legacy-base-lib" legacy_base_uci_defaults="$WORK_DIR/legacy-base-uci-defaults" legacy_base_luci_view="$WORK_DIR/legacy-base-luci-view" legacy_base_menu="$WORK_DIR/legacy-base-menu" legacy_base_acl="$WORK_DIR/legacy-base-acl" legacy_base_i18n="$WORK_DIR/legacy-base-i18n" legacy_tmp_package="$WORK_DIR/luci-app-${LEGACY_BRAND}.ipk" legacy_scan_root="$WORK_DIR/legacy-scan-root" legacy_nested_uci="$legacy_scan_root/.uci/${LEGACY_BACKEND}" legacy_nested_lock="$legacy_scan_root/lock/procd_${LEGACY_BACKEND}.lock" legacy_nested_backup="$legacy_scan_root/audit/${LEGACY_BACKEND}.config" mkdir -p \ "$legacy_persistent/tailscale/server-new" \ "$legacy_persistent/tailscale/server-existing" \ "$tachyon_persistent/tailscale/server-existing" \ "$legacy_runtime" "$legacy_tmp" "$legacy_tmp_alt" printf '%s\n' 'legacy-node-identity' >"$legacy_persistent/tailscale/server-new/node.key" printf '%s\n' 'stale-legacy-identity' >"$legacy_persistent/tailscale/server-existing/node.key" printf '%s\n' 'current-tachyon-identity' >"$tachyon_persistent/tailscale/server-existing/node.key" mkdir -p "$(dirname "$legacy_nested_uci")" "$(dirname "$legacy_nested_lock")" "$(dirname "$legacy_nested_backup")" touch "$legacy_config" "$legacy_config_alt" "$legacy_config-opkg" touch \ "$legacy_base_config.backup" \ "$legacy_base_persistent.cache" \ "$legacy_base_runtime.internal" \ "$legacy_base_tmp.log" \ "$legacy_base_init.old" \ "$legacy_base_bin.bak" \ "$legacy_base_lib.prev" \ "$legacy_base_uci_defaults.done" \ "$legacy_base_luci_view.old" \ "$legacy_base_menu.json" \ "$legacy_base_acl.json" \ "$legacy_base_i18n.ru.lmo" \ "$legacy_tmp_package" \ "$legacy_nested_uci" \ "$legacy_nested_lock" \ "$legacy_nested_backup" TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \ TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_INSTALLER_LEGACY_CONFIG_ALT="$LEGACY_CONFIG_ALT" \ TACHYON_INSTALLER_LEGACY_CONFIG="$legacy_config" \ TACHYON_INSTALLER_LEGACY_CONFIG_FILE_ALT="$legacy_config_alt" \ TACHYON_INSTALLER_LEGACY_PERSISTENT_DIR="$legacy_persistent" \ TACHYON_INSTALLER_PERSISTENT_DIR="$tachyon_persistent" \ TACHYON_INSTALLER_LEGACY_RUNTIME_DIR="$legacy_runtime" \ TACHYON_INSTALLER_LEGACY_TMP_DIR="$legacy_tmp" \ TACHYON_INSTALLER_LEGACY_TMP_ALT_DIR="$legacy_tmp_alt" \ TACHYON_INSTALLER_LEGACY_BASE_CONFIG="$legacy_base_config" \ TACHYON_INSTALLER_LEGACY_BASE_PERSISTENT_DIR="$legacy_base_persistent" \ TACHYON_INSTALLER_LEGACY_BASE_RUNTIME_DIR="$legacy_base_runtime" \ TACHYON_INSTALLER_LEGACY_BASE_TMP_DIR="$legacy_base_tmp" \ TACHYON_INSTALLER_LEGACY_BASE_INIT="$legacy_base_init" \ TACHYON_INSTALLER_LEGACY_BASE_BIN="$legacy_base_bin" \ TACHYON_INSTALLER_LEGACY_BASE_LIB="$legacy_base_lib" \ TACHYON_INSTALLER_LEGACY_BASE_UCI_DEFAULTS="$legacy_base_uci_defaults" \ TACHYON_INSTALLER_LEGACY_BASE_LUCI_VIEW="$legacy_base_luci_view" \ TACHYON_INSTALLER_LEGACY_BASE_MENU_JSON="$legacy_base_menu" \ TACHYON_INSTALLER_LEGACY_BASE_ACL_JSON="$legacy_base_acl" \ TACHYON_INSTALLER_LEGACY_BASE_I18N="$legacy_base_i18n" \ TACHYON_INSTALLER_LEGACY_TMP_PACKAGE_GLOB="$WORK_DIR/*${LEGACY_BRAND}*" \ TACHYON_INSTALLER_LEGACY_SCAN_ROOTS="$legacy_scan_root" \ ucode "$helper" installer-finalize-legacy [ "$(cat "$tachyon_persistent/tailscale/server-new/node.key")" = 'legacy-node-identity' ] || fail "installer finalization must migrate legacy Tailscale node identity" [ "$(cat "$tachyon_persistent/tailscale/server-existing/node.key")" = 'current-tachyon-identity' ] || fail "installer finalization must not overwrite existing Tachyon Tailscale state" for path in "$legacy_config" "$legacy_config_alt" "$legacy_config-opkg" \ "$legacy_persistent" "$legacy_runtime" "$legacy_tmp" "$legacy_tmp_alt" \ "$legacy_base_config.backup" "$legacy_base_persistent.cache" \ "$legacy_base_runtime.internal" "$legacy_base_tmp.log" \ "$legacy_base_init.old" "$legacy_base_bin.bak" "$legacy_base_lib.prev" \ "$legacy_base_uci_defaults.done" "$legacy_base_luci_view.old" \ "$legacy_base_menu.json" "$legacy_base_acl.json" "$legacy_base_i18n.ru.lmo" \ "$legacy_tmp_package" "$legacy_nested_uci" "$legacy_nested_lock" \ "$legacy_nested_backup"; do [ ! -e "$path" ] || fail "installer finalization left a legacy path behind: $path" done mkdir -p "$legacy_persistent/tailscale/server-failed" printf '%s\n' 'preserve-on-failure' >"$legacy_persistent/tailscale/server-failed/node.key" tachyon_persistent_file="$WORK_DIR/tachyon-persistent-file" touch "$tachyon_persistent_file" if TACHYON_INSTALLER_LEGACY_BRAND="$LEGACY_BRAND" \ TACHYON_INSTALLER_LEGACY_BACKEND="$LEGACY_BACKEND" \ TACHYON_INSTALLER_LEGACY_CONFIG_ALT="$LEGACY_CONFIG_ALT" \ TACHYON_INSTALLER_LEGACY_CONFIG="$legacy_config" \ TACHYON_INSTALLER_LEGACY_CONFIG_FILE_ALT="$legacy_config_alt" \ TACHYON_INSTALLER_LEGACY_PERSISTENT_DIR="$legacy_persistent" \ TACHYON_INSTALLER_PERSISTENT_DIR="$tachyon_persistent_file" \ TACHYON_INSTALLER_LEGACY_RUNTIME_DIR="$legacy_runtime" \ TACHYON_INSTALLER_LEGACY_TMP_DIR="$legacy_tmp" \ TACHYON_INSTALLER_LEGACY_TMP_ALT_DIR="$legacy_tmp_alt" \ TACHYON_INSTALLER_LEGACY_BASE_CONFIG="$legacy_base_config" \ TACHYON_INSTALLER_LEGACY_BASE_PERSISTENT_DIR="$legacy_base_persistent" \ TACHYON_INSTALLER_LEGACY_BASE_RUNTIME_DIR="$legacy_base_runtime" \ TACHYON_INSTALLER_LEGACY_BASE_TMP_DIR="$legacy_base_tmp" \ TACHYON_INSTALLER_LEGACY_BASE_INIT="$legacy_base_init" \ TACHYON_INSTALLER_LEGACY_BASE_BIN="$legacy_base_bin" \ TACHYON_INSTALLER_LEGACY_BASE_LIB="$legacy_base_lib" \ TACHYON_INSTALLER_LEGACY_BASE_UCI_DEFAULTS="$legacy_base_uci_defaults" \ TACHYON_INSTALLER_LEGACY_BASE_LUCI_VIEW="$legacy_base_luci_view" \ TACHYON_INSTALLER_LEGACY_BASE_MENU_JSON="$legacy_base_menu" \ TACHYON_INSTALLER_LEGACY_BASE_ACL_JSON="$legacy_base_acl" \ TACHYON_INSTALLER_LEGACY_BASE_I18N="$legacy_base_i18n" \ TACHYON_INSTALLER_LEGACY_TMP_PACKAGE_GLOB="$WORK_DIR/missing-${LEGACY_BRAND}*" \ TACHYON_INSTALLER_LEGACY_SCAN_ROOTS="$legacy_scan_root" \ ucode "$helper" installer-finalize-legacy >/dev/null 2>&1; then fail "installer finalization must fail when legacy Tailscale state cannot be migrated" fi [ "$(cat "$legacy_persistent/tailscale/server-failed/node.key")" = 'preserve-on-failure' ] || fail "failed Tailscale migration must preserve the legacy identity" write_fake_service_init "$WORK_DIR/tachyon-init" touch "$WORK_DIR/luci-indexcache.one" "$WORK_DIR/luci-indexcache.two" : >"$WORK_DIR/init.log" TACHYON_INSTALLER_INIT="$WORK_DIR/tachyon-init" \ TACHYON_INSTALLER_RPCD_INIT="$WORK_DIR/missing-rpcd" \ TACHYON_INSTALLER_LUCI_CACHE_GLOBS="$WORK_DIR/luci-indexcache*" \ TACHYON_INSTALLER_LATEST_VERSION_CACHE="$WORK_DIR/latest.cache" \ TACHYON_INSTALLER_SYSTEM_INFO_CACHE="$WORK_DIR/system-info.json" \ TACHYON_INSTALLER_SERVER_COUNTRY_CACHE="$WORK_DIR/server-country.json" \ TACHYON_INSTALLER_SING_BOX_VERSION_CACHE="$WORK_DIR/sing-box-version" \ TACHYON_INSTALLER_TMP_SYSTEM_INFO_CACHE="$WORK_DIR/tmp-system-info.json" \ TACHYON_WAS_ENABLED=1 \ TACHYON_WAS_RUNNING=1 \ TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/init.log" \ ucode "$helper" installer-post-install if compgen -G "$WORK_DIR/luci-indexcache*" >/dev/null; then fail "installer post-install must clear LuCI caches through ucode owner" fi grep -Fxq 'enable' "$WORK_DIR/init.log" || fail "installer post-install must restore enabled state through ucode owner" grep -Fxq 'start' "$WORK_DIR/init.log" || fail "installer post-install must restore running state through ucode owner" printf 'installer ownership checks passed\n'