diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/build/main.go | 46 | ||||
-rw-r--r-- | cmd/cspgenerator/cspgenerator.go | 13 | ||||
-rw-r--r-- | cmd/dev/main.go | 318 | ||||
-rw-r--r-- | cmd/server/main.go | 65 |
4 files changed, 442 insertions, 0 deletions
diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 0000000..0b1cc46 --- /dev/null +++ b/cmd/build/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "io/fs" + "os" + + "website/internal/builder" + "website/internal/log" + + "github.com/BurntSushi/toml" + "github.com/ardanlabs/conf/v3" + "github.com/pkg/errors" +) + +func main() { + ioConfig := builder.IOConfig{} + if help, err := conf.Parse("", &ioConfig); err != nil { + if errors.Is(err, conf.ErrHelpWanted) { + fmt.Println(help) + os.Exit(1) + } + panic("error parsing configuration: " + err.Error()) + } + log.Configure(!ioConfig.Development) + + log.Debug("starting build process") + if ioConfig.Source != "." { + err := os.Chdir(ioConfig.Source) + if err != nil { + log.Panic("could not change to source directory") + } + } + + if err := builder.BuildSite(ioConfig); err != nil { + switch cause := errors.Cause(err).(type) { + case *fs.PathError: + log.Error("path error", "error", err) + case toml.ParseError: + log.Info("parse error", "error", err) + default: + log.Info("other error", "error", err, "cause", errors.Cause(cause)) + } + os.Exit(1) + } +} diff --git a/cmd/cspgenerator/cspgenerator.go b/cmd/cspgenerator/cspgenerator.go new file mode 100644 index 0000000..89d2718 --- /dev/null +++ b/cmd/cspgenerator/cspgenerator.go @@ -0,0 +1,13 @@ +package main + +import ( + "website/internal/config" + "website/internal/log" +) + +func main() { + err := config.GenerateCSP() + if err != nil { + log.Fatal("error generating csp", "error", err) + } +} diff --git a/cmd/dev/main.go b/cmd/dev/main.go new file mode 100644 index 0000000..459cfaf --- /dev/null +++ b/cmd/dev/main.go @@ -0,0 +1,318 @@ +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 err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return + } + stderr, err := cmd.StderrPipe() + if err != nil { + return + } + + go io.Copy(os.Stdout, stdout) + go io.Copy(os.Stderr, stderr) + + 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, err + } + 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, info 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 err + } + return nil + }) + return err +} + +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 err + } + <-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()) + defer cancel() + + 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) + defer eventsource.Close() + srv := http.Server{ + Addr: devConfig.BaseURL.Host, + } + 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.Fatal("error creating file watcher", "error", err) + } + err = fw.WatchAllFiles("content") + if err != nil { + log.Fatal("could not watch files in content directory", "error", err) + } + err = fw.WatchAllFiles("templates") + if err != nil { + log.Fatal("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") + wg.Wait() + os.Exit(exitCode) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..464c438 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "sync" + + "website/internal/log" + "website/internal/server" + + "github.com/ardanlabs/conf/v3" + "github.com/pkg/errors" +) + +var ( + CommitSHA string + ShortSHA string +) + +func main() { + runtimeConfig := server.Config{} + help, err := conf.Parse("", &runtimeConfig) + if err != nil { + if errors.Is(err, conf.ErrHelpWanted) { + fmt.Println(help) + os.Exit(1) + } + panic("parsing runtime configuration" + err.Error()) + } + log.Configure(runtimeConfig.Production) + + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt) + sv, err := server.New(&runtimeConfig) + if err != nil { + log.Fatal("error setting up server", "error", err) + } + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + sig := <-c + log.Info("signal captured", "sig", sig) + <-sv.Stop() + log.Debug("server stopped") + }() + + sErr := make(chan error) + wg.Add(1) + go func() { + defer wg.Done() + sErr <- sv.Start() + }() + if !runtimeConfig.InDevServer { + log.Info("server listening", "address", sv.Addr) + } + + err = <-sErr + if err != nil { + // Error starting or closing listener: + log.Fatal("error", "error", err) + } + wg.Wait() +} |