hugo/commands/commandeer.go

520 lines
12 KiB
Go

// Copyright 2019 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 (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"regexp"
"sync"
"time"
hconfig "github.com/gohugoio/hugo/config"
"golang.org/x/sync/semaphore"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/spf13/cobra"
"github.com/gohugoio/hugo/hugolib"
"github.com/spf13/afero"
"github.com/bep/clock"
"github.com/bep/debounce"
"github.com/bep/overlayfs"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs"
)
type commandeerHugoState struct {
*deps.DepsCfg
hugoSites *hugolib.HugoSites
fsCreate sync.Once
created chan struct{}
}
type commandeer struct {
*commandeerHugoState
logger loggers.Logger
serverConfig *config.Server
buildLock func() (unlock func(), err error)
// Loading state
mustHaveConfigFile bool
failOnInitErr bool
running bool
// Currently only set when in "fast render mode". But it seems to
// be fast enough that we could maybe just add it for all server modes.
changeDetector *fileChangeDetector
// We need to reuse these on server rebuilds.
// These 2 will be different if --renderStaticToDisk is set.
publishDirFs afero.Fs
publishDirServerFs afero.Fs
h *hugoBuilderCommon
ftch flagsToConfigHandler
visitedURLs *types.EvictingStringQueue
cfgInit func(c *commandeer) error
// We watch these for changes.
configFiles []string
// Used in cases where we get flooded with events in server mode.
debounce func(f func())
serverPorts []serverPortListener
languages langs.Languages
doLiveReload bool
renderStaticToDisk bool
fastRenderMode bool
showErrorInBrowser bool
wasError bool
configured bool
paused bool
fullRebuildSem *semaphore.Weighted
// Any error from the last build.
buildErr error
}
type serverPortListener struct {
p int
ln net.Listener
}
func newCommandeerHugoState() *commandeerHugoState {
return &commandeerHugoState{
created: make(chan struct{}),
}
}
func (c *commandeerHugoState) hugo() *hugolib.HugoSites {
<-c.created
return c.hugoSites
}
func (c *commandeerHugoState) hugoTry() *hugolib.HugoSites {
select {
case <-c.created:
return c.hugoSites
case <-time.After(time.Millisecond * 100):
return nil
}
}
func (c *commandeer) errCount() int {
return int(c.logger.LogCounters().ErrorCounter.Count())
}
func (c *commandeer) getErrorWithContext() any {
errCount := c.errCount()
if errCount == 0 {
return nil
}
m := make(map[string]any)
//xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.logger.Errors())))
m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.logger.Errors())))
m["Version"] = hugo.BuildVersionString()
ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.buildErr)
m["Files"] = ferrors
return m
}
func (c *commandeer) Set(key string, value any) {
if c.configured {
panic("commandeer cannot be changed")
}
c.Cfg.Set(key, value)
}
func (c *commandeer) initFs(fs *hugofs.Fs) error {
c.publishDirFs = fs.PublishDir
c.publishDirServerFs = fs.PublishDirServer
c.DepsCfg.Fs = fs
return nil
}
func (c *commandeer) initClock(loc *time.Location) error {
bt := c.Cfg.GetString("clock")
if bt == "" {
return nil
}
t, err := cast.StringToDateInDefaultLocation(bt, loc)
if err != nil {
return fmt.Errorf(`failed to parse "clock" flag: %s`, err)
}
htime.Clock = clock.Start(t)
return nil
}
func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
var rebuildDebouncer func(f func())
if running {
// The time value used is tested with mass content replacements in a fairly big Hugo site.
// It is better to wait for some seconds in those cases rather than get flooded
// with rebuilds.
rebuildDebouncer = debounce.New(4 * time.Second)
}
out := ioutil.Discard
if !h.quiet {
out = os.Stdout
}
c := &commandeer{
h: h,
ftch: f,
commandeerHugoState: newCommandeerHugoState(),
cfgInit: cfgInit,
visitedURLs: types.NewEvictingStringQueue(10),
debounce: rebuildDebouncer,
fullRebuildSem: semaphore.NewWeighted(1),
// Init state
mustHaveConfigFile: mustHaveConfigFile,
failOnInitErr: failOnInitErr,
running: running,
// This will be replaced later, but we need something to log to before the configuration is read.
logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, ioutil.Discard, running),
}
return c, c.loadConfig()
}
type fileChangeDetector struct {
sync.Mutex
current map[string]string
prev map[string]string
irrelevantRe *regexp.Regexp
}
func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
f.Lock()
defer f.Unlock()
f.current[name] = md5sum
}
func (f *fileChangeDetector) changed() []string {
if f == nil {
return nil
}
f.Lock()
defer f.Unlock()
var c []string
for k, v := range f.current {
vv, found := f.prev[k]
if !found || v != vv {
c = append(c, k)
}
}
return f.filterIrrelevant(c)
}
func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
var filtered []string
for _, v := range in {
if !f.irrelevantRe.MatchString(v) {
filtered = append(filtered, v)
}
}
return filtered
}
func (f *fileChangeDetector) PrepareNew() {
if f == nil {
return
}
f.Lock()
defer f.Unlock()
if f.current == nil {
f.current = make(map[string]string)
f.prev = make(map[string]string)
return
}
f.prev = make(map[string]string)
for k, v := range f.current {
f.prev[k] = v
}
f.current = make(map[string]string)
}
func (c *commandeer) loadConfig() error {
if c.DepsCfg == nil {
c.DepsCfg = &deps.DepsCfg{}
}
if c.logger != nil {
// Truncate the error log if this is a reload.
c.logger.Reset()
}
cfg := c.DepsCfg
c.configured = false
cfg.Running = c.running
var dir string
if c.h.source != "" {
dir, _ = filepath.Abs(c.h.source)
} else {
dir, _ = os.Getwd()
}
var sourceFs afero.Fs = hugofs.Os
if c.DepsCfg.Fs != nil {
sourceFs = c.DepsCfg.Fs.Source
}
environment := c.h.getEnvironment(c.running)
doWithConfig := func(cfg config.Provider) error {
if c.ftch != nil {
c.ftch.flagsToConfig(cfg)
}
cfg.Set("workingDir", dir)
cfg.Set("environment", environment)
return nil
}
cfgSetAndInit := func(cfg config.Provider) error {
c.Cfg = cfg
if c.cfgInit == nil {
return nil
}
err := c.cfgInit(c)
return err
}
configPath := c.h.source
if configPath == "" {
configPath = dir
}
config, configFiles, err := hugolib.LoadConfig(
hugolib.ConfigSourceDescriptor{
Fs: sourceFs,
Logger: c.logger,
Path: configPath,
WorkingDir: dir,
Filename: c.h.cfgFile,
AbsConfigDir: c.h.getConfigDir(dir),
Environment: environment,
},
cfgSetAndInit,
doWithConfig)
if err != nil {
// We should improve the error handling here,
// but with hugo mod init and similar there is a chicken and egg situation
// with modules already configured in config.toml, so ignore those errors.
if c.mustHaveConfigFile || (c.failOnInitErr && !moduleNotFoundRe.MatchString(err.Error())) {
return err
} else {
// Just make it a warning.
c.logger.Warnln(err)
}
} else if c.mustHaveConfigFile && len(configFiles) == 0 {
return hugolib.ErrNoConfigFile
}
c.configFiles = configFiles
var ok bool
c.languages, ok = c.Cfg.Get("languagesSorted").(langs.Languages)
if !ok {
panic("languages not configured")
}
err = c.initClock(langs.GetLocation(c.languages[0]))
if err != nil {
return err
}
// Set some commonly used flags
c.doLiveReload = c.running && !c.Cfg.GetBool("disableLiveReload")
c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
// This is potentially double work, but we need to do this one more time now
// that all the languages have been configured.
if c.cfgInit != nil {
if err := c.cfgInit(c); err != nil {
return err
}
}
logger, err := c.createLogger(config)
if err != nil {
return err
}
cfg.Logger = logger
c.logger = logger
c.serverConfig, err = hconfig.DecodeServer(cfg.Cfg)
if err != nil {
return err
}
createMemFs := config.GetBool("renderToMemory")
c.renderStaticToDisk = config.GetBool("renderStaticToDisk")
if createMemFs {
// Rendering to memoryFS, publish to Root regardless of publishDir.
config.Set("publishDir", "/")
config.Set("publishDirStatic", "/")
} else if c.renderStaticToDisk {
// Hybrid, render dynamic content to Root.
config.Set("publishDirStatic", config.Get("publishDir"))
config.Set("publishDir", "/")
}
c.fsCreate.Do(func() {
// Assume both source and destination are using same filesystem.
fs := hugofs.NewFromSourceAndDestination(sourceFs, sourceFs, config)
if c.publishDirFs != nil {
// Need to reuse the destination on server rebuilds.
fs.PublishDir = c.publishDirFs
fs.PublishDirServer = c.publishDirServerFs
} else {
if c.renderStaticToDisk {
publishDirStatic := config.GetString("publishDirStatic")
workingDir := config.GetString("workingDir")
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
// Writes the dynamic output to memory,
// while serve others directly from /public on disk.
dynamicFs := fs.PublishDir
staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
// Serve from both the static and dynamic fs,
// the first will take priority.
// THis is a read-only filesystem,
// we do all the writes to
// fs.Destination and fs.DestinationStatic.
fs.PublishDirServer = overlayfs.New(
overlayfs.Options{
Fss: []afero.Fs{
dynamicFs,
staticFs,
},
},
)
fs.PublishDirStatic = staticFs
} else if createMemFs {
// Hugo writes the output to memory instead of the disk.
fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
}
}
if c.fastRenderMode {
// For now, fast render mode only. It should, however, be fast enough
// for the full variant, too.
changeDetector := &fileChangeDetector{
// We use this detector to decide to do a Hot reload of a single path or not.
// We need to filter out source maps and possibly some other to be able
// to make that decision.
irrelevantRe: regexp.MustCompile(`\.map$`),
}
changeDetector.PrepareNew()
fs.PublishDir = hugofs.NewHashingFs(fs.PublishDir, changeDetector)
fs.PublishDirStatic = hugofs.NewHashingFs(fs.PublishDirStatic, changeDetector)
c.changeDetector = changeDetector
}
if c.Cfg.GetBool("logPathWarnings") {
// Note that we only care about the "dynamic creates" here,
// so skip the static fs.
fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
}
// To debug hard-to-find path issues.
// fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
err = c.initFs(fs)
if err != nil {
close(c.created)
return
}
var h *hugolib.HugoSites
var createErr error
h, createErr = hugolib.NewHugoSites(*c.DepsCfg)
if h == nil || c.failOnInitErr {
err = createErr
}
c.hugoSites = h
// TODO(bep) improve.
if c.buildLock == nil && h != nil {
c.buildLock = h.LockBuild
}
close(c.created)
})
if err != nil {
return err
}
cacheDir, err := helpers.GetCacheDir(sourceFs, config)
if err != nil {
return err
}
config.Set("cacheDir", cacheDir)
return nil
}