hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

commandeer.go (12671B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package commands
   15 
   16 import (
   17 	"errors"
   18 	"fmt"
   19 	"io/ioutil"
   20 	"net"
   21 	"os"
   22 	"path/filepath"
   23 	"regexp"
   24 	"sync"
   25 	"time"
   26 
   27 	hconfig "github.com/gohugoio/hugo/config"
   28 
   29 	"golang.org/x/sync/semaphore"
   30 
   31 	"github.com/gohugoio/hugo/common/herrors"
   32 	"github.com/gohugoio/hugo/common/htime"
   33 	"github.com/gohugoio/hugo/common/hugo"
   34 	"github.com/gohugoio/hugo/common/paths"
   35 
   36 	"github.com/spf13/cast"
   37 	jww "github.com/spf13/jwalterweatherman"
   38 
   39 	"github.com/gohugoio/hugo/common/loggers"
   40 	"github.com/gohugoio/hugo/config"
   41 
   42 	"github.com/spf13/cobra"
   43 
   44 	"github.com/gohugoio/hugo/hugolib"
   45 	"github.com/spf13/afero"
   46 
   47 	"github.com/bep/clock"
   48 	"github.com/bep/debounce"
   49 	"github.com/bep/overlayfs"
   50 	"github.com/gohugoio/hugo/common/types"
   51 	"github.com/gohugoio/hugo/deps"
   52 	"github.com/gohugoio/hugo/helpers"
   53 	"github.com/gohugoio/hugo/hugofs"
   54 	"github.com/gohugoio/hugo/langs"
   55 )
   56 
   57 type commandeerHugoState struct {
   58 	*deps.DepsCfg
   59 	hugoSites *hugolib.HugoSites
   60 	fsCreate  sync.Once
   61 	created   chan struct{}
   62 }
   63 
   64 type commandeer struct {
   65 	*commandeerHugoState
   66 
   67 	logger       loggers.Logger
   68 	serverConfig *config.Server
   69 
   70 	buildLock func() (unlock func(), err error)
   71 
   72 	// Loading state
   73 	mustHaveConfigFile bool
   74 	failOnInitErr      bool
   75 	running            bool
   76 
   77 	// Currently only set when in "fast render mode". But it seems to
   78 	// be fast enough that we could maybe just add it for all server modes.
   79 	changeDetector *fileChangeDetector
   80 
   81 	// We need to reuse these on server rebuilds.
   82 	// These 2 will be different if --renderStaticToDisk is set.
   83 	publishDirFs       afero.Fs
   84 	publishDirServerFs afero.Fs
   85 
   86 	h    *hugoBuilderCommon
   87 	ftch flagsToConfigHandler
   88 
   89 	visitedURLs *types.EvictingStringQueue
   90 
   91 	cfgInit func(c *commandeer) error
   92 
   93 	// We watch these for changes.
   94 	configFiles []string
   95 
   96 	// Used in cases where we get flooded with events in server mode.
   97 	debounce func(f func())
   98 
   99 	serverPorts []serverPortListener
  100 
  101 	languages          langs.Languages
  102 	doLiveReload       bool
  103 	renderStaticToDisk bool
  104 	fastRenderMode     bool
  105 	showErrorInBrowser bool
  106 	wasError           bool
  107 
  108 	configured bool
  109 	paused     bool
  110 
  111 	fullRebuildSem *semaphore.Weighted
  112 
  113 	// Any error from the last build.
  114 	buildErr error
  115 }
  116 
  117 type serverPortListener struct {
  118 	p  int
  119 	ln net.Listener
  120 }
  121 
  122 func newCommandeerHugoState() *commandeerHugoState {
  123 	return &commandeerHugoState{
  124 		created: make(chan struct{}),
  125 	}
  126 }
  127 
  128 func (c *commandeerHugoState) hugo() *hugolib.HugoSites {
  129 	<-c.created
  130 	return c.hugoSites
  131 }
  132 
  133 func (c *commandeerHugoState) hugoTry() *hugolib.HugoSites {
  134 	select {
  135 	case <-c.created:
  136 		return c.hugoSites
  137 	case <-time.After(time.Millisecond * 100):
  138 		return nil
  139 	}
  140 }
  141 
  142 func (c *commandeer) errCount() int {
  143 	return int(c.logger.LogCounters().ErrorCounter.Count())
  144 }
  145 
  146 func (c *commandeer) getErrorWithContext() any {
  147 	errCount := c.errCount()
  148 
  149 	if errCount == 0 {
  150 		return nil
  151 	}
  152 
  153 	m := make(map[string]any)
  154 
  155 	//xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.logger.Errors())))
  156 	m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.logger.Errors())))
  157 	m["Version"] = hugo.BuildVersionString()
  158 	ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.buildErr)
  159 	m["Files"] = ferrors
  160 
  161 	return m
  162 }
  163 
  164 func (c *commandeer) Set(key string, value any) {
  165 	if c.configured {
  166 		panic("commandeer cannot be changed")
  167 	}
  168 	c.Cfg.Set(key, value)
  169 }
  170 
  171 func (c *commandeer) initFs(fs *hugofs.Fs) error {
  172 	c.publishDirFs = fs.PublishDir
  173 	c.publishDirServerFs = fs.PublishDirServer
  174 	c.DepsCfg.Fs = fs
  175 
  176 	return nil
  177 }
  178 
  179 func (c *commandeer) initClock(loc *time.Location) error {
  180 	bt := c.Cfg.GetString("clock")
  181 	if bt == "" {
  182 		return nil
  183 	}
  184 
  185 	t, err := cast.StringToDateInDefaultLocation(bt, loc)
  186 	if err != nil {
  187 		return fmt.Errorf(`failed to parse "clock" flag: %s`, err)
  188 	}
  189 
  190 	htime.Clock = clock.Start(t)
  191 	return nil
  192 }
  193 
  194 func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
  195 	var rebuildDebouncer func(f func())
  196 	if running {
  197 		// The time value used is tested with mass content replacements in a fairly big Hugo site.
  198 		// It is better to wait for some seconds in those cases rather than get flooded
  199 		// with rebuilds.
  200 		rebuildDebouncer = debounce.New(4 * time.Second)
  201 	}
  202 
  203 	out := ioutil.Discard
  204 	if !h.quiet {
  205 		out = os.Stdout
  206 	}
  207 
  208 	c := &commandeer{
  209 		h:                   h,
  210 		ftch:                f,
  211 		commandeerHugoState: newCommandeerHugoState(),
  212 		cfgInit:             cfgInit,
  213 		visitedURLs:         types.NewEvictingStringQueue(10),
  214 		debounce:            rebuildDebouncer,
  215 		fullRebuildSem:      semaphore.NewWeighted(1),
  216 
  217 		// Init state
  218 		mustHaveConfigFile: mustHaveConfigFile,
  219 		failOnInitErr:      failOnInitErr,
  220 		running:            running,
  221 
  222 		// This will be replaced later, but we need something to log to before the configuration is read.
  223 		logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, ioutil.Discard, running),
  224 	}
  225 
  226 	return c, c.loadConfig()
  227 }
  228 
  229 type fileChangeDetector struct {
  230 	sync.Mutex
  231 	current map[string]string
  232 	prev    map[string]string
  233 
  234 	irrelevantRe *regexp.Regexp
  235 }
  236 
  237 func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
  238 	f.Lock()
  239 	defer f.Unlock()
  240 	f.current[name] = md5sum
  241 }
  242 
  243 func (f *fileChangeDetector) changed() []string {
  244 	if f == nil {
  245 		return nil
  246 	}
  247 	f.Lock()
  248 	defer f.Unlock()
  249 	var c []string
  250 	for k, v := range f.current {
  251 		vv, found := f.prev[k]
  252 		if !found || v != vv {
  253 			c = append(c, k)
  254 		}
  255 	}
  256 
  257 	return f.filterIrrelevant(c)
  258 }
  259 
  260 func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
  261 	var filtered []string
  262 	for _, v := range in {
  263 		if !f.irrelevantRe.MatchString(v) {
  264 			filtered = append(filtered, v)
  265 		}
  266 	}
  267 	return filtered
  268 }
  269 
  270 func (f *fileChangeDetector) PrepareNew() {
  271 	if f == nil {
  272 		return
  273 	}
  274 
  275 	f.Lock()
  276 	defer f.Unlock()
  277 
  278 	if f.current == nil {
  279 		f.current = make(map[string]string)
  280 		f.prev = make(map[string]string)
  281 		return
  282 	}
  283 
  284 	f.prev = make(map[string]string)
  285 	for k, v := range f.current {
  286 		f.prev[k] = v
  287 	}
  288 	f.current = make(map[string]string)
  289 }
  290 
  291 func (c *commandeer) loadConfig() error {
  292 	if c.DepsCfg == nil {
  293 		c.DepsCfg = &deps.DepsCfg{}
  294 	}
  295 
  296 	if c.logger != nil {
  297 		// Truncate the error log if this is a reload.
  298 		c.logger.Reset()
  299 	}
  300 
  301 	cfg := c.DepsCfg
  302 	c.configured = false
  303 	cfg.Running = c.running
  304 
  305 	var dir string
  306 	if c.h.source != "" {
  307 		dir, _ = filepath.Abs(c.h.source)
  308 	} else {
  309 		dir, _ = os.Getwd()
  310 	}
  311 
  312 	var sourceFs afero.Fs = hugofs.Os
  313 	if c.DepsCfg.Fs != nil {
  314 		sourceFs = c.DepsCfg.Fs.Source
  315 	}
  316 
  317 	environment := c.h.getEnvironment(c.running)
  318 
  319 	doWithConfig := func(cfg config.Provider) error {
  320 		if c.ftch != nil {
  321 			c.ftch.flagsToConfig(cfg)
  322 		}
  323 
  324 		cfg.Set("workingDir", dir)
  325 		cfg.Set("environment", environment)
  326 		return nil
  327 	}
  328 
  329 	cfgSetAndInit := func(cfg config.Provider) error {
  330 		c.Cfg = cfg
  331 		if c.cfgInit == nil {
  332 			return nil
  333 		}
  334 		err := c.cfgInit(c)
  335 		return err
  336 	}
  337 
  338 	configPath := c.h.source
  339 	if configPath == "" {
  340 		configPath = dir
  341 	}
  342 	config, configFiles, err := hugolib.LoadConfig(
  343 		hugolib.ConfigSourceDescriptor{
  344 			Fs:           sourceFs,
  345 			Logger:       c.logger,
  346 			Path:         configPath,
  347 			WorkingDir:   dir,
  348 			Filename:     c.h.cfgFile,
  349 			AbsConfigDir: c.h.getConfigDir(dir),
  350 			Environment:  environment,
  351 		},
  352 		cfgSetAndInit,
  353 		doWithConfig)
  354 
  355 	if err != nil {
  356 		// We should improve the error handling here,
  357 		// but with hugo mod init and similar there is a chicken and egg situation
  358 		// with modules already configured in config.toml, so ignore those errors.
  359 		if c.mustHaveConfigFile || (c.failOnInitErr && !moduleNotFoundRe.MatchString(err.Error())) {
  360 			return err
  361 		} else {
  362 			// Just make it a warning.
  363 			c.logger.Warnln(err)
  364 		}
  365 	} else if c.mustHaveConfigFile && len(configFiles) == 0 {
  366 		return hugolib.ErrNoConfigFile
  367 	}
  368 
  369 	c.configFiles = configFiles
  370 
  371 	var ok bool
  372 	loc := time.Local
  373 	c.languages, ok = c.Cfg.Get("languagesSorted").(langs.Languages)
  374 	if ok {
  375 		loc = langs.GetLocation(c.languages[0])
  376 	}
  377 
  378 	err = c.initClock(loc)
  379 	if err != nil {
  380 		return err
  381 	}
  382 
  383 	// Set some commonly used flags
  384 	c.doLiveReload = c.running && !c.Cfg.GetBool("disableLiveReload")
  385 	c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
  386 	c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
  387 
  388 	// This is potentially double work, but we need to do this one more time now
  389 	// that all the languages have been configured.
  390 	if c.cfgInit != nil {
  391 		if err := c.cfgInit(c); err != nil {
  392 			return err
  393 		}
  394 	}
  395 
  396 	logger, err := c.createLogger(config)
  397 	if err != nil {
  398 		return err
  399 	}
  400 
  401 	cfg.Logger = logger
  402 	c.logger = logger
  403 	c.serverConfig, err = hconfig.DecodeServer(cfg.Cfg)
  404 	if err != nil {
  405 		return err
  406 	}
  407 
  408 	createMemFs := config.GetBool("renderToMemory")
  409 	c.renderStaticToDisk = config.GetBool("renderStaticToDisk")
  410 
  411 	if createMemFs {
  412 		// Rendering to memoryFS, publish to Root regardless of publishDir.
  413 		config.Set("publishDir", "/")
  414 		config.Set("publishDirStatic", "/")
  415 	} else if c.renderStaticToDisk {
  416 		// Hybrid, render dynamic content to Root.
  417 		config.Set("publishDirStatic", config.Get("publishDir"))
  418 		config.Set("publishDir", "/")
  419 
  420 	}
  421 
  422 	c.fsCreate.Do(func() {
  423 		// Assume both source and destination are using same filesystem.
  424 		fs := hugofs.NewFromSourceAndDestination(sourceFs, sourceFs, config)
  425 
  426 		if c.publishDirFs != nil {
  427 			// Need to reuse the destination on server rebuilds.
  428 			fs.PublishDir = c.publishDirFs
  429 			fs.PublishDirServer = c.publishDirServerFs
  430 		} else {
  431 			if c.renderStaticToDisk {
  432 				publishDirStatic := config.GetString("publishDirStatic")
  433 				workingDir := config.GetString("workingDir")
  434 				absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
  435 
  436 				fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
  437 				// Writes the dynamic output to memory,
  438 				// while serve others directly from /public on disk.
  439 				dynamicFs := fs.PublishDir
  440 				staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
  441 
  442 				// Serve from both the static and dynamic fs,
  443 				// the first will take priority.
  444 				// THis is a read-only filesystem,
  445 				// we do all the writes to
  446 				// fs.Destination and fs.DestinationStatic.
  447 				fs.PublishDirServer = overlayfs.New(
  448 					overlayfs.Options{
  449 						Fss: []afero.Fs{
  450 							dynamicFs,
  451 							staticFs,
  452 						},
  453 					},
  454 				)
  455 				fs.PublishDirStatic = staticFs
  456 			} else if createMemFs {
  457 				// Hugo writes the output to memory instead of the disk.
  458 				fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
  459 			}
  460 		}
  461 
  462 		if c.fastRenderMode {
  463 			// For now, fast render mode only. It should, however, be fast enough
  464 			// for the full variant, too.
  465 			changeDetector := &fileChangeDetector{
  466 				// We use this detector to decide to do a Hot reload of a single path or not.
  467 				// We need to filter out source maps and possibly some other to be able
  468 				// to make that decision.
  469 				irrelevantRe: regexp.MustCompile(`\.map$`),
  470 			}
  471 
  472 			changeDetector.PrepareNew()
  473 			fs.PublishDir = hugofs.NewHashingFs(fs.PublishDir, changeDetector)
  474 			fs.PublishDirStatic = hugofs.NewHashingFs(fs.PublishDirStatic, changeDetector)
  475 			c.changeDetector = changeDetector
  476 		}
  477 
  478 		if c.Cfg.GetBool("logPathWarnings") {
  479 			// Note that we only care about the "dynamic creates" here,
  480 			// so skip the static fs.
  481 			fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
  482 		}
  483 
  484 		// To debug hard-to-find path issues.
  485 		// fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
  486 
  487 		err = c.initFs(fs)
  488 		if err != nil {
  489 			close(c.created)
  490 			return
  491 		}
  492 
  493 		var h *hugolib.HugoSites
  494 
  495 		var createErr error
  496 		h, createErr = hugolib.NewHugoSites(*c.DepsCfg)
  497 		if h == nil || c.failOnInitErr {
  498 			err = createErr
  499 		}
  500 
  501 		c.hugoSites = h
  502 		// TODO(bep) improve.
  503 		if c.buildLock == nil && h != nil {
  504 			c.buildLock = h.LockBuild
  505 		}
  506 		close(c.created)
  507 	})
  508 
  509 	if err != nil {
  510 		return err
  511 	}
  512 
  513 	cacheDir, err := helpers.GetCacheDir(sourceFs, config)
  514 	if err != nil {
  515 		return err
  516 	}
  517 	config.Set("cacheDir", cacheDir)
  518 
  519 	return nil
  520 }