Improve local server (#6)

* don't interrupt watcher on error

* don't halt build on missing file

* extract buildFile function

* build the site with concurrent workers

* improve fsnotify event treatment and output
This commit is contained in:
Facundo Olano 2024-02-17 21:13:50 -03:00 committed by GitHub
parent f3eb6d58c6
commit ccc8bc5e99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 100 additions and 56 deletions

View file

@ -1,7 +1,6 @@
package commands package commands
import ( import (
"errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
@ -86,15 +85,9 @@ func setupWatcher(config *config.Config) (*fsnotify.Watcher, error) {
return return
} }
// some events can be received for temporary files, e.g. .#backup files generated by emacs while editing .org // chmod events are noisy, ignore them. also skip create events
// when events regarding such files arrive, discard them here instead of triggering a faulty rebuild // which we assume meaningless until the write that comes next
if _, err := os.Stat(event.Name); errors.Is(err, os.ErrNotExist) { if event.Has(fsnotify.Chmod) || event.Has(fsnotify.Create) {
// FIXME change to log debug
fmt.Println("ignoring temporary file", event.Name)
continue
}
// chmod events are noisy, ignore them
if !event.Has(fsnotify.Chmod) {
continue continue
} }
@ -103,15 +96,17 @@ func setupWatcher(config *config.Config) (*fsnotify.Watcher, error) {
// since new nested directories could be triggering this change, and we need to watch those too // since new nested directories could be triggering this change, and we need to watch those too
// and since re-watching files is a noop, I just re-add the entire src everytime there's a change // and since re-watching files is a noop, I just re-add the entire src everytime there's a change
if err := addAll(watcher, config); err != nil { if err := addAll(watcher, config); err != nil {
fmt.Println("error:", err) fmt.Println("couldn't add watchers:", err)
return continue
} }
if err := rebuild(config); err != nil { if err := rebuild(config); err != nil {
fmt.Println("error:", err) fmt.Println("build error:", err)
return continue
} }
fmt.Println("done\nserver listening at", config.SiteUrl)
case err, ok := <-watcher.Errors: case err, ok := <-watcher.Errors:
if !ok { if !ok {
return return

View file

@ -7,8 +7,10 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"github.com/facundoolano/jorge/config" "github.com/facundoolano/jorge/config"
@ -131,7 +133,7 @@ func (site *Site) loadTemplates() error {
templ, err := templates.Parse(site.templateEngine, path) templ, err := templates.Parse(site.templateEngine, path)
// if something fails or this is not a template, skip // if something fails or this is not a template, skip
if err != nil || templ == nil { if err != nil || templ == nil {
return err return checkFileError(err)
} }
// set site related (?) metadata. Not sure if this should go elsewhere // set site related (?) metadata. Not sure if this should go elsewhere
@ -191,8 +193,12 @@ func (site *Site) Build() error {
os.RemoveAll(site.Config.TargetDir) os.RemoveAll(site.Config.TargetDir)
os.Mkdir(site.Config.SrcDir, FILE_RW_MODE) os.Mkdir(site.Config.SrcDir, FILE_RW_MODE)
wg, files := spawnBuildWorkers(site)
defer wg.Wait()
defer close(files)
// walk the source directory, creating directories and files at the target dir // walk the source directory, creating directories and files at the target dir
return filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error { err := filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err
} }
@ -203,20 +209,51 @@ func (site *Site) Build() error {
if entry.IsDir() { if entry.IsDir() {
return os.MkdirAll(targetPath, FILE_RW_MODE) return os.MkdirAll(targetPath, FILE_RW_MODE)
} }
// if it's a file (either static or template) send the path to a worker to build in target
files <- path
return nil
})
return err
}
// Create a channel to send paths to build and a worker pool to handle them concurrently
func spawnBuildWorkers(site *Site) (*sync.WaitGroup, chan string) {
var wg sync.WaitGroup
files := make(chan string, 20)
for range runtime.NumCPU() {
wg.Add(1)
go func(files <-chan string) {
defer wg.Done()
for path := range files {
site.buildFile(path)
}
}(files)
}
return &wg, files
}
func (site *Site) buildFile(path string) error {
subpath, _ := filepath.Rel(site.Config.SrcDir, path)
targetPath := filepath.Join(site.Config.TargetDir, subpath)
var contentReader io.Reader var contentReader io.Reader
var err error
templ, found := site.templates[path] templ, found := site.templates[path]
if !found { if !found {
// if no template found at location, treat the file as static write its contents to target // if no template found at location, treat the file as static write its contents to target
if site.Config.LinkStatic { if site.Config.LinkStatic {
// dev optimization: link static files instead of copying them // dev optimization: link static files instead of copying them
abs, _ := filepath.Abs(path) abs, _ := filepath.Abs(path)
return os.Symlink(abs, targetPath) err = os.Symlink(abs, targetPath)
return checkFileError(err)
} }
srcFile, err := os.Open(path) srcFile, err := os.Open(path)
if err != nil { if err != nil {
return err return checkFileError(err)
} }
defer srcFile.Close() defer srcFile.Close()
contentReader = srcFile contentReader = srcFile
@ -240,12 +277,10 @@ func (site *Site) Build() error {
contentReader = site.minify(targetExt, contentReader) contentReader = site.minify(targetExt, contentReader)
// write the file contents over to target // write the file contents over to target
fmt.Println("writing", targetPath)
return writeToFile(targetPath, contentReader) return writeToFile(targetPath, contentReader)
})
} }
func (site Site) render(templ *templates.Template) ([]byte, error) { func (site *Site) render(templ *templates.Template) ([]byte, error) {
ctx := map[string]interface{}{ ctx := map[string]interface{}{
"site": map[string]interface{}{ "site": map[string]interface{}{
"config": site.Config.AsContext(), "config": site.Config.AsContext(),
@ -281,6 +316,19 @@ func (site Site) render(templ *templates.Template) ([]byte, error) {
return content, nil return content, nil
} }
func checkFileError(err error) error {
// When walking the source dir it can happen that a file is present when walking starts
// but missing or inaccessible when trying to open it (this is particularly frequent with
// backup files from emacs and when running the dev server). We don't want to halt the build
// process in that situation, just inform and continue.
if os.IsNotExist(err) {
// don't abort on missing files, usually spurious temps
fmt.Println("skipping missing file", err)
return nil
}
return err
}
func writeToFile(targetPath string, source io.Reader) error { func writeToFile(targetPath string, source io.Reader) error {
targetFile, err := os.Create(targetPath) targetFile, err := os.Create(targetPath)
if err != nil { if err != nil {
@ -293,6 +341,7 @@ func writeToFile(targetPath string, source io.Reader) error {
return err return err
} }
fmt.Println("added", targetPath)
return targetFile.Sync() return targetFile.Sync()
} }