diff --git a/deploy/test/smoke-noninteractive.sh b/deploy/test/smoke-noninteractive.sh index f05ac57fa..fa02972c2 100644 --- a/deploy/test/smoke-noninteractive.sh +++ b/deploy/test/smoke-noninteractive.sh @@ -87,6 +87,24 @@ docker run --rm \ *) echo "FAIL: panel did not serve (status ${code:-none})"; tail -n 30 /tmp/xui.log; exit 1 ;; esac + echo "--- verifying a second install preserves custom bin/ files ---" + echo "custom-sentinel" > /usr/local/x-ui/bin/geoip_custom.dat + geoip_sum_before=$(sha256sum /usr/local/x-ui/bin/geoip.dat | cut -d" " -f1) + + if [ -n "${XUI_SMOKE_VERSION:-}" ]; then + cat /root/install.sh | bash -s -- "$XUI_SMOKE_VERSION" + else + cat /root/install.sh | bash + fi + + test -f /usr/local/x-ui/bin/geoip_custom.dat \ + || { echo "FAIL: custom bin/ file did not survive a second install"; exit 1; } + [ "$(cat /usr/local/x-ui/bin/geoip_custom.dat)" = "custom-sentinel" ] \ + || { echo "FAIL: custom bin/ file content changed across a second install"; exit 1; } + geoip_sum_after=$(sha256sum /usr/local/x-ui/bin/geoip.dat | cut -d" " -f1) + [ "$geoip_sum_after" = "$geoip_sum_before" ] \ + || { echo "FAIL: bundled geoip.dat changed across a same-version reinstall"; exit 1; } + echo "SMOKE_PASS: user=$XUI_USERNAME port=$XUI_PANEL_PORT path=$XUI_WEB_BASE_PATH" ' diff --git a/install.sh b/install.sh index 38878873c..f5725d46e 100644 --- a/install.sh +++ b/install.sh @@ -1478,6 +1478,7 @@ install_x-ui() { fi # Stop x-ui service and remove old resources + local custom_bin_backup="" if [[ -e ${xui_folder}/ ]]; then if [[ $release == "alpine" ]]; then rc-service x-ui stop @@ -1489,6 +1490,31 @@ install_x-ui() { # an inbound port with an outdated secret, silently breaking new clients. # The freshly installed panel respawns a clean mtg per inbound on start. pkill -f 'mtg-linux-[^ ]* run ' > /dev/null 2>&1 || true + + # bin/ is about to be wiped wholesale by the tar extraction below. The + # release only ships known assets (xray/mtg binaries, the bundled + # geoip*/geosite*.dat sets) -- anything else in bin/ was placed there + # by the admin (e.g. a hand-added custom geoip/geosite file referenced + # from a routing rule via "ext::") and would otherwise be + # silently deleted on every update, breaking Xray at next start with + # "failed to open : no such file or directory" for any routing + # rule that references it. Moved aside rather than copied: a rename + # on the same filesystem is atomic (no truncated file if disk space + # runs out mid-copy, unlike `cp`) and keeps the snapshot under + # /usr/local rather than a separate, possibly small/tmpfs $TMPDIR. + if [[ -d "${xui_folder}/bin" ]]; then + custom_bin_backup="${xui_folder%/x-ui}/x-ui-bin-backup.$$" + rm -rf "${custom_bin_backup}" + if ! mv "${xui_folder}/bin" "${custom_bin_backup}"; then + custom_bin_backup="" + echo -e "${yellow}Could not back up bin/ -- custom files there will not be preserved across this update${plain}" + fi + fi + # Sole cleanup path for the backup from here on -- covers both the + # two `exit 1`s below (extraction/binary-missing failures) and an + # interrupted update (Ctrl-C, signal) before the restore runs. + # Cleared once the restore below finishes normally. + trap '[[ -n "${custom_bin_backup}" ]] && rm -rf "${custom_bin_backup}"' EXIT INT TERM rm ${xui_folder}/ -rf fi @@ -1529,6 +1555,39 @@ install_x-ui() { chmod +x bin/mtg-linux-$(arch) fi + # Restore anything from the old bin/ that the fresh release doesn't ship + # (custom geoip/geosite files, or anything else an admin hand-placed + # there) -- never overwrites a same-named file the new release provides, + # so bundled assets (geoip.dat, geoip_RU.dat, ...) still get the fresh + # per-release copy. Runs after the arch-rename above so xray-linux-arm32/ + # mtg-linux-arm already exist under their final names there and aren't + # mistaken for custom files needing a restore. Skips paths the panel + # itself regenerates at runtime (config.json, mtproto/*.toml -- see + # internal/xray/process.go, internal/mtproto/manager.go): those aren't + # admin-placed, and restoring a stale one only resurrects dead state (an + # orphaned mtg config for a since-deleted inbound) or the wrong + # directory permissions. + if [[ -n "${custom_bin_backup}" ]]; then + local restored_custom_bin=() + while IFS= read -r -d '' f; do + local rel="${f#"${custom_bin_backup}"/}" + case "${rel}" in + config.json | mtproto | mtproto/*) continue ;; + esac + if [[ ! -e "bin/${rel}" ]]; then + mkdir -p "bin/$(dirname "${rel}")" + cp -a "${f}" "bin/${rel}" + restored_custom_bin+=("${rel}") + fi + done < <(find "${custom_bin_backup}" \( -type f -o -type l \) -print0) + rm -rf "${custom_bin_backup}" + custom_bin_backup="" + if [[ ${#restored_custom_bin[@]} -gt 0 ]]; then + echo -e "${green}Restored custom file(s) in bin/ not shipped by this release: ${restored_custom_bin[*]}${plain}" + fi + fi + trap - EXIT INT TERM + # Update x-ui cli and se set permission mv -f "${xui_script_temp}" /usr/bin/x-ui if [[ $? -ne 0 ]]; then