github.com/gohugoio/hugo@v0.88.1/commands/commandeer.go (about)

     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  	"bytes"
    18  	"errors"
    19  	"io/ioutil"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"sync"
    24  	"time"
    25  
    26  	hconfig "github.com/gohugoio/hugo/config"
    27  
    28  	"golang.org/x/sync/semaphore"
    29  
    30  	"github.com/gohugoio/hugo/common/herrors"
    31  	"github.com/gohugoio/hugo/common/hugo"
    32  
    33  	jww "github.com/spf13/jwalterweatherman"
    34  
    35  	"github.com/gohugoio/hugo/common/loggers"
    36  	"github.com/gohugoio/hugo/config"
    37  
    38  	"github.com/spf13/cobra"
    39  
    40  	"github.com/gohugoio/hugo/hugolib"
    41  	"github.com/spf13/afero"
    42  
    43  	"github.com/bep/debounce"
    44  	"github.com/gohugoio/hugo/common/types"
    45  	"github.com/gohugoio/hugo/deps"
    46  	"github.com/gohugoio/hugo/helpers"
    47  	"github.com/gohugoio/hugo/hugofs"
    48  	"github.com/gohugoio/hugo/langs"
    49  )
    50  
    51  type commandeerHugoState struct {
    52  	*deps.DepsCfg
    53  	hugoSites *hugolib.HugoSites
    54  	fsCreate  sync.Once
    55  	created   chan struct{}
    56  }
    57  
    58  type commandeer struct {
    59  	*commandeerHugoState
    60  
    61  	logger       loggers.Logger
    62  	serverConfig *config.Server
    63  
    64  	// Loading state
    65  	mustHaveConfigFile bool
    66  	failOnInitErr      bool
    67  	running            bool
    68  
    69  	// Currently only set when in "fast render mode". But it seems to
    70  	// be fast enough that we could maybe just add it for all server modes.
    71  	changeDetector *fileChangeDetector
    72  
    73  	// We need to reuse this on server rebuilds.
    74  	destinationFs afero.Fs
    75  
    76  	h    *hugoBuilderCommon
    77  	ftch flagsToConfigHandler
    78  
    79  	visitedURLs *types.EvictingStringQueue
    80  
    81  	cfgInit func(c *commandeer) error
    82  
    83  	// We watch these for changes.
    84  	configFiles []string
    85  
    86  	// Used in cases where we get flooded with events in server mode.
    87  	debounce func(f func())
    88  
    89  	serverPorts         []int
    90  	languagesConfigured bool
    91  	languages           langs.Languages
    92  	doLiveReload        bool
    93  	fastRenderMode      bool
    94  	showErrorInBrowser  bool
    95  	wasError            bool
    96  
    97  	configured bool
    98  	paused     bool
    99  
   100  	fullRebuildSem *semaphore.Weighted
   101  
   102  	// Any error from the last build.
   103  	buildErr error
   104  }
   105  
   106  func newCommandeerHugoState() *commandeerHugoState {
   107  	return &commandeerHugoState{
   108  		created: make(chan struct{}),
   109  	}
   110  }
   111  
   112  func (c *commandeerHugoState) hugo() *hugolib.HugoSites {
   113  	<-c.created
   114  	return c.hugoSites
   115  }
   116  
   117  func (c *commandeer) errCount() int {
   118  	return int(c.logger.LogCounters().ErrorCounter.Count())
   119  }
   120  
   121  func (c *commandeer) getErrorWithContext() interface{} {
   122  	errCount := c.errCount()
   123  
   124  	if errCount == 0 {
   125  		return nil
   126  	}
   127  
   128  	m := make(map[string]interface{})
   129  
   130  	m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors()))
   131  	m["Version"] = hugo.BuildVersionString()
   132  
   133  	fe := herrors.UnwrapErrorWithFileContext(c.buildErr)
   134  	if fe != nil {
   135  		m["File"] = fe
   136  	}
   137  
   138  	if c.h.verbose {
   139  		var b bytes.Buffer
   140  		herrors.FprintStackTraceFromErr(&b, c.buildErr)
   141  		m["StackTrace"] = b.String()
   142  	}
   143  
   144  	return m
   145  }
   146  
   147  func (c *commandeer) Set(key string, value interface{}) {
   148  	if c.configured {
   149  		panic("commandeer cannot be changed")
   150  	}
   151  	c.Cfg.Set(key, value)
   152  }
   153  
   154  func (c *commandeer) initFs(fs *hugofs.Fs) error {
   155  	c.destinationFs = fs.Destination
   156  	c.DepsCfg.Fs = fs
   157  
   158  	return nil
   159  }
   160  
   161  func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
   162  	var rebuildDebouncer func(f func())
   163  	if running {
   164  		// The time value used is tested with mass content replacements in a fairly big Hugo site.
   165  		// It is better to wait for some seconds in those cases rather than get flooded
   166  		// with rebuilds.
   167  		rebuildDebouncer = debounce.New(4 * time.Second)
   168  	}
   169  
   170  	out := ioutil.Discard
   171  	if !h.quiet {
   172  		out = os.Stdout
   173  	}
   174  
   175  	c := &commandeer{
   176  		h:                   h,
   177  		ftch:                f,
   178  		commandeerHugoState: newCommandeerHugoState(),
   179  		cfgInit:             cfgInit,
   180  		visitedURLs:         types.NewEvictingStringQueue(10),
   181  		debounce:            rebuildDebouncer,
   182  		fullRebuildSem:      semaphore.NewWeighted(1),
   183  
   184  		// Init state
   185  		mustHaveConfigFile: mustHaveConfigFile,
   186  		failOnInitErr:      failOnInitErr,
   187  		running:            running,
   188  
   189  		// This will be replaced later, but we need something to log to before the configuration is read.
   190  		logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, ioutil.Discard, running),
   191  	}
   192  
   193  	return c, c.loadConfig()
   194  }
   195  
   196  type fileChangeDetector struct {
   197  	sync.Mutex
   198  	current map[string]string
   199  	prev    map[string]string
   200  
   201  	irrelevantRe *regexp.Regexp
   202  }
   203  
   204  func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
   205  	f.Lock()
   206  	defer f.Unlock()
   207  	f.current[name] = md5sum
   208  }
   209  
   210  func (f *fileChangeDetector) changed() []string {
   211  	if f == nil {
   212  		return nil
   213  	}
   214  	f.Lock()
   215  	defer f.Unlock()
   216  	var c []string
   217  	for k, v := range f.current {
   218  		vv, found := f.prev[k]
   219  		if !found || v != vv {
   220  			c = append(c, k)
   221  		}
   222  	}
   223  
   224  	return f.filterIrrelevant(c)
   225  }
   226  
   227  func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
   228  	var filtered []string
   229  	for _, v := range in {
   230  		if !f.irrelevantRe.MatchString(v) {
   231  			filtered = append(filtered, v)
   232  		}
   233  	}
   234  	return filtered
   235  }
   236  
   237  func (f *fileChangeDetector) PrepareNew() {
   238  	if f == nil {
   239  		return
   240  	}
   241  
   242  	f.Lock()
   243  	defer f.Unlock()
   244  
   245  	if f.current == nil {
   246  		f.current = make(map[string]string)
   247  		f.prev = make(map[string]string)
   248  		return
   249  	}
   250  
   251  	f.prev = make(map[string]string)
   252  	for k, v := range f.current {
   253  		f.prev[k] = v
   254  	}
   255  	f.current = make(map[string]string)
   256  }
   257  
   258  func (c *commandeer) loadConfig() error {
   259  	if c.DepsCfg == nil {
   260  		c.DepsCfg = &deps.DepsCfg{}
   261  	}
   262  
   263  	if c.logger != nil {
   264  		// Truncate the error log if this is a reload.
   265  		c.logger.Reset()
   266  	}
   267  
   268  	cfg := c.DepsCfg
   269  	c.configured = false
   270  	cfg.Running = c.running
   271  
   272  	var dir string
   273  	if c.h.source != "" {
   274  		dir, _ = filepath.Abs(c.h.source)
   275  	} else {
   276  		dir, _ = os.Getwd()
   277  	}
   278  
   279  	var sourceFs afero.Fs = hugofs.Os
   280  	if c.DepsCfg.Fs != nil {
   281  		sourceFs = c.DepsCfg.Fs.Source
   282  	}
   283  
   284  	environment := c.h.getEnvironment(c.running)
   285  
   286  	doWithConfig := func(cfg config.Provider) error {
   287  		if c.ftch != nil {
   288  			c.ftch.flagsToConfig(cfg)
   289  		}
   290  
   291  		cfg.Set("workingDir", dir)
   292  		cfg.Set("environment", environment)
   293  		return nil
   294  	}
   295  
   296  	cfgSetAndInit := func(cfg config.Provider) error {
   297  		c.Cfg = cfg
   298  		if c.cfgInit == nil {
   299  			return nil
   300  		}
   301  		err := c.cfgInit(c)
   302  		return err
   303  	}
   304  
   305  	configPath := c.h.source
   306  	if configPath == "" {
   307  		configPath = dir
   308  	}
   309  	config, configFiles, err := hugolib.LoadConfig(
   310  		hugolib.ConfigSourceDescriptor{
   311  			Fs:           sourceFs,
   312  			Logger:       c.logger,
   313  			Path:         configPath,
   314  			WorkingDir:   dir,
   315  			Filename:     c.h.cfgFile,
   316  			AbsConfigDir: c.h.getConfigDir(dir),
   317  			Environment:  environment,
   318  		},
   319  		cfgSetAndInit,
   320  		doWithConfig)
   321  
   322  	if err != nil {
   323  		// We should improve the error handling here,
   324  		// but with hugo mod init and similar there is a chicken and egg situation
   325  		// with modules already configured in config.toml, so ignore those errors.
   326  		if c.mustHaveConfigFile || !moduleNotFoundRe.MatchString(err.Error()) {
   327  			return err
   328  		}
   329  	} else if c.mustHaveConfigFile && len(configFiles) == 0 {
   330  		return hugolib.ErrNoConfigFile
   331  	}
   332  
   333  	c.configFiles = configFiles
   334  
   335  	if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
   336  		c.languagesConfigured = true
   337  		c.languages = l
   338  	}
   339  
   340  	// Set some commonly used flags
   341  	c.doLiveReload = c.running && !c.Cfg.GetBool("disableLiveReload")
   342  	c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
   343  	c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
   344  
   345  	// This is potentially double work, but we need to do this one more time now
   346  	// that all the languages have been configured.
   347  	if c.cfgInit != nil {
   348  		if err := c.cfgInit(c); err != nil {
   349  			return err
   350  		}
   351  	}
   352  
   353  	logger, err := c.createLogger(config)
   354  	if err != nil {
   355  		return err
   356  	}
   357  
   358  	cfg.Logger = logger
   359  	c.logger = logger
   360  	c.serverConfig, err = hconfig.DecodeServer(cfg.Cfg)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	createMemFs := config.GetBool("renderToMemory")
   366  
   367  	if createMemFs {
   368  		// Rendering to memoryFS, publish to Root regardless of publishDir.
   369  		config.Set("publishDir", "/")
   370  	}
   371  
   372  	c.fsCreate.Do(func() {
   373  		fs := hugofs.NewFrom(sourceFs, config)
   374  
   375  		if c.destinationFs != nil {
   376  			// Need to reuse the destination on server rebuilds.
   377  			fs.Destination = c.destinationFs
   378  		} else if createMemFs {
   379  			// Hugo writes the output to memory instead of the disk.
   380  			fs.Destination = new(afero.MemMapFs)
   381  		}
   382  
   383  		if c.fastRenderMode {
   384  			// For now, fast render mode only. It should, however, be fast enough
   385  			// for the full variant, too.
   386  			changeDetector := &fileChangeDetector{
   387  				// We use this detector to decide to do a Hot reload of a single path or not.
   388  				// We need to filter out source maps and possibly some other to be able
   389  				// to make that decision.
   390  				irrelevantRe: regexp.MustCompile(`\.map$`),
   391  			}
   392  
   393  			changeDetector.PrepareNew()
   394  			fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
   395  			c.changeDetector = changeDetector
   396  		}
   397  
   398  		if c.Cfg.GetBool("logPathWarnings") {
   399  			fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
   400  		}
   401  
   402  		// To debug hard-to-find path issues.
   403  		// fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
   404  
   405  		err = c.initFs(fs)
   406  		if err != nil {
   407  			close(c.created)
   408  			return
   409  		}
   410  
   411  		var h *hugolib.HugoSites
   412  
   413  		var createErr error
   414  		h, createErr = hugolib.NewHugoSites(*c.DepsCfg)
   415  		if h == nil || c.failOnInitErr {
   416  			err = createErr
   417  		}
   418  		c.hugoSites = h
   419  		close(c.created)
   420  	})
   421  
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	cacheDir, err := helpers.GetCacheDir(sourceFs, config)
   427  	if err != nil {
   428  		return err
   429  	}
   430  	config.Set("cacheDir", cacheDir)
   431  
   432  	return nil
   433  }