1package serve23import (4 "context"5 "fmt"6 "net/http"7 "os"8 "os/signal"9 "path/filepath"10 "strconv"11 "sync"12 "syscall"13 "time"1415 "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)2223var (24 syncHooks bool2526 // 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 }4546 if err := cfg.ParseEnv(); err != nil {47 return fmt.Errorf("parse environment variables: %w", err)48 }4950 // Create custom hooks directory if it doesn't exist51 customHooksPath := filepath.Join(cfg.DataPath, "hooks")52 if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {53 os.MkdirAll(customHooksPath, os.ModePerm) // nolint: errcheck54 // Generate update hook example without executable permissions55 hookPath := filepath.Join(customHooksPath, "update.sample")56 // nolint: gosec57 if err := os.WriteFile(hookPath, []byte(updateHookExample), 0o744); err != nil {58 return fmt.Errorf("failed to generate update hook example: %w", err)59 }60 }6162 // Create log directory if it doesn't exist63 logPath := filepath.Join(cfg.DataPath, "log")64 if _, err := os.Stat(logPath); err != nil && os.IsNotExist(err) {65 os.MkdirAll(logPath, os.ModePerm) // nolint: errcheck66 }6768 db := db.FromContext(ctx)69 if err := migrate.Migrate(ctx, db); err != nil {70 return fmt.Errorf("migration error: %w", err)71 }7273 s, err := NewServer(ctx)74 if err != nil {75 return fmt.Errorf("start server: %w", err)76 }7778 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 }8485 lch := make(chan error, 1)86 done := make(chan os.Signal, 1)87 doneOnce := sync.OnceFunc(func() { close(done) })8889 signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)9091 // This endpoint is added for testing purposes92 // 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.Handler96 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 return100 }101 h.ServeHTTP(w, r)102 })103 }104105 go func() {106 lch <- s.Start()107 doneOnce()108 }()109110 select {111 case err := <-lch:112 if err != nil {113 return fmt.Errorf("server error: %w", err)114 }115 case <-done:116 }117118 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)119 defer cancel()120 if err := s.Shutdown(ctx); err != nil {121 return err122 }123124 return nil125 },126 }127)128129func init() {130 Command.Flags().BoolVarP(&syncHooks, "sync-hooks", "", false, "synchronize hooks for all repositories before running the server")131}132133const updateHookExample = `#!/bin/sh134#135# An example hook script to echo information about the push136# and send it to the client.137#138# To enable this hook, rename this file to "update" and make it executable.139140refname="$1"141oldrev="$2"142newrev="$3"143144# Safety check145if [ -z "$GIT_DIR" ]; then146 echo "Don't run this script from the command line." >&2147 echo " (if you want, you could supply GIT_DIR then run" >&2148 echo " $0 <ref> <oldrev> <newrev>)" >&2149 exit 1150fi151152if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then153 echo "usage: $0 <ref> <oldrev> <newrev>" >&2154 exit 1155fi156157# Check types158# if $newrev is 0000...0000, it's a commit to delete a ref.159zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')160if [ "$newrev" = "$zero" ]; then161 newrev_type=delete162else163 newrev_type=$(git cat-file -t $newrev)164fi165166echo "Hi from Soft Serve update hook!"167echo168echo "Repository: $SOFT_SERVE_REPO_NAME"169echo "RefName: $refname"170echo "Change Type: $newrev_type"171echo "Old SHA1: $oldrev"172echo "New SHA1: $newrev"173174exit 0175`