about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2024-06-05 12:16:36 +0200
committerAlan Pearce2024-06-05 12:16:36 +0200
commitc0036498bba6cbe6e9a5815ca7687cc57129376e (patch)
tree47c1a8135a672f280417c36a798597a23b99a127
parent779bfc1e56eec9f70bba21aff3ea490856d90876 (diff)
downloadwebsite-c0036498bba6cbe6e9a5815ca7687cc57129376e.tar.lz
website-c0036498bba6cbe6e9a5815ca7687cc57129376e.tar.zst
website-c0036498bba6cbe6e9a5815ca7687cc57129376e.zip
replace unreliable dev server with modd
-rw-r--r--cmd/dev/main.go340
-rw-r--r--flake.nix1
-rwxr-xr-xjustfile10
-rw-r--r--modd.conf7
4 files changed, 9 insertions, 349 deletions
diff --git a/cmd/dev/main.go b/cmd/dev/main.go
deleted file mode 100644
index e1525a2..0000000
--- a/cmd/dev/main.go
+++ /dev/null
@@ -1,340 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"io/fs"
-	"net/http"
-	"net/http/httputil"
-
-	"os"
-	"os/exec"
-	"os/signal"
-	"path"
-	"path/filepath"
-	"sync"
-	"syscall"
-	"time"
-
-	"website/internal/config"
-	"website/internal/log"
-
-	"github.com/antage/eventsource"
-	"github.com/ardanlabs/conf/v3"
-	"github.com/gohugoio/hugo/watcher"
-
-	"github.com/pkg/errors"
-)
-
-type DevConfig struct {
-	Source    string     `conf:"default:.,short:s"`
-	TempDir   string     `conf:"required,short:t"`
-	BaseURL   config.URL `conf:"default:http://localhost:3000"`
-	ServerURL config.URL `conf:"default:http://localhost:3001"`
-}
-
-func RunCommandPiped(
-	ctx context.Context,
-	command string,
-	args ...string,
-) (cmd *exec.Cmd, err error) {
-	log.Debug("running command", "command", command, "args", args)
-	cmd = exec.CommandContext(ctx, command, args...)
-	cmd.Env = append(os.Environ(), "DEBUG=")
-	cmd.Cancel = func() error {
-		log.Debug("signalling child")
-		err := cmd.Process.Signal(os.Interrupt)
-		if err != nil {
-			log.Error("signal error:", "error", err)
-		}
-
-		return errors.Wrap(err, "error signalling child process")
-	}
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		return
-	}
-	stderr, err := cmd.StderrPipe()
-	if err != nil {
-		return
-	}
-
-	go func() {
-		_, err := io.Copy(os.Stdout, stdout)
-		if err != nil {
-			log.Error("error copying stdout", "error", err)
-		}
-	}()
-	go func() {
-		_, err := io.Copy(os.Stderr, stderr)
-		if err != nil {
-			log.Error("error copying stderr", "error", err)
-		}
-	}()
-
-	return
-}
-
-type FileWatcher struct {
-	*watcher.Batcher
-}
-
-func NewFileWatcher(pollTime time.Duration) (*FileWatcher, error) {
-	batcher, err := watcher.New(pollTime/5, pollTime, true)
-	if err != nil {
-		return nil, errors.Wrap(err, "could not create file watcher")
-	}
-
-	return &FileWatcher{batcher}, nil
-}
-
-func (watcher FileWatcher) WatchAllFiles(from string) error {
-	log.Debug("watching files under", "from", from)
-	err := filepath.Walk(from, func(path string, _ fs.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		// log.Debug(fmt.Sprintf("adding file %s to watcher", path))
-		if err = watcher.Add(path); err != nil {
-			return errors.Wrapf(err, "could not add path %s to watcher", path)
-		}
-
-		return nil
-	})
-
-	return errors.Wrapf(err, "could not walk directory tree %s", from)
-}
-
-func build(ctx context.Context, config DevConfig) error {
-	buildExe := filepath.Join(config.TempDir, "build")
-	cmd, err := RunCommandPiped(ctx, buildExe,
-		"--dest", path.Join(config.TempDir, "output"),
-		"--dev",
-	)
-	// cmd, err := RunCommandPiped(ctx, "./devfakebuild")
-
-	if err != nil {
-		return errors.WithMessage(err, "error running build command")
-	}
-
-	err = cmd.Run()
-	log.Debug(
-		"build command exited",
-		"status",
-		cmd.ProcessState.ExitCode(),
-	)
-	if err != nil {
-		return errors.WithMessage(err, "error running build command")
-	}
-
-	return nil
-}
-
-func server(ctx context.Context, devConfig DevConfig) error {
-	serverExe := path.Join(devConfig.TempDir, "server")
-
-	cmd, err := RunCommandPiped(ctx,
-		serverExe,
-		"--port", devConfig.ServerURL.Port(),
-		"--root", path.Join(devConfig.TempDir, "output"),
-		"--in-dev-server",
-	)
-	if err != nil {
-		return errors.WithMessage(err, "error running server command")
-	}
-	// cmd.Env = append(cmd.Env, "DEBUG=1")
-
-	cmdErr := make(chan error, 1)
-	done := make(chan struct{})
-	err = cmd.Start()
-	if err != nil {
-		return errors.WithMessage(err, "error starting server binary")
-	}
-
-	go func() {
-		err := cmd.Wait()
-		if err == nil && cmd.ProcessState.Exited() {
-			err = errors.Errorf("server exited unexpectedly")
-		}
-
-		cmdErr <- err
-		close(done)
-	}()
-
-	for {
-		select {
-		case <-ctx.Done():
-			log.Debug("server context done")
-			err := cmd.Process.Signal(os.Interrupt)
-			if err != nil {
-				return errors.Wrap(err, "could not process signal")
-			}
-			<-done
-		case err := <-cmdErr:
-			return err
-		}
-	}
-}
-
-func main() {
-	var wg sync.WaitGroup
-	log.Configure(false)
-
-	devConfig := DevConfig{}
-	help, err := conf.Parse("", &devConfig)
-	if err != nil {
-		if errors.Is(err, conf.ErrHelpWanted) {
-			fmt.Println(help)
-			os.Exit(1)
-		}
-		log.Panic("parsing dev configuration", "error", err)
-	}
-
-	log.Debug("running with in /tmp", "dir", devConfig.TempDir)
-
-	ctx, cancel := context.WithCancel(context.Background())
-
-	log.Debug("setting interrupt handler")
-	c := make(chan os.Signal, 1)
-	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
-	go func() {
-		sig := <-c
-		log.Info("shutting down on signal", "sig", sig)
-		cancel()
-		sig = <-c
-		log.Info("got second signal, dying", "sig", sig)
-		os.Exit(1)
-	}()
-
-	serverChan := make(chan bool, 1)
-	eventsource := eventsource.New(nil, nil)
-	srv := http.Server{
-		Addr:              devConfig.BaseURL.Host,
-		ReadHeaderTimeout: 1 * time.Minute,
-	}
-	devCtx, devCancel := context.WithTimeout(ctx, 1*time.Second)
-
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-		defer devCancel()
-		log.Debug("waiting for first server launch")
-		<-serverChan
-		log.Debug("got first server launch event")
-
-		http.Handle("/", &httputil.ReverseProxy{
-			Rewrite: func(req *httputil.ProxyRequest) {
-				req.SetURL(devConfig.ServerURL.URL)
-				req.Out.Host = req.In.Host
-			},
-		})
-		http.Handle("/_/reload", eventsource)
-		done := make(chan bool)
-		go func() {
-			err := srv.ListenAndServe()
-			if err != nil && err != http.ErrServerClosed {
-				log.Error(err.Error())
-				cancel()
-			}
-			done <- true
-		}()
-		go func() {
-			for ready := range serverChan {
-				if ready {
-					log.Debug("sending reload message")
-					eventsource.SendEventMessage("reload", "", "")
-				} else {
-					log.Debug("server not ready")
-				}
-			}
-		}()
-		log.Info("dev server listening on", "host", devConfig.BaseURL.String())
-		<-done
-		log.Debug("dev server closed")
-	}()
-
-	fw, err := NewFileWatcher(500 * time.Millisecond)
-	if err != nil {
-		log.Panic("error creating file watcher", "error", err)
-	}
-	err = fw.WatchAllFiles("content")
-	if err != nil {
-		log.Panic("could not watch files in content directory", "error", err)
-	}
-	err = fw.WatchAllFiles("templates")
-	if err != nil {
-		log.Panic("could not watch files in templates directory", "error", err)
-	}
-
-	var exitCode int
-	serverErr := make(chan error, 1)
-loop:
-	for {
-		serverCtx, stopServer := context.WithCancel(ctx)
-		log.Debug("starting build")
-
-		err := build(ctx, devConfig)
-		if err != nil {
-			log.Error("build error:", "error", err)
-			// don't set up the server until there's a FS change event
-		} else {
-			log.Debug("setting up server")
-			wg.Add(1)
-			go func() {
-				defer wg.Done()
-				serverChan <- true
-				serverErr <- server(serverCtx, devConfig)
-			}()
-		}
-
-		select {
-		case <-ctx.Done():
-			log.Debug("main context cancelled")
-			log.Debug("calling server shutdown")
-			err := srv.Shutdown(devCtx)
-			if err != nil {
-				log.Debug("shutdown error", "error", err)
-			}
-			exitCode = 1
-
-			break loop
-		case event := <-fw.Events:
-			log.Debug("event received:", "event", event)
-			stopServer()
-			serverChan <- false
-			log.Debug("waiting for server shutdown")
-			<-serverErr
-			log.Debug("server shutdown completed")
-
-			continue
-		case err = <-serverErr:
-			if err != nil && err != context.Canceled {
-				var exerr *exec.ExitError
-				log.Error("server reported error:", "error", err)
-				if errors.As(err, &exerr) {
-					log.Debug("server exit error")
-				} else {
-					log.Debug("server other error")
-				}
-
-				break
-			}
-			log.Debug("no error or server context cancelled")
-
-			continue
-		}
-
-		log.Debug("waiting on server")
-		exitCode = 0
-
-		break
-	}
-
-	log.Debug("waiting for wg before shutting down")
-	eventsource.Close()
-	cancel()
-	wg.Wait()
-
-	os.Exit(exitCode)
-}
diff --git a/flake.nix b/flake.nix
index 8b4057f..e912820 100644
--- a/flake.nix
+++ b/flake.nix
@@ -46,6 +46,7 @@
                 gomod2nix.packages.${system}.default
                 gci
                 hyperlink
+                modd
                 nodePackages.vercel
                 netlify-cli
                 sentry-cli
diff --git a/justfile b/justfile
index dcd242a..aec4a36 100755
--- a/justfile
+++ b/justfile
@@ -40,15 +40,7 @@ nix-build what:
 watch-server: (watch-flake "watchexec -r -i content -i templates go run ./cmd/server")
 
 dev:
-    #!/usr/bin/env bash
-    set -euxo pipefail
-    tmp="$(mktemp -d -t website-XXXXXX)" || exit 1
-    echo using temp directory $tmp
-    trap "{ echo cleaning up $tmp; rm -rf \"$tmp\"; }" EXIT
-    go build -o $tmp ./cmd/dev ./cmd/build ./cmd/server
-    "${tmp}/dev" --temp-dir "${tmp}"
-
-watch-dev: (watch-flake "watchexec -r -e go just dev")
+	modd
 
 docker-stream system=(arch() + "-linux"):
     @nix build --print-out-paths .#docker-stream-{{ system }} | sh
diff --git a/modd.conf b/modd.conf
new file mode 100644
index 0000000..ab3e70e
--- /dev/null
+++ b/modd.conf
@@ -0,0 +1,7 @@
+config.toml content/** static/** cmd/build/* internal/** {
+    prep: go run ./cmd/build --base-url http://localhost:3000
+}
+
+config.toml website/** cmd/server/* "internal/{config,log,server,website}/**" {
+    daemon: go run ./cmd/server
+}