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