opencode-cursor-agent/internal/server/server.go

181 lines
4.5 KiB
Go
Raw Normal View History

2026-03-30 14:09:15 +00:00
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
}