diff --git a/cmd/server/main.go b/cmd/server/main.go index a84d2d21..ce6770b5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -20,6 +20,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/cmd" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/store" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" @@ -391,6 +392,7 @@ func main() { } else { cfg.AuthDir = resolvedAuthDir } + managementasset.SetCurrentConfig(cfg) // Create login options to be used in authentication flows. options := &cmd.LoginOptions{ @@ -434,6 +436,7 @@ func main() { return } // Start the main proxy service + managementasset.StartAutoUpdater(context.Background(), configFilePath) cmd.StartService(cfg, configFilePath, password) } } diff --git a/internal/api/server.go b/internal/api/server.go index 79b1b4ba..1df9c7a4 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -232,6 +232,7 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk // Save initial YAML snapshot s.oldConfigYaml, _ = yaml.Marshal(cfg) s.applyAccessConfig(nil, cfg) + managementasset.SetCurrentConfig(cfg) // Initialize management handler s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager) if optionState.localPassword != "" { @@ -759,6 +760,7 @@ func (s *Server) UpdateClients(cfg *config.Config) { s.applyAccessConfig(oldCfg, cfg) s.cfg = cfg + managementasset.SetCurrentConfig(cfg) // Save YAML snapshot for next comparison s.oldConfigYaml, _ = yaml.Marshal(cfg) s.handlers.UpdateClients(&cfg.SDKConfig) diff --git a/internal/managementasset/updater.go b/internal/managementasset/updater.go index 22aa9dea..4a9ba095 100644 --- a/internal/managementasset/updater.go +++ b/internal/managementasset/updater.go @@ -13,8 +13,10 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "time" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" log "github.com/sirupsen/logrus" @@ -33,8 +35,83 @@ const ManagementFileName = managementAssetName var ( lastUpdateCheckMu sync.Mutex lastUpdateCheckTime time.Time + + currentConfigPtr atomic.Pointer[config.Config] + disableControlPanel atomic.Bool + schedulerOnce sync.Once + schedulerConfigPath atomic.Value ) +// SetCurrentConfig stores the latest configuration snapshot for management asset decisions. +func SetCurrentConfig(cfg *config.Config) { + if cfg == nil { + currentConfigPtr.Store(nil) + return + } + + prevDisabled := disableControlPanel.Load() + currentConfigPtr.Store(cfg) + disableControlPanel.Store(cfg.RemoteManagement.DisableControlPanel) + + if prevDisabled && !cfg.RemoteManagement.DisableControlPanel { + lastUpdateCheckMu.Lock() + lastUpdateCheckTime = time.Time{} + lastUpdateCheckMu.Unlock() + } +} + +// StartAutoUpdater launches a background goroutine that periodically ensures the management asset is up to date. +// It respects the disable-control-panel flag on every iteration and supports hot-reloaded configurations. +func StartAutoUpdater(ctx context.Context, configFilePath string) { + configFilePath = strings.TrimSpace(configFilePath) + if configFilePath == "" { + log.Debug("management asset auto-updater skipped: empty config path") + return + } + + schedulerConfigPath.Store(configFilePath) + + schedulerOnce.Do(func() { + go runAutoUpdater(ctx) + }) +} + +func runAutoUpdater(ctx context.Context) { + if ctx == nil { + ctx = context.Background() + } + + ticker := time.NewTicker(updateCheckInterval) + defer ticker.Stop() + + runOnce := func() { + cfg := currentConfigPtr.Load() + if cfg == nil { + log.Debug("management asset auto-updater skipped: config not yet available") + return + } + if disableControlPanel.Load() { + log.Debug("management asset auto-updater skipped: control panel disabled") + return + } + + configPath, _ := schedulerConfigPath.Load().(string) + staticDir := StaticDir(configPath) + EnsureLatestManagementHTML(ctx, staticDir, cfg.ProxyURL) + } + + runOnce() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + runOnce() + } + } +} + func newHTTPClient(proxyURL string) *http.Client { client := &http.Client{Timeout: 15 * time.Second} @@ -109,6 +186,11 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL ctx = context.Background() } + if disableControlPanel.Load() { + log.Debug("management asset sync skipped: control panel disabled by configuration") + return + } + staticDir = strings.TrimSpace(staticDir) if staticDir == "" { log.Debug("management asset sync skipped: empty static directory")