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}