Ver código fonte

fix(warp): surface update-clock persistence failures (#6209)

Co-authored-by: n0ctal <[email protected]>
n0ctal 12 horas atrás
pai
commit
64f4f0746c

+ 2 - 2
internal/web/controller/xray_setting.go

@@ -190,7 +190,7 @@ func (a *XraySettingController) warp(c *gin.Context) {
 			a.XrayService.SetToNeedRestart()
 			// Restart the auto-update clock so a scheduled rotation
 			// doesn't fire right after this manual one.
-			_ = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
+			err = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
 		}
 	case "license":
 		license := c.PostForm("license")
@@ -202,7 +202,7 @@ func (a *XraySettingController) warp(c *gin.Context) {
 		} else if err = a.SettingService.SetWarpUpdateInterval(interval); err == nil && interval > 0 {
 			// Count the interval from now rather than from epoch 0,
 			// otherwise the job would rotate on its next tick.
-			_ = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
+			err = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
 		}
 	}
 

+ 71 - 0
internal/web/controller/xray_setting_warp_test.go

@@ -0,0 +1,71 @@
+package controller
+
+import (
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
+)
+
+func TestWarpIntervalReportsClockPersistenceFailure(t *testing.T) {
+	gin.SetMode(gin.TestMode)
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+	db := database.GetDB()
+	for _, setting := range []*model.Setting{
+		{Key: "warpUpdateInterval", Value: "0"},
+		{Key: "warpLastUpdate", Value: "0"},
+	} {
+		if err := db.Create(setting).Error; err != nil {
+			t.Fatalf("seed %s: %v", setting.Key, err)
+		}
+	}
+
+	const callback = "test:fail_warp_clock_update"
+	errInjected := errors.New("injected WARP clock persistence failure")
+	if err := db.Callback().Update().Before("gorm:update").Register(callback, func(tx *gorm.DB) {
+		setting, ok := tx.Statement.Model.(*model.Setting)
+		if ok && setting.Key == "warpLastUpdate" {
+			tx.AddError(errInjected)
+		}
+	}); err != nil {
+		t.Fatalf("register update callback: %v", err)
+	}
+	t.Cleanup(func() {
+		if err := db.Callback().Update().Remove(callback); err != nil {
+			t.Errorf("remove update callback: %v", err)
+		}
+	})
+
+	engine := gin.New()
+	engine.Use(func(c *gin.Context) {
+		c.Set("I18n", func(_ locale.I18nType, key string, _ ...string) string { return key })
+		c.Next()
+	})
+	NewXraySettingController(engine.Group("/panel/api"))
+	form := url.Values{"interval": {"7"}}
+	req := httptest.NewRequest(http.MethodPost, "/panel/api/xray/warp/interval", strings.NewReader(form.Encode()))
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	w := httptest.NewRecorder()
+	engine.ServeHTTP(w, req)
+	if !strings.Contains(w.Body.String(), `"success":false`) {
+		t.Fatalf("interval update reported success after clock persistence failure: %s", w.Body.String())
+	}
+	if !strings.Contains(w.Body.String(), errInjected.Error()) {
+		t.Fatalf("response omitted clock persistence error: %s", w.Body.String())
+	}
+}

+ 11 - 3
internal/web/job/warp_ip_job.go

@@ -28,13 +28,19 @@ func (j *WarpIpJob) Run() {
 		return
 	}
 
-	lastUpdate, _ := j.settingService.GetWarpLastUpdate()
+	lastUpdate, err := j.settingService.GetWarpLastUpdate()
+	if err != nil {
+		logger.Warning("Failed to read scheduled WARP IP update time: ", err)
+		return
+	}
 	now := time.Now().Unix()
 
 	// First run after the feature is enabled (e.g. interval set via direct
 	// DB edit): establish a baseline instead of rotating immediately.
 	if lastUpdate == 0 {
-		_ = j.settingService.SetWarpLastUpdate(now)
+		if err := j.settingService.SetWarpLastUpdate(now); err != nil {
+			logger.Warning("Failed to establish scheduled WARP IP update time: ", err)
+		}
 		return
 	}
 
@@ -46,7 +52,9 @@ func (j *WarpIpJob) Run() {
 			return
 		}
 
-		_ = j.settingService.SetWarpLastUpdate(now)
+		if err := j.settingService.SetWarpLastUpdate(now); err != nil {
+			logger.Warning("WARP IP changed but the next-update time was not saved: ", err)
+		}
 		j.xrayService.SetToNeedRestart()
 		logger.Info("Successfully updated WARP IP and scheduled Xray restart")
 	}

+ 29 - 0
internal/web/job/warp_ip_job_test.go

@@ -0,0 +1,29 @@
+package job
+
+import (
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+)
+
+func TestWarpIpJobInitializesMissingLastUpdate(t *testing.T) {
+	setupIntegrationDB(t)
+
+	settings := service.SettingService{}
+	if err := settings.SetWarpUpdateInterval(1); err != nil {
+		t.Fatalf("enable scheduled WARP rotation: %v", err)
+	}
+
+	job := &WarpIpJob{settingService: settings}
+	job.Run()
+
+	var stored model.Setting
+	if err := database.GetDB().Where("key = ?", "warpLastUpdate").First(&stored).Error; err != nil {
+		t.Fatalf("scheduled WARP rotation did not establish its baseline: %v", err)
+	}
+	if stored.Value == "" || stored.Value == "0" {
+		t.Fatalf("warpLastUpdate = %q, want a non-zero baseline", stored.Value)
+	}
+}

+ 9 - 3
internal/web/service/setting.go

@@ -391,11 +391,17 @@ func (s *SettingService) setInt(key string, value int) error {
 }
 
 func (s *SettingService) GetWarpLastUpdate() (int64, error) {
-	val, err := s.getString("warpLastUpdate")
-	if err != nil || val == "" {
+	setting, err := s.getSetting("warpLastUpdate")
+	if database.IsNotFound(err) {
+		return 0, nil
+	}
+	if err != nil {
 		return 0, err
 	}
-	return strconv.ParseInt(val, 10, 64)
+	if setting.Value == "" {
+		return 0, nil
+	}
+	return strconv.ParseInt(setting.Value, 10, 64)
 }
 
 func (s *SettingService) SetWarpLastUpdate(val int64) error {