server.go

  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/swagger"
 15	"github.com/charmbracelet/crush/internal/backend"
 16	"github.com/charmbracelet/crush/internal/config"
 17	httpswagger "github.com/swaggo/http-swagger/v2"
 18)
 19
 20// ErrServerClosed is returned when the server is closed.
 21var ErrServerClosed = http.ErrServerClosed
 22
 23// ParseHostURL parses a host URL into a [url.URL].
 24func ParseHostURL(host string) (*url.URL, error) {
 25	proto, addr, ok := strings.Cut(host, "://")
 26	if !ok {
 27		return nil, fmt.Errorf("invalid host format: %s", host)
 28	}
 29
 30	var basePath string
 31	if proto == "tcp" {
 32		parsed, err := url.Parse("tcp://" + addr)
 33		if err != nil {
 34			return nil, fmt.Errorf("invalid tcp address: %v", err)
 35		}
 36		addr = parsed.Host
 37		basePath = parsed.Path
 38	}
 39	return &url.URL{
 40		Scheme: proto,
 41		Host:   addr,
 42		Path:   basePath,
 43	}, nil
 44}
 45
 46// DefaultHost returns the default server host.
 47func DefaultHost() string {
 48	sock := "crush.sock"
 49	usr, err := user.Current()
 50	if err == nil && usr.Uid != "" {
 51		sock = fmt.Sprintf("crush-%s.sock", usr.Uid)
 52	}
 53	if runtime.GOOS == "windows" {
 54		return fmt.Sprintf("npipe:////./pipe/%s", sock)
 55	}
 56	return fmt.Sprintf("unix:///tmp/%s", sock)
 57}
 58
 59// Server represents a Crush server bound to a specific address.
 60type Server struct {
 61	// Addr can be a TCP address, a Unix socket path, or a Windows named pipe.
 62	Addr    string
 63	network string
 64
 65	h  *http.Server
 66	ln net.Listener
 67
 68	backend *backend.Backend
 69	logger  *slog.Logger
 70}
 71
 72// SetLogger sets the logger for the server.
 73func (s *Server) SetLogger(logger *slog.Logger) {
 74	s.logger = logger
 75}
 76
 77// DefaultServer returns a new [Server] with the default address.
 78func DefaultServer(cfg *config.ConfigStore) *Server {
 79	hostURL, err := ParseHostURL(DefaultHost())
 80	if err != nil {
 81		panic("invalid default host")
 82	}
 83	return NewServer(cfg, hostURL.Scheme, hostURL.Host)
 84}
 85
 86// NewServer creates a new [Server] with the given network and address.
 87func NewServer(cfg *config.ConfigStore, network, address string) *Server {
 88	s := new(Server)
 89	s.Addr = address
 90	s.network = network
 91
 92	// The backend is created with a shutdown callback that triggers
 93	// a graceful server shutdown (e.g. when the last workspace is
 94	// removed).
 95	s.backend = backend.New(context.Background(), cfg, func() {
 96		go func() {
 97			slog.Info("Shutting down server...")
 98			if err := s.Shutdown(context.Background()); err != nil {
 99				slog.Error("Failed to shutdown server", "error", err)
100			}
101		}()
102	})
103
104	var p http.Protocols
105	p.SetHTTP1(true)
106	p.SetUnencryptedHTTP2(true)
107	c := &controllerV1{backend: s.backend, server: s}
108	mux := http.NewServeMux()
109	mux.HandleFunc("GET /v1/health", c.handleGetHealth)
110	mux.HandleFunc("GET /v1/version", c.handleGetVersion)
111	mux.HandleFunc("GET /v1/config", c.handleGetConfig)
112	mux.HandleFunc("POST /v1/control", c.handlePostControl)
113	mux.HandleFunc("GET /v1/workspaces", c.handleGetWorkspaces)
114	mux.HandleFunc("POST /v1/workspaces", c.handlePostWorkspaces)
115	mux.HandleFunc("DELETE /v1/workspaces/{id}", c.handleDeleteWorkspaces)
116	mux.HandleFunc("GET /v1/workspaces/{id}", c.handleGetWorkspace)
117	mux.HandleFunc("GET /v1/workspaces/{id}/config", c.handleGetWorkspaceConfig)
118	mux.HandleFunc("GET /v1/workspaces/{id}/events", c.handleGetWorkspaceEvents)
119	mux.HandleFunc("GET /v1/workspaces/{id}/providers", c.handleGetWorkspaceProviders)
120	mux.HandleFunc("GET /v1/workspaces/{id}/sessions", c.handleGetWorkspaceSessions)
121	mux.HandleFunc("POST /v1/workspaces/{id}/sessions", c.handlePostWorkspaceSessions)
122	mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}", c.handleGetWorkspaceSession)
123	mux.HandleFunc("PUT /v1/workspaces/{id}/sessions/{sid}", c.handlePutWorkspaceSession)
124	mux.HandleFunc("DELETE /v1/workspaces/{id}/sessions/{sid}", c.handleDeleteWorkspaceSession)
125	mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}/history", c.handleGetWorkspaceSessionHistory)
126	mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}/messages", c.handleGetWorkspaceSessionMessages)
127	mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}/messages/user", c.handleGetWorkspaceSessionUserMessages)
128	mux.HandleFunc("GET /v1/workspaces/{id}/messages/user", c.handleGetWorkspaceAllUserMessages)
129	mux.HandleFunc("GET /v1/workspaces/{id}/sessions/{sid}/filetracker/files", c.handleGetWorkspaceSessionFileTrackerFiles)
130	mux.HandleFunc("POST /v1/workspaces/{id}/filetracker/read", c.handlePostWorkspaceFileTrackerRead)
131	mux.HandleFunc("GET /v1/workspaces/{id}/filetracker/lastread", c.handleGetWorkspaceFileTrackerLastRead)
132	mux.HandleFunc("GET /v1/workspaces/{id}/lsps", c.handleGetWorkspaceLSPs)
133	mux.HandleFunc("GET /v1/workspaces/{id}/lsps/{lsp}/diagnostics", c.handleGetWorkspaceLSPDiagnostics)
134	mux.HandleFunc("POST /v1/workspaces/{id}/lsps/start", c.handlePostWorkspaceLSPStart)
135	mux.HandleFunc("POST /v1/workspaces/{id}/lsps/stop", c.handlePostWorkspaceLSPStopAll)
136	mux.HandleFunc("GET /v1/workspaces/{id}/permissions/skip", c.handleGetWorkspacePermissionsSkip)
137	mux.HandleFunc("POST /v1/workspaces/{id}/permissions/skip", c.handlePostWorkspacePermissionsSkip)
138	mux.HandleFunc("POST /v1/workspaces/{id}/permissions/grant", c.handlePostWorkspacePermissionsGrant)
139	mux.HandleFunc("GET /v1/workspaces/{id}/agent", c.handleGetWorkspaceAgent)
140	mux.HandleFunc("POST /v1/workspaces/{id}/agent", c.handlePostWorkspaceAgent)
141	mux.HandleFunc("POST /v1/workspaces/{id}/agent/init", c.handlePostWorkspaceAgentInit)
142	mux.HandleFunc("POST /v1/workspaces/{id}/agent/update", c.handlePostWorkspaceAgentUpdate)
143	mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}", c.handleGetWorkspaceAgentSession)
144	mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/cancel", c.handlePostWorkspaceAgentSessionCancel)
145	mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/queued", c.handleGetWorkspaceAgentSessionPromptQueued)
146	mux.HandleFunc("GET /v1/workspaces/{id}/agent/sessions/{sid}/prompts/list", c.handleGetWorkspaceAgentSessionPromptList)
147	mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/prompts/clear", c.handlePostWorkspaceAgentSessionPromptClear)
148	mux.HandleFunc("POST /v1/workspaces/{id}/agent/sessions/{sid}/summarize", c.handlePostWorkspaceAgentSessionSummarize)
149	mux.HandleFunc("GET /v1/workspaces/{id}/agent/default-small-model", c.handleGetWorkspaceAgentDefaultSmallModel)
150	mux.HandleFunc("POST /v1/workspaces/{id}/config/set", c.handlePostWorkspaceConfigSet)
151	mux.HandleFunc("POST /v1/workspaces/{id}/config/remove", c.handlePostWorkspaceConfigRemove)
152	mux.HandleFunc("POST /v1/workspaces/{id}/config/model", c.handlePostWorkspaceConfigModel)
153	mux.HandleFunc("POST /v1/workspaces/{id}/config/compact", c.handlePostWorkspaceConfigCompact)
154	mux.HandleFunc("POST /v1/workspaces/{id}/config/provider-key", c.handlePostWorkspaceConfigProviderKey)
155	mux.HandleFunc("POST /v1/workspaces/{id}/config/import-copilot", c.handlePostWorkspaceConfigImportCopilot)
156	mux.HandleFunc("POST /v1/workspaces/{id}/config/refresh-oauth", c.handlePostWorkspaceConfigRefreshOAuth)
157	mux.HandleFunc("GET /v1/workspaces/{id}/project/needs-init", c.handleGetWorkspaceProjectNeedsInit)
158	mux.HandleFunc("POST /v1/workspaces/{id}/project/init", c.handlePostWorkspaceProjectInit)
159	mux.HandleFunc("GET /v1/workspaces/{id}/project/init-prompt", c.handleGetWorkspaceProjectInitPrompt)
160	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-tools", c.handlePostWorkspaceMCPRefreshTools)
161	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/read-resource", c.handlePostWorkspaceMCPReadResource)
162	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/get-prompt", c.handlePostWorkspaceMCPGetPrompt)
163	mux.HandleFunc("GET /v1/workspaces/{id}/mcp/states", c.handleGetWorkspaceMCPStates)
164	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-prompts", c.handlePostWorkspaceMCPRefreshPrompts)
165	mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-resources", c.handlePostWorkspaceMCPRefreshResources)
166	mux.Handle("/v1/docs/", httpswagger.WrapHandler)
167	s.h = &http.Server{
168		Protocols: &p,
169		Handler:   s.loggingHandler(mux),
170	}
171	if network == "tcp" {
172		s.h.Addr = address
173	}
174	return s
175}
176
177// Serve accepts incoming connections on the listener.
178func (s *Server) Serve(ln net.Listener) error {
179	return s.h.Serve(ln)
180}
181
182// ListenAndServe starts the server and begins accepting connections.
183func (s *Server) ListenAndServe() error {
184	if s.ln != nil {
185		return fmt.Errorf("server already started")
186	}
187	ln, err := listen(s.network, s.Addr)
188	if err != nil {
189		return fmt.Errorf("failed to listen on %s: %w", s.Addr, err)
190	}
191	return s.Serve(ln)
192}
193
194func (s *Server) closeListener() {
195	if s.ln != nil {
196		s.ln.Close()
197		s.ln = nil
198	}
199}
200
201// Close force closes all listeners and connections.
202func (s *Server) Close() error {
203	defer func() { s.closeListener() }()
204	return s.h.Close()
205}
206
207// Shutdown gracefully shuts down the server without interrupting active
208// connections.
209func (s *Server) Shutdown(ctx context.Context) error {
210	defer func() { s.closeListener() }()
211	return s.h.Shutdown(ctx)
212}
213
214func (s *Server) logDebug(r *http.Request, msg string, args ...any) {
215	if s.logger != nil {
216		s.logger.With(
217			slog.String("method", r.Method),
218			slog.String("url", r.URL.String()),
219			slog.String("remote_addr", r.RemoteAddr),
220		).Debug(msg, args...)
221	}
222}
223
224func (s *Server) logError(r *http.Request, msg string, args ...any) {
225	if s.logger != nil {
226		s.logger.With(
227			slog.String("method", r.Method),
228			slog.String("url", r.URL.String()),
229			slog.String("remote_addr", r.RemoteAddr),
230		).Error(msg, args...)
231	}
232}