181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
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
|
|
}
|