serve.go

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