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 }