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}