From a65ced4a9251ff2d26f258187422d7f058435e2a Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Wed, 17 Jun 2026 01:06:57 +0800 Subject: [PATCH] fix(management): reload plugins asynchronously after changes --- internal/api/handlers/management/plugin_store.go | 2 +- .../api/handlers/management/plugin_store_test.go | 11 ++++------- internal/api/handlers/management/plugins.go | 13 +++++++++++-- internal/api/handlers/management/plugins_test.go | 7 +++++++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/internal/api/handlers/management/plugin_store.go b/internal/api/handlers/management/plugin_store.go index a41aae3c..161f8986 100644 --- a/internal/api/handlers/management/plugin_store.go +++ b/internal/api/handlers/management/plugin_store.go @@ -274,7 +274,7 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) { reloadCfg := h.cfg h.mu.Unlock() - h.reloadConfigAfterManagementSave(c.Request.Context(), reloadCfg) + h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg) log.WithFields(log.Fields{ "plugin_id": result.ID, "source_id": source.ID, diff --git a/internal/api/handlers/management/plugin_store_test.go b/internal/api/handlers/management/plugin_store_test.go index 9f10b128..c6a92b84 100644 --- a/internal/api/handlers/management/plugin_store_test.go +++ b/internal/api/handlers/management/plugin_store_test.go @@ -511,12 +511,9 @@ func TestInstallPluginFromStoreOverwritesFilePreservesConfigAndReloads(t *testin "https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"), }, } - reloads := 0 + reloads := make(chan *config.Config, 1) h.SetConfigReloadHook(func(_ context.Context, cfg *config.Config) { - reloads++ - if cfg != h.cfg { - t.Fatalf("reload config = %p, want handler config %p", cfg, h.cfg) - } + reloads <- cfg }) rec := httptest.NewRecorder() @@ -529,8 +526,8 @@ func TestInstallPluginFromStoreOverwritesFilePreservesConfigAndReloads(t *testin if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) } - if reloads != 1 { - t.Fatalf("reloads = %d, want 1", reloads) + if cfg := waitForAsyncReload(t, reloads); cfg != h.cfg { + t.Fatalf("reload config = %p, want handler config %p", cfg, h.cfg) } data, errRead := os.ReadFile(existingPath) if errRead != nil { diff --git a/internal/api/handlers/management/plugins.go b/internal/api/handlers/management/plugins.go index f58f63d8..dcf71630 100644 --- a/internal/api/handlers/management/plugins.go +++ b/internal/api/handlers/management/plugins.go @@ -215,18 +215,27 @@ func (h *Handler) PatchPluginEnabled(c *gin.Context) { } h.mu.Lock() - defer h.mu.Unlock() ensurePluginConfigMap(h.cfg) item := h.cfg.Plugins.Configs[id] node := pluginConfigNode(item) setYAMLMappingValue(node, "enabled", boolYAMLNode(*body.Enabled)) updated, errConfig := pluginInstanceConfigFromNode(node) if errConfig != nil { + h.mu.Unlock() c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_config", "message": errConfig.Error()}) return } h.cfg.Plugins.Configs[id] = updated - h.persistLocked(c) + if errSave := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); errSave != nil { + h.mu.Unlock() + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", errSave)}) + return + } + reloadCfg := h.cfg + h.mu.Unlock() + + h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) } // PutPluginConfig replaces plugins.configs. with the request object. diff --git a/internal/api/handlers/management/plugins_test.go b/internal/api/handlers/management/plugins_test.go index cbfbcdfc..dfb273c7 100644 --- a/internal/api/handlers/management/plugins_test.go +++ b/internal/api/handlers/management/plugins_test.go @@ -241,6 +241,10 @@ func TestPatchPluginEnabledUpdatesOnlyPluginConfig(t *testing.T) { }, configFilePath: writeTestConfigFile(t), } + reloads := make(chan *config.Config, 1) + h.SetConfigReloadHook(func(_ context.Context, cfg *config.Config) { + reloads <- cfg + }) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) @@ -253,6 +257,9 @@ func TestPatchPluginEnabledUpdatesOnlyPluginConfig(t *testing.T) { if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) } + if cfg := waitForAsyncReload(t, reloads); cfg != h.cfg { + t.Fatalf("reload config = %p, want handler config %p", cfg, h.cfg) + } if h.cfg.Plugins.Enabled { t.Fatal("global Plugins.Enabled changed to true") }