soft-serve

Fork https://github.com/charmbracelet/soft-serve

git clone git://git.lin.moe/go/soft-serve.git

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