serve.go

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