all repos — website @ 2a4c795d5a165f995e9f7dc84e07465b140f3770

My website

implement live-reloading dev server Squashed commit of the following: commit 02f077432202af4d633eb2cad81dfdaa6921317f Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 21:09:14 2024 +0200 builder: only remove output directory if set and in dev mode commit 47001e01c55fa6e74aafeda04ebc3e4e7c47eba0 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 21:03:37 2024 +0200 implement live reload on dev server commit 411ec969f61e4b73439f1c54ea29f75135ecc618 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:59:26 2024 +0200 server: implement graceful shutdown commit 5400132eb6eb1b638e0b3fd4265f51611c92d473 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:41:07 2024 +0200 add some debug logs commit 3c9b678197c044603950232d222f501ef74d7873 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:39:09 2024 +0200 prefix log output with executable name commit 300e24c179e390e9d3f5aeab4471c97f17f1fa64 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:29:42 2024 +0200 don't panic inside internal packages, return error instead commit fe2715d330402ad67fe866471bed89c7238ad2ec Author: Alan Pearce <alan@alanpearce.eu> Date: Fri Apr 26 01:18:29 2024 +0200 config: use a table to configure CSP headers commit d012553aaf78a436fa8871830b5d720a9e292d4b Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 17:13:39 2024 +0200 dev: create basic dev server to build and serve from a temporary directory commit a1d11d3e69650d9b43ca1b1d7b7ccc05a808d5c1 Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 13:02:22 2024 +0200 remove unused redirect_other_hostnames config option commit fd67b19b5c7f76f0c3579e8a05ef20a618e90be7 Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 12:58:53 2024 +0200 server: make port a string, which is what go uses commit c798e8e736c0649008cade337158399470a9099b Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 12:58:33 2024 +0200 config: remove unused port variable commit f94882b9001f3b0855e26b26b4a84b96e3deb22b Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 12:49:10 2024 +0200 re-organise module layout

Alan Pearce
commit

2a4c795d5a165f995e9f7dc84e07465b140f3770

parent

9b4ca4783a186c345d99f613aeaf73e1bc112bfa

1 file changed, 72 insertions(+), 12 deletions(-)

changed files
M cmd/server/server.gointernal/server/server.go
@@ -1,6 +1,7 @@
-package main +package server import ( + "context" "fmt" "io" "log"
@@ -9,6 +10,8 @@ "mime"
"net" "net/http" "os" + "path" + "slices" "strings" "time"
@@ -16,15 +19,29 @@ cfg "website/internal/config"
"github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http" + "github.com/pkg/errors" "github.com/shengyanli1982/law" ) var config *cfg.Config +type Config struct { + Production bool `conf:"default:false"` + InDevServer bool `conf:"default:false"` + Root string `conf:"default:website"` + ListenAddress string `conf:"default:localhost"` + Port string `conf:"default:3000,short:p"` + BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"` +} + type HTTPError struct { Error error Message string Code int +} + +type Server struct { + *http.Server } func canonicalisePath(path string) (cPath string, differs bool) {
@@ -52,6 +69,7 @@ }
} w.Header().Add("ETag", file.etag) w.Header().Add("Vary", "Accept-Encoding") + w.Header().Add("Content-Security-Policy", config.CSP.String()) for k, v := range config.Extra.Headers { w.Header().Add(k, v) }
@@ -93,20 +111,28 @@ }
} } -func startServer(runtimeConfig *Config) { +func applyDevModeOverrides(config *cfg.Config) { + config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'") + config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'") +} + +func New(runtimeConfig *Config) (*Server, error) { fixupMIMETypes() - c, err := cfg.GetConfig() + var err error + config, err = cfg.GetConfig() if err != nil { - log.Panicf("parsing configuration file: %v", err) + return nil, errors.WithMessage(err, "error parsing configuration file") + } + if runtimeConfig.InDevServer { + applyDevModeOverrides(config) } - config = c - prefix := "website/public" + prefix := path.Join(runtimeConfig.Root, "public") slog.Debug("registering content files", "prefix", prefix) err = registerContentFiles(prefix) if err != nil { - log.Panicf("registering content files: %v", err) + return nil, errors.WithMessagef(err, "registering content files") } env := "development"
@@ -121,13 +147,14 @@ Release: CommitSHA,
Environment: env, }) if err != nil { - log.Panic("could not set up sentry") + return nil, errors.WithMessage(err, "could not set up sentry") } defer sentry.Flush(2 * time.Second) sentryHandler := sentryhttp.New(sentryhttp.Options{ Repanic: true, }) + top := http.NewServeMux() mux := http.NewServeMux() slog.Debug("binding main handler to", "host", runtimeConfig.BaseURL.Hostname()+"/") mux.Handle(runtimeConfig.BaseURL.Hostname()+"/", webHandler(serveFile))
@@ -143,7 +170,7 @@ logWriter = law.NewWriteAsyncer(os.Stdout, nil)
} else { logWriter = os.Stdout } - http.Handle("/", + top.Handle("/", sentryHandler.Handle( wrapHandlerWithLogging(mux, wrappedHandlerOptions{ defaultHostname: runtimeConfig.BaseURL.Hostname(),
@@ -152,10 +179,43 @@ }),
), ) // no logging, no sentry - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + top.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, fmt.Sprint(runtimeConfig.Port)) - log.Fatal(http.ListenAndServe(listenAddress, nil)) + listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, runtimeConfig.Port) + return &Server{ + &http.Server{ + Addr: listenAddress, + Handler: top, + }, + }, nil +} + +func (s *Server) Start() error { + if err := s.ListenAndServe(); err != http.ErrServerClosed { + return err + } + return nil +} + +func (s *Server) Stop() chan struct{} { + slog.Debug("stop called") + + idleConnsClosed := make(chan struct{}) + + go func() { + slog.Debug("shutting down server") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := s.Server.Shutdown(ctx) + slog.Debug("server shut down") + if err != nil { + // Error from closing listeners, or context timeout: + log.Printf("HTTP server Shutdown: %v", err) + } + close(idleConnsClosed) + }() + + return idleConnsClosed }