فهرست منبع

Write config.json after a hot apply (#6686)

* Write config.json after a hot apply

tryHotApply only updated the in-memory snapshot, so bin/config.json stayed
stale until the next cold start and the Telegram/Discord config backups
uploaded old rules. Persist the new config once the API calls succeed; a
write failure is logged and does not restart the running core.

* Test that a hot apply refreshes config.json
kaveh 12 ساعت پیش
والد
کامیت
3948b83405

+ 10 - 0
internal/web/service/xray.go

@@ -1454,6 +1454,7 @@ func (s *XrayService) tryHotApply(process *xray.Process, newCfg *xray.Config) bo
 	}
 	if diff.Empty() {
 		process.SetConfig(newCfg)
+		persistHotConfig(process)
 		return true
 	}
 	// The core's RemoveUser drops the credential only, so a disabled or deleted
@@ -1522,9 +1523,18 @@ func (s *XrayService) tryHotApply(process *xray.Process, newCfg *xray.Config) bo
 	}
 
 	process.SetConfig(newCfg)
+	persistHotConfig(process)
 	return true
 }
 
+// persistHotConfig refreshes config.json after a hot apply; a write failure is
+// logged only, since the running core already has the change.
+func persistHotConfig(process *xray.Process) {
+	if err := process.PersistConfig(); err != nil {
+		logger.Warning("hot apply: failed to update config.json:", err)
+	}
+}
+
 // addUserReconciling adds a user, and on an email conflict (the user was
 // already applied through the runtime API) replaces the existing user instead.
 func addUserReconciling(api *xray.XrayAPI, u xray.UserOp) error {

+ 25 - 0
internal/web/service/xray_hot_apply_persist_test.go

@@ -0,0 +1,25 @@
+package service
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
+)
+
+// The empty-diff branch of tryHotApply makes no gRPC call, so it can prove the
+// hot-apply path refreshes config.json without a running core.
+func TestTryHotApplyWritesConfigFile(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "config.json")
+	routing := json_util.RawMessage(`{"rules":[]}`)
+	process := xray.NewTestProcess(&xray.Config{RouterConfig: routing}, path)
+
+	if !(&XrayService{}).tryHotApply(process, &xray.Config{RouterConfig: routing}) {
+		t.Fatal("tryHotApply = false, want true for an unchanged config")
+	}
+	if data, err := os.ReadFile(path); err != nil || len(data) == 0 {
+		t.Fatalf("config.json not written after hot apply (err %v)", err)
+	}
+}

+ 16 - 0
internal/xray/process.go

@@ -316,6 +316,22 @@ func (p *Process) SetConfig(config *Config) {
 	p.config = config
 }
 
+// PersistConfig writes the current configuration snapshot to the config file,
+// keeping it in step after a hot apply (Start only writes it on a cold start).
+func (p *Process) PersistConfig() error {
+	p.mu.RLock()
+	data, err := json.MarshalIndent(p.config, "", "  ")
+	path := p.configPath
+	p.mu.RUnlock()
+	if err != nil {
+		return common.NewErrorf("Failed to generate XRAY configuration files: %v", err)
+	}
+	if path == "" {
+		path = GetConfigPath()
+	}
+	return writeFileAtomic(path, data, 0o600)
+}
+
 // GetOnlineClients returns the union of locally-online clients and
 // node-online clients from every registered remote panel. Dedupes by
 // email so a client connected to both a local and a node-managed inbound

+ 27 - 0
internal/xray/process_persist_test.go

@@ -0,0 +1,27 @@
+package xray
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
+)
+
+func TestPersistConfigWritesCurrentSnapshot(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "config.json")
+	p := NewTestProcess(&Config{RouterConfig: json_util.RawMessage(`{"rules":[]}`)}, path)
+
+	p.SetConfig(&Config{RouterConfig: json_util.RawMessage(`{"rules":[{"outboundTag":"warp"}]}`)})
+	if err := p.PersistConfig(); err != nil {
+		t.Fatalf("PersistConfig: %v", err)
+	}
+	data, err := os.ReadFile(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !strings.Contains(string(data), `"outboundTag": "warp"`) && !strings.Contains(string(data), `"outboundTag":"warp"`) {
+		t.Fatalf("config file does not hold the new routing:\n%s", data)
+	}
+}