package server import ( "context" "crypto/tls" "cursor-api-proxy/internal/config" "cursor-api-proxy/internal/handlers" "cursor-api-proxy/internal/pool" "cursor-api-proxy/internal/process" "cursor-api-proxy/internal/router" "fmt" "net/http" "os" "os/signal" "syscall" "time" ) type ServerOptions struct { Version string Config config.BridgeConfig } func StartBridgeServer(opts ServerOptions) []*http.Server { cfg := opts.Config var servers []*http.Server if len(cfg.ConfigDirs) > 0 { if cfg.MultiPort { for i, dir := range cfg.ConfigDirs { port := cfg.Port + i subCfg := cfg subCfg.Port = port subCfg.ConfigDirs = []string{dir} subCfg.MultiPort = false pool.InitAccountPool([]string{dir}) srv := startSingleServer(ServerOptions{Version: opts.Version, Config: subCfg}) servers = append(servers, srv) } return servers } pool.InitAccountPool(cfg.ConfigDirs) } servers = append(servers, startSingleServer(opts)) return servers } func startSingleServer(opts ServerOptions) *http.Server { cfg := opts.Config modelCache := &handlers.ModelCacheRef{} lastModel := cfg.DefaultModel handler := router.NewRouter(router.RouterOptions{ Version: opts.Version, Config: cfg, ModelCache: modelCache, LastModel: &lastModel, }) handler = router.WrapWithRecovery(cfg.SessionsLogPath, handler) useTLS := cfg.TLSCertPath != "" && cfg.TLSKeyPath != "" srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), Handler: handler, } if useTLS { cert, err := tls.LoadX509KeyPair(cfg.TLSCertPath, cfg.TLSKeyPath) if err != nil { fmt.Fprintf(os.Stderr, "TLS error: %v\n", err) os.Exit(1) } srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}} } scheme := "http" if useTLS { scheme = "https" } go func() { var err error if useTLS { err = srv.ListenAndServeTLS("", "") } else { err = srv.ListenAndServe() } if err != nil && err != http.ErrServerClosed { if isAddrInUse(err) { fmt.Fprintf(os.Stderr, "❌ Port %d is already in use. Set CURSOR_BRIDGE_PORT to use a different port.\n", cfg.Port) } else { fmt.Fprintf(os.Stderr, "❌ Server error: %v\n", err) } os.Exit(1) } }() fmt.Printf("cursor-api-proxy listening on %s://%s:%d\n", scheme, cfg.Host, cfg.Port) fmt.Printf("- agent bin: %s\n", cfg.AgentBin) fmt.Printf("- workspace: %s\n", cfg.Workspace) fmt.Printf("- mode: %s\n", cfg.Mode) fmt.Printf("- default model: %s\n", cfg.DefaultModel) fmt.Printf("- force: %v\n", cfg.Force) fmt.Printf("- approve mcps: %v\n", cfg.ApproveMcps) fmt.Printf("- required api key: %v\n", cfg.RequiredKey != "") fmt.Printf("- sessions log: %s\n", cfg.SessionsLogPath) if cfg.ChatOnlyWorkspace { fmt.Println("- chat-only workspace: yes (isolated temp dir)") } else { fmt.Println("- chat-only workspace: no") } if cfg.Verbose { fmt.Println("- verbose traffic: yes (CURSOR_BRIDGE_VERBOSE=true)") } else { fmt.Println("- verbose traffic: no") } if cfg.MaxMode { fmt.Println("- max mode: yes (CURSOR_BRIDGE_MAX_MODE=true)") } else { fmt.Println("- max mode: no") } fmt.Printf("- Windows cmdline budget: %d (prompt tail truncation when over limit; Windows only)\n", cfg.WinCmdlineMax) if len(cfg.ConfigDirs) > 0 { fmt.Printf("- account pool: enabled with %d configuration directories\n", len(cfg.ConfigDirs)) } return srv } func SetupGracefulShutdown(servers []*http.Server, timeoutMs int) { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) go func() { sig := <-sigCh fmt.Printf("\n[%s] %s received — shutting down gracefully…\n", time.Now().UTC().Format(time.RFC3339), sig) process.KillAllChildProcesses() ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond) defer cancel() done := make(chan struct{}) go func() { for _, srv := range servers { _ = srv.Shutdown(ctx) } close(done) }() select { case <-done: os.Exit(0) case <-ctx.Done(): fmt.Fprintln(os.Stderr, "[shutdown] Timed out waiting for connections to drain — forcing exit.") os.Exit(1) } }() } func isAddrInUse(err error) bool { return err != nil && (contains(err.Error(), "address already in use") || contains(err.Error(), "bind: address already in use")) } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsHelper(s, sub)) } func containsHelper(s, sub string) bool { for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return true } } return false }