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 }