server.go

 1package cmd
 2
 3import (
 4	"context"
 5	"errors"
 6	"fmt"
 7	"log/slog"
 8	"os"
 9	"os/signal"
10	"path/filepath"
11	"time"
12
13	"github.com/charmbracelet/crush/internal/config"
14	crushlog "github.com/charmbracelet/crush/internal/log"
15	"github.com/charmbracelet/crush/internal/server"
16	"github.com/charmbracelet/x/term"
17	"github.com/spf13/cobra"
18)
19
20var serverHost string
21
22func init() {
23	serverCmd.Flags().StringVarP(&serverHost, "host", "H", server.DefaultHost(), "Server host (TCP or Unix socket)")
24	rootCmd.AddCommand(serverCmd)
25}
26
27var serverCmd = &cobra.Command{
28	Use:   "server",
29	Short: "Start the Crush server",
30	RunE: func(cmd *cobra.Command, _ []string) error {
31		dataDir, err := cmd.Flags().GetString("data-dir")
32		if err != nil {
33			return fmt.Errorf("failed to get data directory: %v", err)
34		}
35		debug, err := cmd.Flags().GetBool("debug")
36		if err != nil {
37			return fmt.Errorf("failed to get debug flag: %v", err)
38		}
39
40		cfg, err := config.Load(config.GlobalWorkspaceDir(), dataDir, debug)
41		if err != nil {
42			return fmt.Errorf("failed to load configuration: %v", err)
43		}
44
45		logFile := filepath.Join(config.GlobalCacheDir(), "server-"+safeNameRegexp.ReplaceAllString(serverHost, "_"), "crush.log")
46
47		if term.IsTerminal(os.Stderr.Fd()) {
48			crushlog.Setup(logFile, debug, os.Stderr)
49		} else {
50			crushlog.Setup(logFile, debug)
51		}
52
53		hostURL, err := server.ParseHostURL(serverHost)
54		if err != nil {
55			return fmt.Errorf("invalid server host: %v", err)
56		}
57
58		srv := server.NewServer(cfg, hostURL.Scheme, hostURL.Host)
59		srv.SetLogger(slog.Default())
60		slog.Info("Starting Crush server...", "addr", serverHost)
61
62		errch := make(chan error, 1)
63		sigch := make(chan os.Signal, 1)
64		sigs := []os.Signal{os.Interrupt}
65		sigs = append(sigs, addSignals(sigs)...)
66		signal.Notify(sigch, sigs...)
67
68		go func() {
69			errch <- srv.ListenAndServe()
70		}()
71
72		select {
73		case <-sigch:
74			slog.Info("Received interrupt signal...")
75		case err = <-errch:
76			if err != nil && !errors.Is(err, server.ErrServerClosed) {
77				_ = srv.Close()
78				slog.Error("Server error", "error", err)
79				return fmt.Errorf("server error: %v", err)
80			}
81		}
82
83		if errors.Is(err, server.ErrServerClosed) {
84			return nil
85		}
86
87		ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
88		defer cancel()
89
90		slog.Info("Shutting down...")
91
92		if err := srv.Shutdown(ctx); err != nil {
93			slog.Error("Failed to shutdown server", "error", err)
94			return fmt.Errorf("failed to shutdown server: %v", err)
95		}
96
97		return nil
98	},
99}