diff --git a/commands/commandeer.go b/commands/commandeer.go index c55806980e4..73b339e75cb 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -14,6 +14,8 @@ package commands import ( + "fmt" + "log" "os" "path/filepath" "regexp" @@ -21,13 +23,15 @@ import ( "sync" "time" + "github.com/gohugoio/hugo/livereload" + "github.com/gohugoio/hugo/config" "github.com/spf13/cobra" - "github.com/spf13/afero" - "github.com/gohugoio/hugo/hugolib" + "github.com/spf13/afero" + jww "github.com/spf13/jwalterweatherman" "github.com/bep/debounce" "github.com/gohugoio/hugo/common/types" @@ -72,6 +76,70 @@ type commandeer struct { configured bool paused bool + + errorHandler *errorHandler +} + +type errorHandler struct { + logger *log.Logger + sync.RWMutex + err *buildError +} + +type buildError struct { + logger *log.Logger + What string + Err error +} + +func (b buildError) log() { + b.logger.Println(b.Error()) +} + +func (b buildError) Error() string { + return fmt.Sprintf("%s: %s", b.What, b.Err) +} + +func (c *commandeer) getError() *buildError { + h := c.errorHandler + h.RLock() + defer h.RUnlock() + return h.err +} + +func (c *commandeer) getErrorWithContext() interface{} { + h := c.errorHandler + h.RLock() + defer h.RUnlock() + if h.err == nil { + return nil + } + m := make(map[string]interface{}) + + m["Error"] = h.err + m["Version"] = hugoVersionString() + + return m +} + +func (c *commandeer) handleError(what string, err error) error { + h := c.errorHandler + h.Lock() + defer h.Unlock() + + if err == nil { + h.err = nil + return nil + } + + h.err = &buildError{logger: h.logger, What: what, Err: err} + h.err.log() + + // TODO(bep) server error config + // Need to refresh the browser to show the error page. + livereload.ForceRefresh() + + return h.err } func (c *commandeer) Set(key string, value interface{}) { @@ -98,6 +166,15 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla rebuildDebouncer, _, _ = debounce.New(4 * time.Second) } + /*var logger *log.Logger + if c.Logger != nil { + logger = c.Logger.ERROR + } else { + logger = jww.ERROR + }*/ + // TODO(bep) server error + logger := jww.ERROR + c := &commandeer{ h: h, ftch: f, @@ -105,6 +182,7 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla doWithCommandeer: doWithCommandeer, visitedURLs: types.NewEvictingStringQueue(10), debounce: rebuildDebouncer, + errorHandler: &errorHandler{logger: logger}, } return c, c.loadConfig(mustHaveConfigFile, running) diff --git a/commands/commands.go b/commands/commands.go index 54eb03b5b9a..a132cf5e38b 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -14,8 +14,6 @@ package commands import ( - "os" - "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" "github.com/spf13/cobra" @@ -255,25 +253,3 @@ func checkErr(logger *jww.Notepad, err error, s ...string) { } logger.ERROR.Println(err) } - -func stopOnErr(logger *jww.Notepad, err error, s ...string) { - if err == nil { - return - } - - defer os.Exit(-1) - - if len(s) == 0 { - newMessage := err.Error() - // Printing an empty string results in a error with - // no message, no bueno. - if newMessage != "" { - logger.CRITICAL.Println(newMessage) - } - } - for _, message := range s { - if message != "" { - logger.CRITICAL.Println(message) - } - } -} diff --git a/commands/hugo.go b/commands/hugo.go index 2e7353d5152..6244b376235 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -24,6 +24,7 @@ import ( "syscall" "github.com/gohugoio/hugo/hugolib/filesystems" + "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -637,7 +638,7 @@ func (c *commandeer) fullRebuild() { c.commandeerHugoState = &commandeerHugoState{} err := c.loadConfig(true, true) if err != nil { - jww.ERROR.Println("Failed to reload config:", err) + c.handleError("Failed to reload config", errors.WithStack(err)) // Set the processing on pause until the state is recovered. c.paused = true } else { @@ -645,9 +646,9 @@ func (c *commandeer) fullRebuild() { } if !c.paused { - if err := c.buildSites(); err != nil { - jww.ERROR.Println(err) - } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { + err := c.buildSites() + c.handleError("Build failed", errors.WithStack(err)) + if err == nil && !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { livereload.ForceRefresh() } } @@ -839,11 +840,12 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) { c.Logger.FEEDBACK.Printf("Syncing all static files\n") _, err := c.copyStatic() if err != nil { - stopOnErr(c.Logger, err, "Error copying static files to publish dir") + c.handleError("Error copying static files to publish dir", errors.WithStack(err)) + break } } else { if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { - c.Logger.ERROR.Println(err) + c.handleError("Error syncing static files to publish dir", errors.WithStack(err)) continue } } @@ -917,7 +919,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) { } case err := <-watcher.Errors: if err != nil { - c.Logger.ERROR.Println(err) + c.handleError("Error while watching", errors.WithStack(err)) } } } diff --git a/commands/server.go b/commands/server.go index 27999fa6c2a..3954c94a5a3 100644 --- a/commands/server.go +++ b/commands/server.go @@ -316,6 +316,16 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro decorate := func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // First check the error state + err := f.c.getErrorWithContext() + if err != nil { + errTemplate := defaultBuildErrorTemplate + w.WriteHeader(errTemplate.statusCode) // TODO(bep) error server improve + errTemplate.buildErrorPage(err, w) + return + } + if f.s.noHTTPCache { w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") w.Header().Set("Pragma", "no-cache") diff --git a/commands/server_errors.go b/commands/server_errors.go new file mode 100644 index 00000000000..335385eccf7 --- /dev/null +++ b/commands/server_errors.go @@ -0,0 +1,50 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "html/template" + "io" +) + +type errorTemplate struct { + // To text template to produce a nice looking error page in the browser. + templ string + + // The default HTTP status code shown for this error. + statusCode int +} + +var defaultBuildErrorTemplate = errorTemplate{ + templ: `

{{ .Error.What }}

+
{{ .Error.Err }}
+ + +
{{ .Error.Err.StackTrace }}
+ +{{ .Version }} + + +`, + statusCode: 500, +} + +func (templ errorTemplate) buildErrorPage(data interface{}, w io.Writer) error { + t, err := template.New("").Parse(templ.templ) + if err != nil { + return err + } + + return t.Execute(w, data) +} diff --git a/commands/version.go b/commands/version.go index ea4e4c926c0..b85f53725fe 100644 --- a/commands/version.go +++ b/commands/version.go @@ -14,14 +14,16 @@ package commands import ( + "fmt" "runtime" "strings" + jww "github.com/spf13/jwalterweatherman" + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resource/tocss/scss" "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" ) var _ cmder = (*versionCmd)(nil) @@ -45,6 +47,10 @@ func newVersionCmd() *versionCmd { } func printHugoVersion() { + jww.FEEDBACK.Println(hugoVersionString()) +} + +func hugoVersionString() string { program := "Hugo Static Site Generator" version := "v" + helpers.CurrentHugoVersion.String() @@ -64,5 +70,6 @@ func printHugoVersion() { buildDate = "unknown" } - jww.FEEDBACK.Println(program, version, osArch, "BuildDate:", buildDate) + return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, buildDate) + } diff --git a/go.mod b/go.mod index 62f22531fff..6e091524867 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n v1.10.0 github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 + github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba github.com/sanity-io/litter v1.1.0 diff --git a/go.sum b/go.sum index 5a71e5d7690..f9e48640c5e 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,8 @@ github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4J github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba h1:8Vzt8HxRjy7hp1eqPKVoAEPK9npQFW2510qlobGzvi0=