1package serve23import (4 "context"5 "errors"6 "fmt"7 "net/http"89 "charm.land/log/v2"1011 "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)2324// Server is the Soft Serve server.25type Server struct {26 SSHServer *sshsrv.SSHServer27 GitDaemon *daemon.GitDaemon28 HTTPServer *web.HTTPServer29 StatsServer *stats.StatsServer30 Cron *cron.Scheduler31 Config *config.Config32 Backend *backend.Backend33 DB *db.DB3435 logger *log.Logger36 ctx context.Context37}3839// NewServer returns a new *Server configured to serve Soft Serve. The SSH40// server key-pair will be created if none exists.41// It expects a context with *backend.Backend, *db.DB, *log.Logger, and42// *config.Config attached.43func NewServer(ctx context.Context) (*Server, error) {44 var err error45 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 }5657 // 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 continue63 }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 }6869 j.ID = id70 }7172 srv.Cron = sched7374 srv.SSHServer, err = sshsrv.NewSSHServer(ctx)75 if err != nil {76 return nil, fmt.Errorf("create ssh server: %w", err)77 }7879 srv.GitDaemon, err = daemon.NewGitDaemon(ctx)80 if err != nil {81 return nil, fmt.Errorf("create git daemon: %w", err)82 }8384 srv.HTTPServer, err = web.NewHTTPServer(ctx)85 if err != nil {86 return nil, fmt.Errorf("create http server: %w", err)87 }8889 srv.StatsServer, err = stats.NewStatsServer(ctx)90 if err != nil {91 return nil, fmt.Errorf("create stats server: %w", err)92 }9394 return srv, nil95}9697// Start starts the SSH server.98func (s *Server) Start() error {99 errg, _ := errgroup.WithContext(s.ctx)100101 // optionally start the SSH server102 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 err107 }108 return nil109 })110 }111112 // optionally start the git daemon113 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 err118 }119 return nil120 })121 }122123 // optionally start the HTTP server124 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 err129 }130 return nil131 })132 }133134 // optionally start the Stats server135 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 err140 }141 return nil142 })143 }144145 errg.Go(func() error {146 s.Cron.Start()147 return nil148 })149 return errg.Wait()150}151152// 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 nil175 })176 // defer s.DB.Close() // nolint: errcheck177 return errg.Wait()178}179180// Close closes the SSH server.181func (s *Server) Close() error {182 var errg errgroup.Group183 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 nil190 })191 // defer s.DB.Close() // nolint: errcheck192 return errg.Wait()193}