Ver código fonte

fix(web): restart panel after ImportDB so subPath routes match (#6446) (#6456)

* fix(web): restart panel after ImportDB so subPath routes match (#6446)

ImportDB only restarted Xray, leaving the subscription HTTP server on
startup-registered paths. Schedule the same in-process restart hook used
by restartPanel so restored subPath (and related) routes take effect
without relying on a browser follow-up that can fail after session invalidation.

* fix(web): schedule the post-import panel restart once, via PanelService

ImportDB grew a private copy of PanelService.RestartPanel (same hook check,
same Windows bail-out, same SIGHUP fallback, already diverging in log
severity) while BackupModal kept POSTing restartPanel after a successful
import, so one restore bounced the panel and the public sub server twice
back to back. The service package cannot reuse PanelService (panel imports
service), so the importDB controller now calls the existing
RestartPanel(3s) after ImportDB succeeds, the duplicated helper is dropped,
and the browser follow-up is removed; it waits out the restart and reloads.

Test drives the importDB handler against a stub xray binary and fails when
no restart is scheduled through the global restart hook.

---------

Co-authored-by: mrchatam <[email protected]>
Co-authored-by: Sanaei <[email protected]>
mrchatam 1 dia atrás
pai
commit
435ed976c0

+ 3 - 7
frontend/src/pages/index/BackupModal.tsx

@@ -59,14 +59,10 @@ export default function BackupModal({
         return;
       }
 
+      // importDB schedules the panel restart server-side; wait it out, then reload.
       onBusy({ busy: true, tip: `${t('pages.settings.restartPanel')}…` });
-      const restart = await HttpUtil.post('/panel/api/setting/restartPanel');
-      if (restart?.success) {
-        await PromiseUtil.sleep(5000);
-        window.location.reload();
-      } else {
-        onBusy({ busy: false });
-      }
+      await PromiseUtil.sleep(5000);
+      window.location.reload();
     });
     fileInput.click();
   }

+ 91 - 0
internal/web/controller/import_db_restart_test.go

@@ -0,0 +1,91 @@
+package controller
+
+import (
+	"bytes"
+	"encoding/json"
+	"mime/multipart"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"runtime"
+	"testing"
+	"time"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/global"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
+
+	"github.com/gin-gonic/gin"
+)
+
+// A successful import must schedule the panel restart itself: the browser's
+// restartPanel follow-up can 401 once the imported users table lands (#6446).
+func TestImportDBSchedulesPanelRestart(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("the stub xray binary is a shell script")
+	}
+	uploadPath := filepath.Join(t.TempDir(), "x-ui.db")
+	if err := database.InitDB(uploadPath); err != nil {
+		t.Fatalf("InitDB(upload): %v", err)
+	}
+	if err := database.CloseDB(); err != nil {
+		t.Fatalf("CloseDB(upload): %v", err)
+	}
+	upload, err := os.ReadFile(uploadPath)
+	if err != nil {
+		t.Fatalf("read upload: %v", err)
+	}
+
+	newHostTestDB(t)
+	binDir := t.TempDir()
+	t.Setenv("XUI_BIN_FOLDER", binDir)
+	t.Setenv("XUI_LOG_FOLDER", t.TempDir())
+	if err := os.WriteFile(filepath.Join(binDir, xray.GetBinaryName()), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
+		t.Fatalf("write stub xray: %v", err)
+	}
+
+	restarts := make(chan struct{}, 1)
+	global.SetRestartHook(func() {
+		select {
+		case restarts <- struct{}{}:
+		default:
+		}
+	})
+	t.Cleanup(func() { global.SetRestartHook(func() {}) })
+
+	var body bytes.Buffer
+	mw := multipart.NewWriter(&body)
+	part, err := mw.CreateFormFile("db", "x-ui.db")
+	if err != nil {
+		t.Fatalf("CreateFormFile: %v", err)
+	}
+	if _, err := part.Write(upload); err != nil {
+		t.Fatalf("write part: %v", err)
+	}
+	if err := mw.Close(); err != nil {
+		t.Fatalf("close multipart: %v", err)
+	}
+
+	a := &ServerController{}
+	engine := gin.New()
+	engine.POST("/panel/api/server/importDB", a.importDB)
+	req := httptest.NewRequest(http.MethodPost, "/panel/api/server/importDB", &body)
+	req.Header.Set("Content-Type", mw.FormDataContentType())
+	w := httptest.NewRecorder()
+	engine.ServeHTTP(w, req)
+
+	var env hostEnvelope
+	if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil {
+		t.Fatalf("decode envelope: %v body=%s", err, w.Body.String())
+	}
+	if !env.Success {
+		t.Fatalf("importDB failed: %s", env.Msg)
+	}
+
+	select {
+	case <-restarts:
+	case <-time.After(6 * time.Second):
+		t.Fatal("importDB succeeded but no panel restart was scheduled within 6s")
+	}
+}

+ 3 - 0
internal/web/controller/server.go

@@ -391,6 +391,9 @@ func (a *ServerController) importDB(c *gin.Context) {
 		jsonMsg(c, I18nWeb(c, "pages.index.importDatabaseError"), err)
 		return
 	}
+	// Startup-registered routes (subPath) must match the restored DB, and the
+	// browser's restartPanel follow-up can 401 once the imported users land (#6446).
+	_ = a.panelService.RestartPanel(3 * time.Second)
 	jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
 }