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
+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