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/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}