Parent directory

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`