Repos / s4g / 160382f09d
commit 160382f09dfbfb5fd4d6976250759c0b3915fa5a
Author: Nhân <hi@imnhan.com>
Date: Sat Jul 15 13:33:07 2023 +0700
show error page; restart server when root changes
TODO: when Root changes from a subpath to "/", the server will return
a 404 instead of redirecting user to new path. Should fix that.
diff --git a/feed.go b/feed.go
index 7ecd3dd..c49dff7 100644
--- a/feed.go
+++ b/feed.go
@@ -9,7 +9,7 @@
// TODO: Use Article's updated date instead of PostedAt.
// I need to implement Article.UpdatedAt first though.
-func generateFeed(site SiteMetadata, posts []Article, path string) []byte {
+func generateFeed(site *SiteMetadata, posts []Article, path string) []byte {
siteAddr := site.Address
if !strings.HasSuffix(siteAddr, "/") {
siteAddr += "/"
diff --git a/livereload/error.html b/livereload/error.html
new file mode 100644
index 0000000..c2d392e
--- /dev/null
+++ b/livereload/error.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <title>Error</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ </head>
+ <body>
+ <b>Error:</b> {{.}}
+ <style>
+ body {
+ background-color: pink;
+ color: red;
+ }
+ </style>
+ </body>
+</html>
diff --git a/livereload/livereload.go b/livereload/livereload.go
index 6bd7e02..898e6dd 100644
--- a/livereload/livereload.go
+++ b/livereload/livereload.go
@@ -7,6 +7,7 @@
"net/http"
"strings"
"sync"
+ "text/template"
"go.imnhan.com/webmaker2000/writablefs"
)
@@ -17,6 +18,9 @@
//go:embed livereload.html
var lrScript []byte
+//go:embed error.html
+var errorTmpl string
+
var pleaseReload = []byte("1")
var dontReload = []byte("0")
@@ -25,8 +29,10 @@
//
// Client IDs are generated on client side so that an open tab's
// livereload feature keeps working even when the server is restarted.
- clients map[string]bool
- mut sync.RWMutex
+ clients map[string]bool
+ clientsMut sync.RWMutex
+ err error
+ errMut sync.RWMutex
}{
clients: make(map[string]bool),
}
@@ -45,16 +51,16 @@ func init() {
func handleFunc(w http.ResponseWriter, r *http.Request) {
clientId := r.Header.Get(clientIdHeader)
- state.mut.RLock()
+ state.clientsMut.RLock()
shouldReload, ok := state.clients[clientId]
- state.mut.RUnlock()
+ state.clientsMut.RUnlock()
// New client: add client to state, don't reload
if !ok {
//fmt.Println("New livereload client:", clientId)
- state.mut.Lock()
+ state.clientsMut.Lock()
state.clients[clientId] = false
- state.mut.Unlock()
+ state.clientsMut.Unlock()
w.Write(dontReload)
return
}
@@ -64,21 +70,30 @@ func handleFunc(w http.ResponseWriter, r *http.Request) {
w.Write(pleaseReload)
// On reload, the browser tab will generate another client ID,
// so we can safely delete the old client ID now:
- state.mut.Lock()
+ state.clientsMut.Lock()
delete(state.clients, clientId)
- state.mut.Unlock()
+ state.clientsMut.Unlock()
} else {
w.Write(dontReload)
}
}
// For html pages, insert a script tag to enable livereload
-func Middleware(root string, fsys writablefs.FS, f http.Handler) http.Handler {
+func Middleware(mux *http.ServeMux, root string, fsys writablefs.FS, f http.Handler) http.Handler {
// Handle AJAX endpoint
- http.HandleFunc(endpoint, handleFunc)
+ mux.HandleFunc(endpoint, handleFunc)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var err error
+ state.errMut.RLock()
+ err = state.err
+ state.errMut.RUnlock()
+ if err != nil {
+ serveError(w, r, err)
+ return
+ }
+
path := r.URL.Path
// For non-html requests, fall through to default FileServer handler
@@ -107,13 +122,22 @@ func Middleware(root string, fsys writablefs.FS, f http.Handler) http.Handler {
// Tell current browser tabs to reload
func Trigger() {
- state.mut.Lock()
- defer state.mut.Unlock()
+ state.clientsMut.Lock()
+ defer state.clientsMut.Unlock()
for k := range state.clients {
state.clients[k] = true
}
}
+// When a non-nil error is set, the local webserver returns
+// the error page for every path (except livereload duh).
+func SetError(err error) {
+ state.errMut.Lock()
+ state.err = err
+ state.errMut.Unlock()
+ Trigger()
+}
+
func withLiveReload(original []byte) []byte {
bodyEndPos := bytes.LastIndex(original, []byte("</body>"))
if bodyEndPos == -1 {
@@ -128,3 +152,12 @@ func withLiveReload(original []byte) []byte {
copy(result[bodyEndPos+len(lrScript):], original[bodyEndPos:])
return result
}
+
+var errTmpl = template.Must(template.New("error").Parse(errorTmpl))
+
+func serveError(w http.ResponseWriter, r *http.Request, err error) {
+ var buf bytes.Buffer
+ errTmpl.Execute(&buf, err.Error())
+ body := withLiveReload(buf.Bytes())
+ w.Write(body)
+}
diff --git a/main.go b/main.go
index 7e7b6f7..7eb15a6 100644
--- a/main.go
+++ b/main.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "context"
"flag"
"fmt"
"html/template"
@@ -13,6 +14,7 @@
"path/filepath"
"sort"
"strings"
+ "sync"
"time"
"go.imnhan.com/webmaker2000/djot"
@@ -81,8 +83,34 @@ func handleServeCmd(folder, port string) {
}
fsys := writablefs.WriteDirFS(absolutePath)
+ site, err := ReadSiteMetadata(fsys)
+ if err != nil {
+ panic(err)
+ }
- site := regenerate(fsys)
+ webRootUpdates := make(chan string)
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func(webRoot string) {
+ defer wg.Done()
+
+ srv := runServer(fsys, webRoot, port)
+
+ for {
+ newRoot := <-webRootUpdates
+ if newRoot == webRoot {
+ continue
+ }
+ fmt.Println("Root changed => restarting server")
+ webRoot = newRoot
+ err := srv.Shutdown(context.TODO())
+ if err != nil {
+ panic(err)
+ }
+ srv = runServer(fsys, webRoot, port)
+ }
+ }(site.Root)
// TODO: only rebuild necessary bits instead of regenerating
// the whole thing. To do that I'll probably need to:
@@ -93,35 +121,61 @@ func handleServeCmd(folder, port string) {
// directory.
closeWatcher := WatchLocalFS(fsys, func() {
fmt.Println("Change detected. Regenerating...")
- regenerate(fsys)
- livereload.Trigger()
+ newSite, err := regenerate(fsys)
+ livereload.SetError(err)
+ if err == nil {
+ fmt.Println("Sending", newSite.Root)
+ webRootUpdates <- newSite.Root
+ fmt.Println("Done", newSite.Root)
+ }
})
defer closeWatcher()
- println("Serving local website at http://localhost:" + port + site.Root)
- http.Handle(
- site.Root,
+ site, err = regenerate(fsys)
+ livereload.SetError(err)
+
+ wg.Wait()
+}
+
+// Non-blocking. Returns srv handle to allow calling Shutdown() later.
+func runServer(fsys writablefs.FS, webRoot string, port string) *http.Server {
+ println("Serving local website at http://localhost:" + port + webRoot)
+ mux := http.NewServeMux()
+ mux.Handle(
+ webRoot,
livereload.Middleware(
- site.Root,
+ mux,
+ webRoot,
fsys,
- http.StripPrefix(site.Root, http.FileServer(http.FS(fsys))),
+ http.StripPrefix(webRoot, http.FileServer(http.FS(fsys))),
),
)
- if site.Root != "/" {
- http.Handle("/", http.RedirectHandler(site.Root, http.StatusTemporaryRedirect))
+ if webRoot != "/" {
+ mux.Handle("/", http.RedirectHandler(webRoot, http.StatusTemporaryRedirect))
}
- err = http.ListenAndServe("127.0.0.1:"+port, nil)
- if err != nil {
- panic(err)
+ srv := &http.Server{
+ Addr: "127.0.0.1:" + port,
+ Handler: mux,
}
+
+ go func() {
+ srv.ListenAndServe()
+ }()
+
+ return srv
}
-func regenerate(fsys writablefs.FS) (site SiteMetadata) {
+func regenerate(fsys writablefs.FS) (site *SiteMetadata, err error) {
defer timer("Took %s")()
- site = ReadSiteMetadata(fsys)
+ site, err = ReadSiteMetadata(fsys)
+ if err != nil {
+ livereload.SetError(err)
+ return nil, err
+ }
+
articles := findArticles(fsys, site)
if len(articles) == 0 {
@@ -136,8 +190,8 @@ func regenerate(fsys writablefs.FS) (site SiteMetadata) {
for _, link := range site.NavbarLinks {
a, ok := articles[link]
if !ok {
- fmt.Printf("NavbarLinks: %s not found\n", link)
- continue
+ return nil,
+ fmt.Errorf("%s: NavbarLinks: %s not found", FeedPath, link)
}
articlesInNav = append(articlesInNav, a)
}
@@ -160,7 +214,7 @@ func regenerate(fsys writablefs.FS) (site SiteMetadata) {
for _, a := range articles {
fmt.Println(">", a.Path, "-", a.Title)
- a.WriteHtmlFile(&site, articlesInNav, articlesInFeed, startYear)
+ a.WriteHtmlFile(site, articlesInNav, articlesInFeed, startYear)
generatedFiles[a.OutputPath] = true
}
fmt.Printf("Processed %d articles\n", len(articles))
@@ -270,7 +324,7 @@ func (a *Article) WriteHtmlFile(
}
}
-func findArticles(fsys writablefs.FS, site SiteMetadata) map[string]Article {
+func findArticles(fsys writablefs.FS, site *SiteMetadata) map[string]Article {
result := make(map[string]Article)
fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
diff --git a/metadata.go b/metadata.go
index a047d37..d32badb 100644
--- a/metadata.go
+++ b/metadata.go
@@ -42,12 +42,12 @@ func NewSiteMetadata() SiteMetadata {
}
}
-func ReadSiteMetadata(fsys writablefs.FS) SiteMetadata {
+func ReadSiteMetadata(fsys writablefs.FS) (*SiteMetadata, error) {
sm := NewSiteMetadata()
data, err := fs.ReadFile(fsys, SiteFileName)
if err != nil {
- panic(err)
+ return nil, fmt.Errorf("ReadSiteMetadata: %w", err)
}
UnmarshalMetadata(data, &sm)
@@ -60,7 +60,7 @@ func ReadSiteMetadata(fsys writablefs.FS) SiteMetadata {
sm.Root = fmt.Sprintf("/%s/", trimmed)
}
- return sm
+ return &sm, nil
}
// Similar API to json.Unmarshal but supports neither struct tags nor nesting.