fix(update): keep Xray-core running until update downloads finish (#7002)

UpdateXray stopped the core before downloading, which also stopped the Panel Traffic Outbound bridge the download needed on servers that reach GitHub only through it, leaving every client offline. The new core is now downloaded, staged and checked with run -test against the current config while the old one serves; only the rename and restart swap it (Windows still stops first). update.sh gets the bridge as XUI_UPDATE_PROXY, installs x-ui.sh from the release archive, and fetches Alpine's x-ui.rc before stopping x-ui.
This commit is contained in:
Cheng Ho Ming, Eric
2026-11-02 22:17:01 +08:00
committed by GitHub
parent 4d583cebf3
commit 6d9e836d61
8 changed files with 422 additions and 97 deletions
+5
View File
@@ -250,6 +250,9 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) {
updateScript := fmt.Sprintf("set -e; trap 'rm -f %s' EXIT; %s %s", shellQuote(scriptPath), shellQuote(bash), shellQuote(scriptPath))
runIDEnv := "XUI_UPDATE_RUN_ID=" + strconv.FormatInt(runID, 10)
statusFileEnv := "XUI_UPDATE_STATUS_FILE=" + statusFile
// update.sh sends its curl downloads through the panel egress bridge. With socks5h the
// proxy resolves GitHub's hostnames, since this server's own DNS may be filtered.
egressEnv := "XUI_UPDATE_PROXY=" + strings.Replace((&service.SettingService{}).PanelEgressProxyURL(), "socks5://", "socks5h://", 1)
proxyEnv := updateProxyEnvVars()
if systemdRun, err := exec.LookPath("systemd-run"); err == nil {
@@ -261,6 +264,7 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) {
"--setenv", "XUI_UPDATE_TAG=" + updateTag,
"--setenv", runIDEnv,
"--setenv", statusFileEnv,
"--setenv", egressEnv,
}
for _, kv := range proxyEnv {
args = append(args, "--setenv", kv)
@@ -290,6 +294,7 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) {
"XUI_UPDATE_TAG="+updateTag,
runIDEnv,
statusFileEnv,
egressEnv,
)
setDetachedProcess(cmd)
if err := cmd.Start(); err != nil {
+48 -37
View File
@@ -933,13 +933,16 @@ const (
maxXrayReleasesBytes = 32 << 20
)
func (s *ServerService) GetXrayVersions() ([]string, error) {
const (
XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
bufferSize = 8192
)
// Tests point these at a local server that stands in for GitHub.
var (
xrayReleasesURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
xrayDownloadURL = "https://github.com/XTLS/Xray-core/releases/download"
)
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, XrayURL, nil)
func (s *ServerService) GetXrayVersions() ([]string, error) {
const bufferSize = 8192
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, xrayReleasesURL, nil)
if reqErr != nil {
return nil, reqErr
}
@@ -1041,7 +1044,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
}
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
url := fmt.Sprintf("%s/%s/%s", xrayDownloadURL, version, fileName)
client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if reqErr != nil {
@@ -1154,12 +1157,8 @@ func (s *ServerService) UpdateXray(version string) error {
return fmt.Errorf("xray version %q is not in the fetched release list", version)
}
// 1. Stop xray before doing anything
if err := s.StopXrayService(); err != nil {
logger.Warning("failed to stop xray before update:", err)
}
// 2. Download the zip
// Keep the running core up until the swap. The download may need its panel egress
// bridge, and if the candidate fails the config check, clients stay on the old core.
zipFileName, err := s.downloadXRay(version)
if err != nil {
return err
@@ -1181,19 +1180,20 @@ func (s *ServerService) UpdateXray(version string) error {
return err
}
// 3. Helper to extract files
copyZipFile := func(zipName string, fileName string) error {
// Stage next to the target so the swap is an atomic rename on one filesystem, and
// keep the target's extension so Windows will run the staged file for the check.
stageZipFile := func(zipName string, fileName string) (string, error) {
zipFile, err := reader.Open(zipName)
if err != nil {
return err
return "", err
}
defer zipFile.Close()
if err := os.MkdirAll(filepath.Dir(fileName), 0o755); err != nil {
return err
return "", err
}
tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*")
tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*"+filepath.Ext(fileName))
if err != nil {
return err
return "", err
}
tmpPath := tmpFile.Name()
ok := false
@@ -1205,42 +1205,53 @@ func (s *ServerService) UpdateXray(version string) error {
}()
n, err := io.Copy(tmpFile, io.LimitReader(zipFile, maxXrayBinaryBytes+1))
if err != nil {
return err
return "", err
}
if n > maxXrayBinaryBytes {
return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
return "", fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
}
if err := tmpFile.Chmod(0o755); err != nil {
return err
return "", err
}
if err := tmpFile.Close(); err != nil {
return err
}
if runtime.GOOS == "windows" {
_ = os.Remove(fileName)
}
if err := os.Rename(tmpPath, fileName); err != nil {
return err
return "", err
}
ok = true
return nil
return tmpPath, nil
}
// 4. Extract correct binary
zipName, target := "xray", xray.GetBinaryPath()
if runtime.GOOS == "windows" {
targetBinary := filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
err = copyZipFile("xray.exe", targetBinary)
} else {
err = copyZipFile("xray", xray.GetBinaryPath())
zipName, target = "xray.exe", filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
}
staged, err := stageZipFile(zipName, target)
if err != nil {
return err
}
defer os.Remove(staged)
xrayConfig, err := s.xrayService.GetXrayConfig()
if err != nil {
return err
}
if err := xray.CheckConfig(staged, xrayConfig); err != nil {
return fmt.Errorf("xray %s failed the config check and was not installed: %w", version, err)
}
if runtime.GOOS == "windows" {
// Windows cannot replace a running executable.
if err := s.StopXrayService(); err != nil {
logger.Warning("failed to stop xray before update:", err)
}
_ = os.Remove(target)
}
if err := os.Rename(staged, target); err != nil {
return err
}
// 5. Restart xray
if err := s.xrayService.RestartXray(true); err != nil {
logger.Error("start xray failed:", err)
return err
return fmt.Errorf("xray %s is installed, but the restart failed: %w", version, err)
}
return nil
@@ -0,0 +1,207 @@
package service
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
// fakeXrayCore handles only the command lines the panel uses to run a core. It exits 99
// on any other, so a changed invocation fails the test.
func fakeXrayCore(version string, acceptsConfig bool) string {
check := "exit 0"
if !acceptsConfig {
check = "echo 'Failed to start: fake core refuses this config'; exit 23"
}
return "#!/bin/sh\n" +
"case \"$*\" in\n" +
"-version) echo 'Xray " + version + " (fake)' ;;\n" +
"'run -test -c '*) [ -s \"$4\" ] || exit 98; " + check + " ;;\n" +
"'-c '*) exec sleep 60 ;;\n" +
"*) exit 99 ;;\n" +
"esac\n"
}
func xrayReleaseZip(t *testing.T, core string) []byte {
t.Helper()
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
w, err := zw.Create("xray")
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte(core)); err != nil {
t.Fatal(err)
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
// xrayReleaseServer stands in for GitHub. It lists one version and serves asset with a
// matching .dgst, or a 404 when asset is nil.
func xrayReleaseServer(t *testing.T, version string, asset []byte) {
t.Helper()
sum := sha256.Sum256(asset)
mux := http.NewServeMux()
mux.HandleFunc("/releases", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `[{"tag_name":%q}]`, version)
})
mux.HandleFunc("/download/"+version+"/", func(w http.ResponseWriter, r *http.Request) {
switch {
case asset == nil:
http.NotFound(w, r)
case strings.HasSuffix(r.URL.Path, ".zip.dgst"):
fmt.Fprintf(w, "SHA2-256= %s\n", hex.EncodeToString(sum[:]))
case strings.HasSuffix(r.URL.Path, ".zip"):
_, _ = w.Write(asset)
default:
http.NotFound(w, r)
}
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
releases, download := xrayReleasesURL, xrayDownloadURL
xrayReleasesURL, xrayDownloadURL = srv.URL+"/releases", srv.URL+"/download"
t.Cleanup(func() { xrayReleasesURL, xrayDownloadURL = releases, download })
}
// startFakeCore runs the installed core on the panel's own config, as RestartXray
// would, and stops whichever core is current when the test ends.
func startFakeCore(t *testing.T) *xray.Process {
t.Helper()
cfg, err := (&XrayService{}).GetXrayConfig()
if err != nil {
t.Fatalf("GetXrayConfig: %v", err)
}
p := xray.NewProcess(cfg)
if err := p.Start(); err != nil {
t.Fatalf("start fake core: %v", err)
}
restore := SetXrayProcessForTest(p)
t.Cleanup(func() {
if current := XrayProcess(); current != nil {
_ = current.Stop()
}
_ = p.Stop()
restore()
// restore() keeps the hold-back reason that a refused restart sets.
xrayState.holdBack("")
})
return p
}
func TestUpdateXray(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("the fake cores are shell scripts")
}
const current, next = "26.6.27", "26.7.1"
installed := fakeXrayCore(current, true)
for _, tc := range []struct {
name string
candidate string // the release's core; empty makes the download 404
conflict bool // two inbounds on one port, added after the old core starts
wantErr string
wantBinary string
wantVersion string
}{
{
// The running core hosts the panel egress bridge. Stopping it first made the
// download bypass the bridge, and when that failed every client stayed offline.
name: "failed download leaves the running core alone",
wantErr: "unexpected HTTP 404",
wantBinary: installed,
wantVersion: current,
},
{
name: "core that refuses the config is not installed",
candidate: fakeXrayCore(next, false),
wantErr: "fake core refuses this config",
wantBinary: installed,
wantVersion: current,
},
{
name: "accepted core replaces the running one",
candidate: fakeXrayCore(next, true),
wantBinary: fakeXrayCore(next, true),
wantVersion: next,
},
{
// A refused restart keeps the old core running, but the new binary is
// already on disk, so the error has to say the update is installed.
name: "refused restart reports the installed core",
candidate: fakeXrayCore(next, true),
conflict: true,
wantErr: "is installed, but the restart failed: xray config refused",
wantBinary: fakeXrayCore(next, true),
wantVersion: current,
},
} {
t.Run(tc.name, func(t *testing.T) {
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
binFolder := t.TempDir()
t.Setenv("XUI_BIN_FOLDER", binFolder)
t.Setenv("XUI_LOG_FOLDER", t.TempDir())
binary := filepath.Join(binFolder, xray.GetBinaryName())
if err := os.WriteFile(binary, []byte(installed), 0o755); err != nil {
t.Fatal(err)
}
running := startFakeCore(t)
if tc.conflict {
for _, tag := range []string{"first", "second"} {
seedInboundConflict(t, tag, "0.0.0.0", 4443, model.VLESS, `{"network":"tcp"}`, `{}`)
}
}
var asset []byte
if tc.candidate != "" {
asset = xrayReleaseZip(t, tc.candidate)
}
xrayReleaseServer(t, "v"+next, asset)
err := (&ServerService{}).UpdateXray("v" + next)
if tc.wantErr == "" && err != nil {
t.Fatalf("UpdateXray: %v", err)
}
if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) {
t.Fatalf("UpdateXray error = %v, want it to contain %q", err, tc.wantErr)
}
if tc.wantErr != "" && (XrayProcess() != running || !running.IsRunning()) {
t.Fatal("a failed update stopped or replaced the running core")
}
if got := XrayProcess().GetXrayVersion(); got != tc.wantVersion {
t.Fatalf("running core version = %q, want %q", got, tc.wantVersion)
}
got, err := os.ReadFile(binary)
if err != nil {
t.Fatal(err)
}
if string(got) != tc.wantBinary {
t.Fatalf("installed core:\n%s\nwant:\n%s", got, tc.wantBinary)
}
leftovers, err := filepath.Glob(filepath.Join(binFolder, ".*"))
if err != nil || len(leftovers) > 0 {
t.Fatalf("temp files left in the bin folder: %v (glob err %v)", leftovers, err)
}
})
}
}
+5 -30
View File
@@ -2003,37 +2003,12 @@ func normalizeStreamUDPHop(stream map[string]any) bool {
return maskcompat.NormalizeUDPHop(stream["finalmask"])
}
// forceUserspaceWireGuardTun moves every WireGuard outbound to the gVisor TUN (#6796). Values
// stay raw so the template's other keys reach the core byte for byte.
// forceUserspaceWireGuardTun moves every WireGuard outbound to the gVisor TUN (#6796) and
// warns for each one it moves off a kernel TUN.
func forceUserspaceWireGuardTun(raw json_util.RawMessage) json_util.RawMessage {
var outbounds []map[string]json.RawMessage
if err := json.Unmarshal(raw, &outbounds); err != nil {
return raw
}
for _, ob := range outbounds {
var protocol, tag string
if json.Unmarshal(ob["protocol"], &protocol) != nil || !strings.EqualFold(protocol, "wireguard") {
continue
}
var settings map[string]json.RawMessage
if json.Unmarshal(ob["settings"], &settings) != nil || settings == nil {
continue
}
wasUserspace := string(settings["noKernelTun"]) == "true"
settings["noKernelTun"] = json.RawMessage("true")
rewritten, err := json.Marshal(settings)
if err != nil {
continue
}
ob["settings"] = rewritten
if !wasUserspace {
_ = json.Unmarshal(ob["tag"], &tag)
logger.Warningf("Outbound %q: kernel TUN cannot be set up here, running WireGuard on the userspace TUN instead (#6796)", tag)
}
}
rewritten, err := json.Marshal(outbounds)
if err != nil {
return raw
rewritten, moved := xray.ForceUserspaceWireGuardTun(raw)
for _, tag := range moved {
logger.Warningf("Outbound %q: kernel TUN cannot be set up here, running WireGuard on the userspace TUN instead (#6796)", tag)
}
return rewritten
}
+45
View File
@@ -0,0 +1,45 @@
package xray
import (
"encoding/json"
"strings"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
)
// ForceUserspaceWireGuardTun sets noKernelTun on every WireGuard outbound and returns the tags
// it changed. Values stay raw so the template's other keys reach the core byte for byte.
func ForceUserspaceWireGuardTun(raw json_util.RawMessage) (json_util.RawMessage, []string) {
var outbounds []map[string]json.RawMessage
if err := json.Unmarshal(raw, &outbounds); err != nil {
return raw, nil
}
var moved []string
for _, ob := range outbounds {
var protocol string
if json.Unmarshal(ob["protocol"], &protocol) != nil || !strings.EqualFold(protocol, "wireguard") {
continue
}
var settings map[string]json.RawMessage
if json.Unmarshal(ob["settings"], &settings) != nil || settings == nil {
continue
}
wasUserspace := string(settings["noKernelTun"]) == "true"
settings["noKernelTun"] = json.RawMessage("true")
rewritten, err := json.Marshal(settings)
if err != nil {
continue
}
ob["settings"] = rewritten
if !wasUserspace {
var tag string
_ = json.Unmarshal(ob["tag"], &tag)
moved = append(moved, tag)
}
}
rewritten, err := json.Marshal(outbounds)
if err != nil {
return raw, nil
}
return rewritten, moved
}
+37
View File
@@ -595,6 +595,43 @@ func (p *process) refreshAPIPort() {
p.mu.Unlock()
}
// xrayCheckTimeout bounds `run -test`, which loads the config and its geo files but opens
// no listeners, so it can run while the live core holds the ports.
const xrayCheckTimeout = 30 * time.Second
// CheckConfig runs the core at binaryPath in test mode against cfg. Updates call it first,
// so a core that rejects cfg never replaces the running one.
func CheckConfig(binaryPath string, cfg *Config) error {
// The test run builds every outbound. A kernel-TUN WireGuard one would add a second wg
// device and IPv6 policy rules to the host, and the run exits without removing the rules.
checked := *cfg
checked.OutboundConfigs, _ = ForceUserspaceWireGuardTun(cfg.OutboundConfigs)
data, err := json.Marshal(&checked)
if err != nil {
return err
}
// CreateTemp creates the file as 0600; the config holds client credentials.
file, err := os.CreateTemp(filepath.Dir(binaryPath), ".config-check-*.json")
if err != nil {
return err
}
defer os.Remove(file.Name())
_, err = file.Write(data)
if closeErr := file.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), xrayCheckTimeout)
defer cancel()
out, err := exec.CommandContext(ctx, binaryPath, "run", "-test", "-c", file.Name()).CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, bytes.TrimSpace(out))
}
return nil
}
// refreshVersion updates the version string by running the Xray binary with -version.
func (p *process) refreshVersion() {
version := "Unknown"
+48
View File
@@ -0,0 +1,48 @@
package xray
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
)
// A kernel-TUN WireGuard outbound in `run -test` adds a second wg device and IPv6 policy
// rules to the host, and the check exits without removing the rules.
func TestCheckConfigKeepsWireGuardOffKernelTun(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell fixture is Unix-only")
}
dir := t.TempDir()
checked := filepath.Join(dir, "checked.json")
core := filepath.Join(dir, "xray")
script := "#!/bin/sh\n[ \"$1 $2 $3\" = 'run -test -c' ] || exit 99\nexec cp \"$4\" '" + checked + "'\n"
if err := os.WriteFile(core, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
cfg := &Config{OutboundConfigs: json_util.RawMessage(
`[{"protocol":"wireguard","tag":"warp","settings":{"noKernelTun":false,"mtu":1420}}]`)}
if err := CheckConfig(core, cfg); err != nil {
t.Fatalf("CheckConfig: %v", err)
}
data, err := os.ReadFile(checked)
if err != nil {
t.Fatal(err)
}
var got struct {
Outbounds []struct {
Settings map[string]any `json:"settings"`
} `json:"outbounds"`
}
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("checked config: %v\n%s", err, data)
}
if len(got.Outbounds) != 1 || got.Outbounds[0].Settings["noKernelTun"] != true {
t.Fatalf("checked outbounds = %+v, want the wireguard outbound on noKernelTun", got.Outbounds)
}
}
+27 -30
View File
@@ -73,6 +73,9 @@ if _command_exists curl; then
else
_fail "ERROR: Command 'curl' not found."
fi
# The panel sets XUI_UPDATE_PROXY to its egress bridge's socks5h URL for the GitHub downloads.
# The IP lookups use plain curl so they still report this server's own address.
[[ -n "${XUI_UPDATE_PROXY}" ]] && curl_bin="${curl_bin} --proxy ${XUI_UPDATE_PROXY}"
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
@@ -524,17 +527,15 @@ update_x-ui() {
fi
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
# x-ui.sh, x-ui.rc and the unit files must come from the same release as
# the binary; only the rolling dev build tracks main.
# x-ui.rc and the unit files must come from the binary's own release; only the
# rolling dev build tracks main. x-ui.sh ships in the archive and needs no GitHub check.
script_ref="${tag_version}"
if [[ "${tag_version}" == "dev-latest" ]]; then
script_ref="main"
fi
# The unit files are only fetched when the release tarball lacks them, so
# they are checked at that point instead of here.
local required_files=("x-ui.sh")
[[ $release == "alpine" ]] && required_files+=("x-ui.rc")
require_repo_files "${script_ref}" "${required_files[@]}"
[[ $release == "alpine" ]] && require_repo_files "${script_ref}" "x-ui.rc"
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
@@ -566,6 +567,23 @@ update_x-ui() {
_fail "ERROR: Failed to download the checksum for x-ui-linux-$(arch).tar.gz (HTTP ${sidecar_code})"
fi
# Download everything before x-ui stops, because its Xray and any panel egress bridge
# stop with it. Alpine's init script is the only file the archive lacks.
if [[ $release == "alpine" ]]; then
echo -e "${green}Downloading startup unit x-ui.rc...${plain}"
xui_rc_temp="/etc/init.d/x-ui.tmp.$$"
rm -f "${xui_rc_temp}"
${curl_bin} -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.rc" > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
rm -f "${xui_rc_temp}" "${archive}"
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
fi
if [[ ! -s "${xui_rc_temp}" ]]; then
rm -f "${xui_rc_temp}" "${archive}"
_fail "ERROR: Downloaded startup unit x-ui.rc is empty, please be sure that your server can access GitHub"
fi
fi
if [[ -e ${xui_folder}/ ]]; then
echo -e "${green}Stopping x-ui...${plain}"
if [[ $release == "alpine" ]]; then
@@ -650,20 +668,10 @@ update_x-ui() {
chmod +x bin/mtg-linux-$(arch) > /dev/null 2>&1
fi
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
# The archive's x-ui.sh is built from the same commit as the binary.
echo -e "${green}Installing x-ui.sh script...${plain}"
local xui_script_temp="/usr/bin/x-ui-temp.$$"
rm -f "${xui_script_temp}"
${curl_bin} -fLRo "${xui_script_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.sh" > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
rm -f "${xui_script_temp}"
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
fi
if [[ ! -s "${xui_script_temp}" ]]; then
rm -f "${xui_script_temp}"
_fail "ERROR: Downloaded x-ui.sh script is empty, please be sure that your server can access GitHub"
fi
mv -f "${xui_script_temp}" /usr/bin/x-ui
if [[ $? -ne 0 ]]; then
if ! cp -f x-ui.sh "${xui_script_temp}" || ! mv -f "${xui_script_temp}" /usr/bin/x-ui; then
rm -f "${xui_script_temp}"
_fail "ERROR: Failed to install x-ui.sh script"
fi
@@ -686,18 +694,7 @@ update_x-ui() {
"${xui_folder}/x-ui" migrate
if [[ $release == "alpine" ]]; then
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
xui_rc_temp="/etc/init.d/x-ui.tmp.$$"
rm -f "${xui_rc_temp}"
${curl_bin} -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.rc" > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
rm -f "${xui_rc_temp}"
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
fi
if [[ ! -s "${xui_rc_temp}" ]]; then
rm -f "${xui_rc_temp}"
_fail "ERROR: Downloaded startup unit x-ui.rc is empty, please be sure that your server can access GitHub"
fi
echo -e "${green}Installing startup unit x-ui.rc...${plain}"
mv -f "${xui_rc_temp}" /etc/init.d/x-ui
if [[ $? -ne 0 ]]; then
rm -f "${xui_rc_temp}"