File size: 6,442 Bytes
6932c8c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | package server
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/charmbracelet/crush/internal/backend"
"github.com/charmbracelet/crush/internal/config"
)
// maxUnixSocketPathLen is the maximum length of a Unix domain socket
// path. The macOS sun_path field is 104 bytes; Linux allows 108. We
// use 104 so the resulting path is portable across both platforms.
const maxUnixSocketPathLen = 104
// socketDir returns the directory used for the Crush Unix socket.
// It prefers $XDG_RUNTIME_DIR when set (systemd's per-user runtime
// directory on Linux), and otherwise falls back to [os.TempDir],
// which resolves to the per-user private $TMPDIR on macOS and to
// /tmp on Linux.
func socketDir() string {
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
return dir
}
return os.TempDir()
}
// ErrServerClosed is returned when the server is closed.
var ErrServerClosed = http.ErrServerClosed
// ParseHostURL parses a host URL into a [url.URL].
func ParseHostURL(host string) (*url.URL, error) {
proto, addr, ok := strings.Cut(host, "://")
if !ok {
return nil, fmt.Errorf("invalid host format: %s", host)
}
var basePath string
if proto == "tcp" {
parsed, err := url.Parse("tcp://" + addr)
if err != nil {
return nil, fmt.Errorf("invalid tcp address: %v", err)
}
addr = parsed.Host
basePath = parsed.Path
}
return &url.URL{
Scheme: proto,
Host: addr,
Path: basePath,
}, nil
}
// DefaultHost returns the default server host.
//
// On Windows the address is a named pipe under \\.\pipe\. On Unix
// platforms the socket lives in the per-user runtime directory
// returned by [socketDir] and is named crush-<uid>.sock, falling
// back to crush.sock when the current uid cannot be determined. If
// the composed path would exceed [maxUnixSocketPathLen] bytes (the
// macOS sun_path limit), we fall back to /tmp/crush-<uid>.sock so
// the socket remains bindable.
func DefaultHost() string {
sock := "crush.sock"
usr, err := user.Current()
if err == nil && usr.Uid != "" {
sock = fmt.Sprintf("crush-%s.sock", usr.Uid)
}
if runtime.GOOS == "windows" {
return fmt.Sprintf("npipe:////./pipe/%s", sock)
}
path := filepath.Join(socketDir(), sock)
if len(path) > maxUnixSocketPathLen {
path = filepath.Join("/tmp", sock)
}
return "unix://" + path
}
// Server represents a Crush server bound to a specific address.
type Server struct {
// Addr can be a TCP address, a Unix socket path, or a Windows named pipe.
Addr string
network string
h *http.Server
ln net.Listener
backend *backend.Backend
logger *slog.Logger
}
// SetLogger sets the logger for the server.
func (s *Server) SetLogger(logger *slog.Logger) {
s.logger = logger
}
// Backend returns the server's backend. Intended for integration tests
// that drive lifecycle transitions (detach, grace tuning) against a live
// HTTP surface.
func (s *Server) Backend() *backend.Backend {
return s.backend
}
// DefaultServer returns a new [Server] with the default address.
func DefaultServer(cfg *config.ConfigStore) *Server {
hostURL, err := ParseHostURL(DefaultHost())
if err != nil {
panic("invalid default host")
}
return NewServer(cfg, hostURL.Scheme, hostURL.Host)
}
// NewServer creates a new [Server] with the given network and address.
func NewServer(cfg *config.ConfigStore, network, address string) *Server {
s := new(Server)
s.Addr = address
s.network = network
// The backend is created with a shutdown callback that triggers
// a graceful server shutdown (e.g. when the last workspace is
// removed).
s.backend = backend.New(context.Background(), cfg, func() {
go func() {
slog.Info("Shutting down server...")
if err := s.Shutdown(context.Background()); err != nil {
slog.Error("Failed to shutdown server", "error", err)
}
}()
})
s.installHandler()
if network == "tcp" {
s.h.Addr = address
}
return s
}
// installHandler builds the protocol/router around s.backend and
// assigns the resulting http.Server to s.h. Extracted from
// [NewServer] so test harnesses can wire a Server around a
// pre-constructed backend.
func (s *Server) installHandler() {
var p http.Protocols
p.SetHTTP1(true)
p.SetUnencryptedHTTP2(true)
c := &controllerV1{backend: s.backend, server: s}
mux := http.NewServeMux()
for _, e := range c.endpoints() {
mux.HandleFunc(e.Method()+" "+e.Path(), e.Handler())
}
mux.HandleFunc("GET /v1/docs/", c.handleDocsIndex)
mux.HandleFunc("GET /v1/docs/openapi.json", c.handleDocsSpec)
s.h = &http.Server{
Protocols: &p,
Handler: s.recoverHandler(s.loggingHandler(mux)),
}
}
// Handler returns the server's HTTP handler. Exposed so test harnesses
// can wrap it in an httptest.Server without going through the
// production listener setup.
func (s *Server) Handler() http.Handler {
return s.h.Handler
}
// Serve accepts incoming connections on the listener.
func (s *Server) Serve(ln net.Listener) error {
return s.h.Serve(ln)
}
// ListenAndServe starts the server and begins accepting connections.
func (s *Server) ListenAndServe() error {
if s.ln != nil {
return fmt.Errorf("server already started")
}
ln, removedStale, err := listen(s.network, s.Addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %w", s.Addr, err)
}
if removedStale && s.logger != nil {
s.logger.Warn("Removed stale socket before binding", "address", s.Addr)
}
return s.Serve(ln)
}
func (s *Server) closeListener() {
if s.ln != nil {
s.ln.Close()
s.ln = nil
}
}
// Close force closes all listeners and connections.
func (s *Server) Close() error {
defer func() { s.closeListener() }()
return s.h.Close()
}
// Shutdown gracefully shuts down the server without interrupting active
// connections.
func (s *Server) Shutdown(ctx context.Context) error {
defer func() { s.closeListener() }()
return s.h.Shutdown(ctx)
}
func (s *Server) logDebug(r *http.Request, msg string, args ...any) {
if s.logger != nil {
s.logger.With(
slog.String("method", r.Method),
slog.String("url", r.URL.String()),
slog.String("remote_addr", r.RemoteAddr),
).Debug(msg, args...)
}
}
func (s *Server) logError(r *http.Request, msg string, args ...any) {
if s.logger != nil {
s.logger.With(
slog.String("method", r.Method),
slog.String("url", r.URL.String()),
slog.String("remote_addr", r.RemoteAddr),
).Error(msg, args...)
}
}
|