349 lines
9.5 KiB
Go
349 lines
9.5 KiB
Go
package cliproxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/api"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
|
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
|
|
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Run starts the service and blocks until the context is cancelled or the server stops.
|
|
// It initializes all components including authentication, file watching, HTTP server,
|
|
// and starts processing requests. The method blocks until the context is cancelled.
|
|
//
|
|
// Parameters:
|
|
// - ctx: The context for controlling the service lifecycle
|
|
//
|
|
// Returns:
|
|
// - error: An error if the service fails to start or run
|
|
func (s *Service) Run(ctx context.Context) error {
|
|
if s == nil {
|
|
return fmt.Errorf("cliproxy: service is nil")
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
ctx, runCancel := context.WithCancel(ctx)
|
|
s.homeMu.Lock()
|
|
s.runCancel = runCancel
|
|
s.homeMu.Unlock()
|
|
defer func() {
|
|
runCancel()
|
|
s.homeMu.Lock()
|
|
if s.runCancel != nil {
|
|
s.runCancel = nil
|
|
}
|
|
s.homeMu.Unlock()
|
|
}()
|
|
|
|
usage.StartDefault(ctx)
|
|
homeEnabled := s.cfg != nil && s.cfg.Home.Enabled
|
|
if homeEnabled {
|
|
forceHomeRuntimeConfig(s.cfg)
|
|
redisqueue.SetUsageStatisticsEnabled(true)
|
|
}
|
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer shutdownCancel()
|
|
defer func() {
|
|
if err := s.Shutdown(shutdownCtx); err != nil {
|
|
log.Errorf("service shutdown returned error: %v", err)
|
|
}
|
|
}()
|
|
|
|
if !homeEnabled {
|
|
if errEnsureAuthDir := s.ensureAuthDir(); errEnsureAuthDir != nil {
|
|
return errEnsureAuthDir
|
|
}
|
|
}
|
|
|
|
s.applyRetryConfig(s.cfg)
|
|
s.configureCooldownStateStore(s.cfg)
|
|
|
|
s.registerPluginAuthParser()
|
|
if s.coreManager != nil && !homeEnabled {
|
|
if errLoad := s.coreManager.Load(ctx); errLoad != nil {
|
|
log.Warnf("failed to load auth store: %v", errLoad)
|
|
}
|
|
s.registerConfigAPIKeyAuths(coreauth.WithSkipPersist(ctx), s.cfg)
|
|
if s.cfg.SaveCooldownStatus {
|
|
if errRestoreCooldown := s.coreManager.RestoreCooldownStates(ctx); errRestoreCooldown != nil {
|
|
log.Warnf("failed to restore cooldown state: %v", errRestoreCooldown)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !homeEnabled {
|
|
tokenResult, err := s.tokenProvider.Load(ctx, s.cfg)
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
return err
|
|
}
|
|
if tokenResult == nil {
|
|
tokenResult = &TokenClientResult{}
|
|
}
|
|
|
|
apiKeyResult, err := s.apiKeyProvider.Load(ctx, s.cfg)
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
return err
|
|
}
|
|
if apiKeyResult == nil {
|
|
apiKeyResult = &APIKeyClientResult{}
|
|
}
|
|
}
|
|
|
|
// legacy clients removed; no caches to refresh
|
|
|
|
s.ensureWebsocketGateway()
|
|
if homeEnabled {
|
|
s.registerAvailableExecutors(ctx, executorRegistrationOptions{
|
|
includeBaseline: true,
|
|
})
|
|
// Home mode does not expose in-process Redis RESP usage output; usage is forwarded to home instead.
|
|
redisqueue.SetEnabled(true)
|
|
}
|
|
|
|
// handlers no longer depend on legacy clients; pass nil slice initially
|
|
s.server = api.NewServer(s.cfg, s.coreManager, s.accessManager, s.configPath, s.serverOptions...)
|
|
s.syncPluginRuntimeConfig(ctx)
|
|
if homeEnabled {
|
|
s.syncPluginModelRuntime(ctx)
|
|
}
|
|
|
|
if s.authManager == nil {
|
|
s.authManager = newDefaultAuthManager()
|
|
}
|
|
|
|
if homeEnabled {
|
|
s.startHomeSubscriber(ctx)
|
|
}
|
|
|
|
if s.hooks.OnBeforeStart != nil {
|
|
s.hooks.OnBeforeStart(s.cfg)
|
|
}
|
|
|
|
s.serverErr = make(chan error, 1)
|
|
go func() {
|
|
if errStart := s.server.Start(); errStart != nil {
|
|
s.serverErr <- errStart
|
|
} else {
|
|
s.serverErr <- nil
|
|
}
|
|
}()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
|
|
|
s.applyPprofConfig(s.cfg)
|
|
|
|
if s.hooks.OnAfterStart != nil {
|
|
s.hooks.OnAfterStart(s)
|
|
}
|
|
|
|
if !homeEnabled {
|
|
var watcherWrapper *WatcherWrapper
|
|
reloadCallback := func(newCfg *config.Config) { s.applyWatcherConfigUpdate(newCfg) }
|
|
|
|
watcherWrapper, errCreate := s.watcherFactory(s.configPath, s.cfg.AuthDir, reloadCallback)
|
|
if errCreate != nil {
|
|
return fmt.Errorf("cliproxy: failed to create watcher: %w", errCreate)
|
|
}
|
|
s.watcher = watcherWrapper
|
|
s.ensureAuthUpdateQueue(ctx)
|
|
if s.authUpdates != nil {
|
|
watcherWrapper.SetAuthUpdateQueue(s.authUpdates)
|
|
}
|
|
watcherWrapper.SetConfig(s.cfg)
|
|
s.registerPluginAuthParser()
|
|
|
|
watcherCtx, watcherCancel := context.WithCancel(context.Background())
|
|
s.watcherCancel = watcherCancel
|
|
if errStart := watcherWrapper.Start(watcherCtx); errStart != nil {
|
|
return fmt.Errorf("cliproxy: failed to start watcher: %w", errStart)
|
|
}
|
|
log.Info("file watcher started for config and auth directory changes")
|
|
s.syncPluginModelRuntime(ctx)
|
|
}
|
|
|
|
s.registerModelRefreshCallback()
|
|
|
|
// Prefer core auth manager auto refresh if available.
|
|
if s.coreManager != nil && !homeEnabled {
|
|
interval := 15 * time.Minute
|
|
s.coreManager.StartAutoRefresh(context.Background(), interval)
|
|
log.Infof("core auth auto-refresh started (interval=%s)", interval)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Debug("service context cancelled, shutting down...")
|
|
return ctx.Err()
|
|
case errServer := <-s.serverErr:
|
|
return errServer
|
|
}
|
|
}
|
|
|
|
// Shutdown gracefully stops background workers and the HTTP server.
|
|
// It ensures all resources are properly cleaned up and connections are closed.
|
|
// The shutdown is idempotent and can be called multiple times safely.
|
|
//
|
|
// Parameters:
|
|
// - ctx: The context for controlling the shutdown timeout
|
|
//
|
|
// Returns:
|
|
// - error: An error if shutdown fails
|
|
func (s *Service) Shutdown(ctx context.Context) error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
var shutdownErr error
|
|
s.shutdownOnce.Do(func() {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
s.homeLifecycleMu.Lock()
|
|
if supervisor := s.homeSupervisor; supervisor != nil {
|
|
s.homeConfigCommitMu.Lock()
|
|
supervisor.cancel()
|
|
s.homeConfigCommitMu.Unlock()
|
|
<-supervisor.done
|
|
}
|
|
s.homeMu.Lock()
|
|
homeCancel := s.homeCancel
|
|
homeClient := s.homeClient
|
|
homeRegistry := s.homeRegistry
|
|
homeDispatchBundle := s.homeDispatchBundle
|
|
homeForwarder := s.homeLogForwarder
|
|
homeForwarderClient := s.homeLogForwarderClient
|
|
s.homeGeneration++
|
|
s.homeCancel = nil
|
|
s.homeClient = nil
|
|
s.homeRegistry = nil
|
|
s.homeDispatchBundle = nil
|
|
s.homeDrainBound = 0
|
|
s.homeLogForwarder = nil
|
|
s.homeLogForwarderClient = nil
|
|
s.homeMu.Unlock()
|
|
if s.coreManager != nil {
|
|
s.coreManager.ClearHomeDispatchBundle(homeDispatchBundle)
|
|
}
|
|
home.ClearCurrentIf(homeClient)
|
|
if homeCancel != nil {
|
|
homeCancel()
|
|
}
|
|
if homeRegistry != nil {
|
|
if errClose := homeRegistry.Close(); errClose != nil {
|
|
log.WithError(errClose).Warn("failed to close Home execution registry during shutdown")
|
|
}
|
|
}
|
|
if homeClient != nil {
|
|
homeClient.Close()
|
|
}
|
|
if homeForwarder != nil {
|
|
if homeForwarderClient == homeClient {
|
|
homeForwarder.Deactivate(homeClient)
|
|
}
|
|
homeForwarder.Stop()
|
|
}
|
|
s.homeLifecycleMu.Unlock()
|
|
|
|
// legacy refresh loop removed; only stopping core auth manager below
|
|
|
|
if s.watcherCancel != nil {
|
|
s.watcherCancel()
|
|
}
|
|
if s.coreManager != nil {
|
|
s.coreManager.StopAutoRefresh()
|
|
}
|
|
if s.watcher != nil {
|
|
if err := s.watcher.Stop(); err != nil {
|
|
log.Errorf("failed to stop file watcher: %v", err)
|
|
shutdownErr = err
|
|
}
|
|
}
|
|
if s.wsGateway != nil {
|
|
if err := s.wsGateway.Stop(ctx); err != nil {
|
|
log.Errorf("failed to stop websocket gateway: %v", err)
|
|
if shutdownErr == nil {
|
|
shutdownErr = err
|
|
}
|
|
}
|
|
}
|
|
if s.authQueueStop != nil {
|
|
s.authQueueStop()
|
|
s.authQueueStop = nil
|
|
}
|
|
|
|
if errShutdownPprof := s.shutdownPprof(ctx); errShutdownPprof != nil {
|
|
log.Errorf("failed to stop pprof server: %v", errShutdownPprof)
|
|
if shutdownErr == nil {
|
|
shutdownErr = errShutdownPprof
|
|
}
|
|
}
|
|
|
|
// no legacy clients to persist
|
|
|
|
if s.server != nil {
|
|
shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
if err := s.server.Stop(shutdownCtx); err != nil {
|
|
log.Errorf("error stopping API server: %v", err)
|
|
if shutdownErr == nil {
|
|
shutdownErr = err
|
|
}
|
|
}
|
|
}
|
|
|
|
if s.pluginHost != nil {
|
|
sdktranslator.SetPluginHooks(nil)
|
|
sdkAuth.RegisterPluginAuthParser(nil)
|
|
if s.watcher != nil {
|
|
s.watcher.SetPluginAuthParser(nil)
|
|
}
|
|
s.pluginHost.ApplyConfig(ctx, &config.Config{})
|
|
s.pluginHost.RegisterModels(ctx, registry.GetGlobalRegistry())
|
|
s.registerAvailableExecutors(ctx, executorRegistrationOptions{
|
|
includePlugins: true,
|
|
})
|
|
s.pluginHost.RegisterFrontendAuthProviders()
|
|
s.pluginHost.ShutdownAllContext(ctx)
|
|
if s.accessManager != nil {
|
|
s.accessManager.SetProviders(sdkaccess.RegisteredProviders())
|
|
}
|
|
}
|
|
|
|
usage.StopDefault()
|
|
})
|
|
return shutdownErr
|
|
}
|
|
|
|
func (s *Service) ensureAuthDir() error {
|
|
info, err := os.Stat(s.cfg.AuthDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
if mkErr := os.MkdirAll(s.cfg.AuthDir, 0o755); mkErr != nil {
|
|
return fmt.Errorf("cliproxy: failed to create auth directory %s: %w", s.cfg.AuthDir, mkErr)
|
|
}
|
|
log.Infof("created missing auth directory: %s", s.cfg.AuthDir)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("cliproxy: error checking auth directory %s: %w", s.cfg.AuthDir, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("cliproxy: auth path exists but is not a directory: %s", s.cfg.AuthDir)
|
|
}
|
|
return nil
|
|
}
|