serve.go
5147 bytes
1package serve
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "os"
8 "os/signal"
9 "path/filepath"
10 "strconv"
11 "syscall"
12 "time"
13
14 "github.com/charmbracelet/soft-serve/cmd"
15 "github.com/charmbracelet/soft-serve/pkg/backend"
16 "github.com/charmbracelet/soft-serve/pkg/config"
17 "github.com/charmbracelet/soft-serve/pkg/db"
18 "github.com/charmbracelet/soft-serve/pkg/db/migrate"
19 "github.com/spf13/cobra"
20)
21
22var (
23 syncHooks bool
24
25 // Command is the serve command.
26 Command = &cobra.Command{
27 Use: "serve",
28 Short: "Start the server",
29 Args: cobra.NoArgs,
30 PersistentPreRunE: cmd.InitBackendContext,
31 PersistentPostRunE: cmd.CloseDBContext,
32 RunE: func(c *cobra.Command, _ []string) error {
33 ctx := c.Context()
34 cfg := config.DefaultConfig()
35 if cfg.Exist() {
36 if err := cfg.ParseFile(); err != nil {
37 return fmt.Errorf("parse config file: %w", err)
38 }
39 } else {
40 if err := cfg.WriteConfig(); err != nil {
41 return fmt.Errorf("write config file: %w", err)
42 }
43 }
44
45 if err := cfg.ParseEnv(); err != nil {
46 return fmt.Errorf("parse environment variables: %w", err)
47 }
48
49 // Create custom hooks directory if it doesn't exist
50 customHooksPath := filepath.Join(cfg.DataPath, "hooks")
51 if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
52 os.MkdirAll(customHooksPath, os.ModePerm) //nolint: errcheck
53 // Generate update hook example without executable permissions
54 hookPath := filepath.Join(customHooksPath, "update.sample")
55 //nolint: gosec
56 if err := os.WriteFile(hookPath, []byte(updateHookExample), 0o744); err != nil {
57 return fmt.Errorf("failed to generate update hook example: %w", err)
58 }
59 }
60
61 // Create log directory if it doesn't exist
62 logPath := filepath.Join(cfg.DataPath, "log")
63 if _, err := os.Stat(logPath); err != nil && os.IsNotExist(err) {
64 os.MkdirAll(logPath, os.ModePerm) //nolint: errcheck
65 }
66
67 db := db.FromContext(ctx)
68 if err := migrate.Migrate(ctx, db); err != nil {
69 return fmt.Errorf("migration error: %w", err)
70 }
71
72 s, err := NewServer(ctx)
73 if err != nil {
74 return fmt.Errorf("start server: %w", err)
75 }
76
77 if syncHooks {
78 be := backend.FromContext(ctx)
79 if err := cmd.InitializeHooks(ctx, cfg, be); err != nil {
80 return fmt.Errorf("initialize hooks: %w", err)
81 }
82 }
83
84 lch := make(chan error, 1)
85 done := make(chan os.Signal, 1)
86 testStop := make(chan struct{})
87
88 signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
89
90 // This endpoint is added for testing purposes
91 // It allows us to stop the server from the test suite.
92 // This is needed since Windows doesn't support signals.
93 if testRun, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_TESTRUN")); testRun {
94 h := s.HTTPServer.Server.Handler
95 s.HTTPServer.Server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
96 if r.URL.Path == "/__stop" && r.Method == http.MethodHead {
97 close(testStop)
98 return
99 }
100 h.ServeHTTP(w, r)
101 })
102 }
103
104 go func() {
105 lch <- s.Start()
106 }()
107
108 for {
109 select {
110 case err := <-lch:
111 if err != nil {
112 return fmt.Errorf("server error: %w", err)
113 }
114 // Server exited cleanly; shut down
115 goto shutdown
116 case sig := <-done:
117 if sig == syscall.SIGHUP {
118 s.logger.Info("received SIGHUP signal, reloading TLS certificates if enabled")
119 if err := s.ReloadCertificates(); err != nil {
120 s.logger.Error("failed to reload TLS certificates", "err", err)
121 }
122 continue
123 }
124 // Real signal received; shut down
125 goto shutdown
126 case <-testStop:
127 // Test hook triggered; shut down
128 goto shutdown
129 }
130 }
131
132 shutdown:
133 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
134 defer cancel()
135 if err := s.Shutdown(ctx); err != nil {
136 return err
137 }
138
139 return nil
140 },
141 }
142)
143
144func init() {
145 Command.Flags().BoolVarP(&syncHooks, "sync-hooks", "", false, "synchronize hooks for all repositories before running the server")
146}
147
148const updateHookExample = `#!/bin/sh
149#
150# An example hook script to echo information about the push
151# and send it to the client.
152#
153# To enable this hook, rename this file to "update" and make it executable.
154
155refname="$1"
156oldrev="$2"
157newrev="$3"
158
159# Safety check
160if [ -z "$GIT_DIR" ]; then
161 echo "Don't run this script from the command line." >&2
162 echo " (if you want, you could supply GIT_DIR then run" >&2
163 echo " $0 <ref> <oldrev> <newrev>)" >&2
164 exit 1
165fi
166
167if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
168 echo "usage: $0 <ref> <oldrev> <newrev>" >&2
169 exit 1
170fi
171
172# Check types
173# if $newrev is 0000...0000, it's a commit to delete a ref.
174zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
175if [ "$newrev" = "$zero" ]; then
176 newrev_type=delete
177else
178 newrev_type=$(git cat-file -t $newrev)
179fi
180
181echo "Hi from Soft Serve update hook!"
182echo
183echo "Repository: $SOFT_SERVE_REPO_NAME"
184echo "RefName: $refname"
185echo "Change Type: $newrev_type"
186echo "Old SHA1: $oldrev"
187echo "New SHA1: $newrev"
188
189exit 0
190`