diff --git a/internal/api/handlers/management/handler.go b/internal/api/handlers/management/handler.go index ba2ef3c9..dc07ee00 100644 --- a/internal/api/handlers/management/handler.go +++ b/internal/api/handlers/management/handler.go @@ -20,6 +20,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v7/internal/pluginstore" sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" + log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" ) @@ -168,6 +169,24 @@ func (h *Handler) reloadConfigAfterManagementSave(ctx context.Context, cfg *conf } } +func (h *Handler) reloadConfigAfterManagementSaveAsync(ctx context.Context, cfg *config.Config) { + if h == nil || cfg == nil { + return + } + reloadCtx := context.Background() + if ctx != nil { + reloadCtx = context.WithoutCancel(ctx) + } + go func() { + defer func() { + if recovered := recover(); recovered != nil { + log.WithField("panic", recovered).Error("management: async config reload panicked") + } + }() + h.reloadConfigAfterManagementSave(reloadCtx, cfg) + }() +} + // SetLocalPassword configures the runtime-local password accepted for localhost requests. func (h *Handler) SetLocalPassword(password string) { h.localPassword = password } diff --git a/internal/api/handlers/management/plugins.go b/internal/api/handlers/management/plugins.go index 078098a3..f58f63d8 100644 --- a/internal/api/handlers/management/plugins.go +++ b/internal/api/handlers/management/plugins.go @@ -369,7 +369,7 @@ func (h *Handler) DeletePlugin(c *gin.Context) { reloadCfg := h.cfg h.mu.Unlock() - h.reloadConfigAfterManagementSave(c.Request.Context(), reloadCfg) + h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg) c.JSON(http.StatusOK, gin.H{ "status": "deleted", "id": htmlsanitize.String(id), diff --git a/internal/api/handlers/management/plugins_test.go b/internal/api/handlers/management/plugins_test.go index feb65e2e..cbfbcdfc 100644 --- a/internal/api/handlers/management/plugins_test.go +++ b/internal/api/handlers/management/plugins_test.go @@ -12,6 +12,7 @@ import ( "runtime" "strings" "testing" + "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" @@ -20,6 +21,17 @@ import ( "gopkg.in/yaml.v3" ) +func waitForAsyncReload(t *testing.T, reloads <-chan *config.Config) *config.Config { + t.Helper() + select { + case cfg := <-reloads: + return cfg + case <-time.After(time.Second): + t.Fatal("timed out waiting for async config reload") + return nil + } +} + func TestListPluginsIncludesScannedAndConfiguredPlugins(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) @@ -342,12 +354,12 @@ func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) { }, configFilePath: writeTestConfigFile(t), } - reloads := 0 + reloads := make(chan *config.Config, 1) + releaseReload := make(chan struct{}) + defer close(releaseReload) 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 + <-releaseReload }) path, errPath := pluginFilePath(pluginsDir, "sample") @@ -363,7 +375,17 @@ func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) { c.Params = gin.Params{{Key: "id", Value: "sample"}} c.Request = httptest.NewRequest(http.MethodDelete, "/v0/management/plugins/sample", nil) - h.DeletePlugin(c) + done := make(chan struct{}) + go func() { + h.DeletePlugin(c) + close(done) + }() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("DeletePlugin blocked waiting for config reload") + } if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) @@ -374,8 +396,8 @@ func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) { if _, errStat := os.Stat(path); !os.IsNotExist(errStat) { t.Fatalf("plugin file stat error = %v, want not exist", errStat) } - 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) } }