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}