1package server
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8
9 "github.com/charmbracelet/log"
10
11 "github.com/charmbracelet/soft-serve/server/backend"
12 "github.com/charmbracelet/soft-serve/server/config"
13 "github.com/charmbracelet/soft-serve/server/cron"
14 "github.com/charmbracelet/soft-serve/server/daemon"
15 "github.com/charmbracelet/soft-serve/server/db"
16 sshsrv "github.com/charmbracelet/soft-serve/server/ssh"
17 "github.com/charmbracelet/soft-serve/server/stats"
18 "github.com/charmbracelet/soft-serve/server/web"
19 "github.com/charmbracelet/ssh"
20 "golang.org/x/sync/errgroup"
21)
22
23// Server is the Soft Serve server.
24type Server struct {
25 SSHServer *sshsrv.SSHServer
26 GitDaemon *daemon.GitDaemon
27 HTTPServer *web.HTTPServer
28 StatsServer *stats.StatsServer
29 Cron *cron.Scheduler
30 Config *config.Config
31 Backend *backend.Backend
32 DB *db.DB
33
34 logger *log.Logger
35 ctx context.Context
36}
37
38// NewServer returns a new *Server configured to serve Soft Serve. The SSH
39// server key-pair will be created if none exists.
40// It expects a context with *backend.Backend, *db.DB, *log.Logger, and
41// *config.Config attached.
42func NewServer(ctx context.Context) (*Server, error) {
43 var err error
44 cfg := config.FromContext(ctx)
45 be := backend.FromContext(ctx)
46 db := db.FromContext(ctx)
47 srv := &Server{
48 Cron: cron.NewScheduler(ctx),
49 Config: cfg,
50 Backend: be,
51 DB: db,
52 logger: log.FromContext(ctx).WithPrefix("server"),
53 ctx: ctx,
54 }
55
56 // Add cron jobs.
57 _, _ = srv.Cron.AddFunc(jobSpecs["mirror"], srv.mirrorJob(be))
58
59 srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
60 if err != nil {
61 return nil, fmt.Errorf("create ssh server: %w", err)
62 }
63
64 srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
65 if err != nil {
66 return nil, fmt.Errorf("create git daemon: %w", err)
67 }
68
69 srv.HTTPServer, err = web.NewHTTPServer(ctx)
70 if err != nil {
71 return nil, fmt.Errorf("create http server: %w", err)
72 }
73
74 srv.StatsServer, err = stats.NewStatsServer(ctx)
75 if err != nil {
76 return nil, fmt.Errorf("create stats server: %w", err)
77 }
78
79 return srv, nil
80}
81
82// Start starts the SSH server.
83func (s *Server) Start() error {
84 errg, _ := errgroup.WithContext(s.ctx)
85 errg.Go(func() error {
86 s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
87 if err := s.GitDaemon.Start(); !errors.Is(err, daemon.ErrServerClosed) {
88 return err
89 }
90 return nil
91 })
92 errg.Go(func() error {
93 s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
94 if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
95 return err
96 }
97 return nil
98 })
99 errg.Go(func() error {
100 s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
101 if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {
102 return err
103 }
104 return nil
105 })
106 errg.Go(func() error {
107 s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
108 if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
109 return err
110 }
111 return nil
112 })
113 errg.Go(func() error {
114 s.Cron.Start()
115 return nil
116 })
117 return errg.Wait()
118}
119
120// Shutdown lets the server gracefully shutdown.
121func (s *Server) Shutdown(ctx context.Context) error {
122 errg, ctx := errgroup.WithContext(ctx)
123 errg.Go(func() error {
124 return s.GitDaemon.Shutdown(ctx)
125 })
126 errg.Go(func() error {
127 return s.HTTPServer.Shutdown(ctx)
128 })
129 errg.Go(func() error {
130 return s.SSHServer.Shutdown(ctx)
131 })
132 errg.Go(func() error {
133 return s.StatsServer.Shutdown(ctx)
134 })
135 errg.Go(func() error {
136 s.Cron.Stop()
137 return nil
138 })
139 // defer s.DB.Close() // nolint: errcheck
140 return errg.Wait()
141}
142
143// Close closes the SSH server.
144func (s *Server) Close() error {
145 var errg errgroup.Group
146 errg.Go(s.GitDaemon.Close)
147 errg.Go(s.HTTPServer.Close)
148 errg.Go(s.SSHServer.Close)
149 errg.Go(s.StatsServer.Close)
150 errg.Go(func() error {
151 s.Cron.Stop()
152 return nil
153 })
154 // defer s.DB.Close() // nolint: errcheck
155 return errg.Wait()
156}