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 }