diff options
Diffstat (limited to 'commands/commandeer.go')
-rw-r--r-- | commands/commandeer.go | 525 |
1 files changed, 525 insertions, 0 deletions
diff --git a/commands/commandeer.go b/commands/commandeer.go new file mode 100644 index 000000000..80d4c55d0 --- /dev/null +++ b/commands/commandeer.go @@ -0,0 +1,525 @@ +// 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. + publishDirFs afero.Fs + publishDirStaticFs 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.publishDirStaticFs = fs.PublishDirStatic + 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 + loc := time.Local + c.languages, ok = c.Cfg.Get("languagesSorted").(langs.Languages) + if ok { + loc = langs.GetLocation(c.languages[0]) + } + + err = c.initClock(loc) + if err != nil { + return err + } + + // Set some commonly used flags + c.doLiveReload = c.running && !c.Cfg.GetBool("disableLiveReload") + c.fastRenderMode = c.running && !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") + // TODO(bep) we/I really need to look at the config set up, but to prevent changing too much + // we store away the original. + config.Set("publishDirOrig", config.GetString("publishDir")) + + 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.PublishDirStatic = c.publishDirStaticFs + 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 +} |