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 select {
111 case err := <-lch:
112 if err != nil {
113 return fmt.Errorf("server error: %w", err)
114 }
115 case <-done:
116 }
117
118 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
119 defer cancel()
120 if err := s.Shutdown(ctx); err != nil {
121 return err
122 }
123
124 return nil
125 },
126 }
127)
128
129func init() {
130 Command.Flags().BoolVarP(&syncHooks, "sync-hooks", "", false, "synchronize hooks for all repositories before running the server")
131}
132
133const updateHookExample = `#!/bin/sh
134#
135# An example hook script to echo information about the push
136# and send it to the client.
137#
138# To enable this hook, rename this file to "update" and make it executable.
139
140refname="$1"
141oldrev="$2"
142newrev="$3"
143
144# Safety check
145if [ -z "$GIT_DIR" ]; then
146 echo "Don't run this script from the command line." >&2
147 echo " (if you want, you could supply GIT_DIR then run" >&2
148 echo " $0 <ref> <oldrev> <newrev>)" >&2
149 exit 1
150fi
151
152if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
153 echo "usage: $0 <ref> <oldrev> <newrev>" >&2
154 exit 1
155fi
156
157# Check types
158# 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" ]; then
161 newrev_type=delete
162else
163 newrev_type=$(git cat-file -t $newrev)
164fi
165
166echo "Hi from Soft Serve update hook!"
167echo
168echo "Repository: $SOFT_SERVE_REPO_NAME"
169echo "RefName: $refname"
170echo "Change Type: $newrev_type"
171echo "Old SHA1: $oldrev"
172echo "New SHA1: $newrev"
173
174exit 0
175`