fix(install.sh): never delete the live install before the new one is verified

Real production incident: the previous update flow stopped the service and
rm -rf'd the existing installation, then extracted the downloaded archive
straight into place -- when that extraction failed, the panel was left
completely gone with no way back short of manual recovery. Now extracts
into a staging directory first, fully verifies and prepares it there (arch
rename, chmod), and only swaps it into place -- stopping the service and
removing the old install -- once that verification has already passed. A
bad download/extraction now just fails the update; the running install is
never touched.

Also added explicit error handling on both cd calls in this function.
Root-caused the original incident to exactly this: if the first cd ever
silently fails, later steps that build paths by absolute string
concatenation still land correctly, but the tar extraction's bare relative
filename resolves against whatever the previous cwd was instead --
producing a confusing "No such file or directory" far from its real cause.

Verified via bash -n, shellcheck -S warning (no new findings beyond this
file's existing ones), and live repro runs against an isolated
XUI_MAIN_FOLDER on a real box: the happy path installs cleanly, and a
forced cd failure now fails loudly instead of corrupting the extraction.
This commit is contained in:
Kuzz007
2026-08-03 20:18:48 +03:00
parent 7e439db065
commit 4a9c2e0b04
+66 -43
View File
@@ -1411,7 +1411,17 @@ _install_xui_service_unit() {
}
install_x-ui() {
cd ${xui_folder%/x-ui}/
# If this cd ever silently failed, every later step that builds a path
# by string-concatenating ${xui_folder} (absolute, CWD-independent)
# would still land in the right place, but the tar extraction below
# references its archive by a bare relative filename -- it would then
# look for that file wherever the shell's PREVIOUS cwd happened to be
# instead, failing with a confusing "No such file or directory" no one
# could easily connect back to a failed cd two hundred lines earlier.
if ! cd "${xui_folder%/x-ui}/"; then
echo -e "${red}Failed to cd into ${xui_folder%/x-ui}/ -- cannot continue${plain}"
exit 1
fi
# Download resources
if [ $# == 0 ]; then
@@ -1476,8 +1486,57 @@ install_x-ui() {
exit 1
fi
# Stop x-ui service and remove old resources
# Extract into a staging directory and fully verify + prepare it there
# BEFORE anything live is touched. A real production incident: an
# earlier version of this script stopped the service and deleted the
# previous installation first, then extracted straight into place --
# when the extraction failed (the archive never even reached disk that
# time), the panel was left completely gone with no way back short of a
# manual recovery. Staging first means a bad download/extraction just
# fails the update with the old installation never touched; running it
# again is always safe.
local staging="${xui_folder%/x-ui}/x-ui-staging.$$"
local custom_bin_backup=""
trap '[[ -n "${staging}" ]] && rm -rf "${staging}"; [[ -n "${custom_bin_backup}" ]] && rm -rf "${custom_bin_backup}"' EXIT INT TERM
rm -rf "${staging}"
mkdir -p "${staging}"
tar zxf x-ui-linux-$(arch).tar.gz -C "${staging}"
if [[ $? -ne 0 ]]; then
rm x-ui-linux-$(arch).tar.gz -f
rm -f "${xui_script_temp}"
echo -e "${red}Failed to extract the x-ui release archive -- the previous installation was not touched, it is safe to just try running the installer again${plain}"
exit 1
fi
rm x-ui-linux-$(arch).tar.gz -f
if [[ ! -s "${staging}/x-ui/x-ui" ]]; then
rm -f "${xui_script_temp}"
echo -e "${red}Extracted x-ui archive is missing the x-ui binary -- the previous installation was not touched, it is safe to just try running the installer again${plain}"
exit 1
fi
chmod +x "${staging}/x-ui/x-ui"
chmod +x "${staging}/x-ui/x-ui.sh"
# Check the system's architecture and rename the file accordingly.
# The panel binary maps GOARCH=arm to "arm32" (internal/xray/process.go),
# so the Xray binary must be named xray-linux-arm32; mtg keeps plain "arm".
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv "${staging}/x-ui/bin/xray-linux-$(arch)" "${staging}/x-ui/bin/xray-linux-arm32"
chmod +x "${staging}/x-ui/bin/xray-linux-arm32"
if [[ -f "${staging}/x-ui/bin/mtg-linux-$(arch)" ]]; then
mv "${staging}/x-ui/bin/mtg-linux-$(arch)" "${staging}/x-ui/bin/mtg-linux-arm"
chmod +x "${staging}/x-ui/bin/mtg-linux-arm"
fi
fi
chmod +x "${staging}/x-ui/x-ui" "${staging}/x-ui/bin/xray-linux-$(arch)"
if [[ -f "${staging}/x-ui/bin/mtg-linux-arm" ]]; then
chmod +x "${staging}/x-ui/bin/mtg-linux-arm"
elif [[ -f "${staging}/x-ui/bin/mtg-linux-$(arch)" ]]; then
chmod +x "${staging}/x-ui/bin/mtg-linux-$(arch)"
fi
# The staged build is now fully verified and prepared -- only now is it
# safe to stop the service and replace the live installation.
if [[ -e ${xui_folder}/ ]]; then
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
@@ -1490,7 +1549,7 @@ install_x-ui() {
# 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
# bin/ is about to be replaced wholesale by the staged build 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
@@ -1509,50 +1568,14 @@ install_x-ui() {
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
# Extract resources and set permissions
tar zxvf x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then
rm x-ui-linux-$(arch).tar.gz -f
rm -f "${xui_script_temp}"
echo -e "${red}Failed to extract the x-ui release archive -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the installer again${plain}"
mv "${staging}/x-ui" "${xui_folder}"
rm -rf "${staging}"
if ! cd "${xui_folder}"; then
echo -e "${red}Failed to cd into ${xui_folder} after installing -- cannot continue${plain}"
exit 1
fi
rm x-ui-linux-$(arch).tar.gz -f
cd x-ui
if [[ $? -ne 0 || ! -s x-ui ]]; then
rm -f "${xui_script_temp}"
echo -e "${red}Extracted x-ui archive is missing the x-ui binary -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the installer again${plain}"
exit 1
fi
chmod +x x-ui
chmod +x x-ui.sh
# Check the system's architecture and rename the file accordingly.
# The panel binary maps GOARCH=arm to "arm32" (internal/xray/process.go),
# so the Xray binary must be named xray-linux-arm32; mtg keeps plain "arm".
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm32
chmod +x bin/xray-linux-arm32
if [[ -f bin/mtg-linux-$(arch) ]]; then
mv bin/mtg-linux-$(arch) bin/mtg-linux-arm
chmod +x bin/mtg-linux-arm
fi
fi
chmod +x x-ui bin/xray-linux-$(arch)
if [[ -f bin/mtg-linux-arm ]]; then
chmod +x bin/mtg-linux-arm
elif [[ -f bin/mtg-linux-$(arch) ]]; then
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