Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
369
backend/sdk/cliproxy/service_lifecycle.go
Normal file
369
backend/sdk/cliproxy/service_lifecycle.go
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
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.server != nil && s.wsGateway != nil {
|
||||
s.server.AttachWebsocketRoute(s.wsGateway.Path(), s.wsGateway.Handler())
|
||||
s.server.SetWebsocketAuthChangeHandler(func(oldEnabled, newEnabled bool) {
|
||||
if oldEnabled == newEnabled {
|
||||
return
|
||||
}
|
||||
if !oldEnabled && newEnabled {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if errStop := s.wsGateway.Stop(ctx); errStop != nil {
|
||||
log.Warnf("failed to reset websocket connections after ws-auth change %t -> %t: %v", oldEnabled, newEnabled, errStop)
|
||||
return
|
||||
}
|
||||
log.Debugf("ws-auth enabled; existing websocket sessions terminated to enforce authentication")
|
||||
return
|
||||
}
|
||||
log.Debugf("ws-auth disabled; existing websocket sessions remain connected")
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in a new issue