1package server
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net"
8 "net/http"
9 "net/url"
10 "os/user"
11 "runtime"
12 "strings"
13
14 "github.com/charmbracelet/crush/internal/backend"
15 "github.com/charmbracelet/crush/internal/config"
16)
17
18// ErrServerClosed is returned when the server is closed.
19var ErrServerClosed = http.ErrServerClosed
20
21// ParseHostURL parses a host URL into a [url.URL].
22func ParseHostURL(host string) (*url.URL, error) {
23 proto, addr, ok := strings.Cut(host, "://")
24 if !ok {
25 return nil, fmt.Errorf("invalid host format: %s", host)
26 }
27
28 var basePath string
29 if proto == "tcp" {
30 parsed, err := url.Parse("tcp://" + addr)
31 if err != nil {
32 return nil, fmt.Errorf("invalid tcp address: %v", err)
33 }
34 addr = parsed.Host
35 basePath = parsed.Path
36 }
37 return &url.URL{
38 Scheme: proto,
39 Host: addr,
40 Path: basePath,
41 }, nil
42}
43
44// DefaultHost returns the default server host.
45func DefaultHost() string {
46 sock := "crush.sock"
47 usr, err := user.Current()
48 if err == nil && usr.Uid != "" {
49 sock = fmt.Sprintf("crush-%s.sock", usr.Uid)
50 }
51 if runtime.GOOS == "windows" {
52 return fmt.Sprintf("npipe:////./pipe/%s", sock)
53 }
54 return fmt.Sprintf("unix:///tmp/%s", sock)
55}
56
57// Server represents a Crush server bound to a specific address.
58type Server struct {
59 // Addr can be a TCP address, a Unix socket path, or a Windows named pipe.
60 Addr string
61 network string
62
63 h *http.Server
64 ln net.Listener
65
66 backend *backend.Backend
67 logger *slog.Logger
68}
69
70// SetLogger sets the logger for the server.
71func (s *Server) SetLogger(logger *slog.Logger) {
72 s.logger = logger
73}
74
75// DefaultServer returns a new [Server] with the default address.
76func DefaultServer(cfg *config.ConfigStore) *Server {
77 hostURL, err := ParseHostURL(DefaultHost())
78 if err != nil {
79 panic("invalid default host")
80 }
81 return NewServer(cfg, hostURL.Scheme, hostURL.Host)
82}
83
84// NewServer creates a new [Server] with the given network and address.
85func NewServer(cfg *config.ConfigStore, network, address string) *Server {
86 s := new(Server)
87 s.Addr = address
88 s.network = network
89
90 // The backend is created with a shutdown callback that triggers
91 // a graceful server shutdown (e.g. when the last workspace is
92 // removed).
93 s.backend = backend.New(context.Background(), cfg, func() {
94 go func() {
95 slog.Info("Shutting down server...")
96 if err := s.Shutdown(context.Background()); err != nil {
97 slog.Error("Failed to shutdown server", "error", err)
98 }
99 }()
100 })
101
102 var p http.Protocols
103 p.SetHTTP1(true)
104 p.SetUnencryptedHTTP2(true)
105 c := &controllerV1{backend: s.backend, server: s}
106 mux := http.NewServeMux()
107 mux.HandleFunc("GET /v1/health", c.handleGetHealth)
108 mux.HandleFunc("GET /v1/version", c.handleGetVersion)
109 mux.HandleFunc("GET /v1/config", c.handleGetConfig)
110 mux.HandleFunc("POST /v1/control", c.handlePostControl)
111 mux.HandleFunc("GET /v1/workspaces", c.handleGetWorkspaces)
112 mux.HandleFunc("POST /v1/workspaces", c.handlePostWorkspaces)
113 mux.HandleFunc("DELETE /v1/workspaces/{id}", c.handleDeleteWorkspaces)
114 mux.HandleFunc("GET /v1/workspaces/{id}", c.handleGetWorkspace)
115 mux.HandleFunc("GET /v1/workspaces/{id}/config", c.handleGetWorkspaceConfig)
116 mux.HandleFunc("GET /v1/workspaces/{id}/events", c.handleGetWorkspaceEvents)
117 mux.HandleFunc("GET /v1/workspaces/{id}/providers", c.handleGetWorkspaceProviders)
118 mux.HandleFunc("GET /v1/workspaces/{id}/sessions", c.handleGetWorkspaceSessions)
119 mux.HandleFunc("POST /v1/workspaces/{id}/sessions", c.handlePostWorkspaceSessions)
120 mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}", c.handleGetWorkspaceSession)
121 mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}/history", c.handleGetWorkspaceSessionHistory)
122 mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}/messages", c.handleGetWorkspaceSessionMessages)
123 mux.HandleFunc("GET /v1/workspaces/{id}/lsps", c.handleGetWorkspaceLSPs)
124 mux.HandleFunc("GET /v1/workspaces/{id}/lsps/{lsp}/diagnostics", c.handleGetWorkspaceLSPDiagnostics)
125 mux.HandleFunc("GET /v1/workspaces/{id}/permissions/skip", c.handleGetWorkspacePermissionsSkip)
126 mux.HandleFunc("POST /v1/workspaces/{id}/permissions/skip", c.handlePostWorkspacePermissionsSkip)
127 mux.HandleFunc("POST /v1/workspaces/{id}/permissions/grant", c.handlePostWorkspacePermissionsGrant)
128 mux.HandleFunc("GET /v1/workspaces/{id}/agent", c.handleGetWorkspaceAgent)
129 mux.HandleFunc("POST /v1/workspaces/{id}/agent", c.handlePostWorkspaceAgent)
130 mux.HandleFunc("POST /v1/workspaces/{id}/agent/init", c.handlePostWorkspaceAgentInit)
131 mux.HandleFunc("POST /v1/workspaces/{id}/agent/update", c.handlePostWorkspaceAgentUpdate)
132 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}", c.handleGetWorkspaceAgentSession)
133 mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/cancel", c.handlePostWorkspaceAgentSessionCancel)
134 mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued)
135 mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/prompts/clear", c.handlePostWorkspaceAgentSessionPromptClear)
136 mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handleGetWorkspaceAgentSessionSummarize)
137 s.h = &http.Server{
138 Protocols: &p,
139 Handler: s.loggingHandler(mux),
140 }
141 if network == "tcp" {
142 s.h.Addr = address
143 }
144 return s
145}
146
147// Serve accepts incoming connections on the listener.
148func (s *Server) Serve(ln net.Listener) error {
149 return s.h.Serve(ln)
150}
151
152// ListenAndServe starts the server and begins accepting connections.
153func (s *Server) ListenAndServe() error {
154 if s.ln != nil {
155 return fmt.Errorf("server already started")
156 }
157 ln, err := listen(s.network, s.Addr)
158 if err != nil {
159 return fmt.Errorf("failed to listen on %s: %w", s.Addr, err)
160 }
161 return s.Serve(ln)
162}
163
164func (s *Server) closeListener() {
165 if s.ln != nil {
166 s.ln.Close()
167 s.ln = nil
168 }
169}
170
171// Close force closes all listeners and connections.
172func (s *Server) Close() error {
173 defer func() { s.closeListener() }()
174 return s.h.Close()
175}
176
177// Shutdown gracefully shuts down the server without interrupting active
178// connections.
179func (s *Server) Shutdown(ctx context.Context) error {
180 defer func() { s.closeListener() }()
181 return s.h.Shutdown(ctx)
182}
183
184func (s *Server) logDebug(r *http.Request, msg string, args ...any) {
185 if s.logger != nil {
186 s.logger.With(
187 slog.String("method", r.Method),
188 slog.String("url", r.URL.String()),
189 slog.String("remote_addr", r.RemoteAddr),
190 ).Debug(msg, args...)
191 }
192}
193
194func (s *Server) logError(r *http.Request, msg string, args ...any) {
195 if s.logger != nil {
196 s.logger.With(
197 slog.String("method", r.Method),
198 slog.String("url", r.URL.String()),
199 slog.String("remote_addr", r.RemoteAddr),
200 ).Error(msg, args...)
201 }
202}