github.com/gohugoio/hugo@v0.88.1/commands/hugo.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 defines and implements command-line commands and flags 15 // used by Hugo. Commands and flags are implemented using Cobra. 16 package commands 17 18 import ( 19 "context" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "os/signal" 24 "path/filepath" 25 "runtime" 26 "runtime/pprof" 27 "runtime/trace" 28 "strings" 29 "sync/atomic" 30 "syscall" 31 "time" 32 33 "github.com/gohugoio/hugo/common/types" 34 35 "github.com/gohugoio/hugo/hugofs" 36 37 "github.com/gohugoio/hugo/resources/page" 38 39 "github.com/pkg/errors" 40 41 "github.com/gohugoio/hugo/common/herrors" 42 "github.com/gohugoio/hugo/common/hugo" 43 "github.com/gohugoio/hugo/common/loggers" 44 "github.com/gohugoio/hugo/common/terminal" 45 46 "github.com/gohugoio/hugo/hugolib/filesystems" 47 48 "golang.org/x/sync/errgroup" 49 50 "github.com/gohugoio/hugo/config" 51 52 flag "github.com/spf13/pflag" 53 54 "github.com/fsnotify/fsnotify" 55 "github.com/gohugoio/hugo/helpers" 56 "github.com/gohugoio/hugo/hugolib" 57 "github.com/gohugoio/hugo/livereload" 58 "github.com/gohugoio/hugo/watcher" 59 "github.com/spf13/afero" 60 "github.com/spf13/cobra" 61 "github.com/spf13/fsync" 62 jww "github.com/spf13/jwalterweatherman" 63 ) 64 65 // The Response value from Execute. 66 type Response struct { 67 // The build Result will only be set in the hugo build command. 68 Result *hugolib.HugoSites 69 70 // Err is set when the command failed to execute. 71 Err error 72 73 // The command that was executed. 74 Cmd *cobra.Command 75 } 76 77 // IsUserError returns true is the Response error is a user error rather than a 78 // system error. 79 func (r Response) IsUserError() bool { 80 return r.Err != nil && isUserError(r.Err) 81 } 82 83 // Execute adds all child commands to the root command HugoCmd and sets flags appropriately. 84 // The args are usually filled with os.Args[1:]. 85 func Execute(args []string) Response { 86 hugoCmd := newCommandsBuilder().addAll().build() 87 cmd := hugoCmd.getCommand() 88 cmd.SetArgs(args) 89 90 c, err := cmd.ExecuteC() 91 92 var resp Response 93 94 if c == cmd && hugoCmd.c != nil { 95 // Root command executed 96 resp.Result = hugoCmd.c.hugo() 97 } 98 99 if err == nil { 100 errCount := int(loggers.GlobalErrorCounter.Count()) 101 if errCount > 0 { 102 err = fmt.Errorf("logged %d errors", errCount) 103 } else if resp.Result != nil { 104 errCount = resp.Result.NumLogErrors() 105 if errCount > 0 { 106 err = fmt.Errorf("logged %d errors", errCount) 107 } 108 } 109 110 } 111 112 resp.Err = err 113 resp.Cmd = c 114 115 return resp 116 } 117 118 // InitializeConfig initializes a config file with sensible default configuration flags. 119 func initializeConfig(mustHaveConfigFile, failOnInitErr, running bool, 120 h *hugoBuilderCommon, 121 f flagsToConfigHandler, 122 cfgInit func(c *commandeer) error) (*commandeer, error) { 123 c, err := newCommandeer(mustHaveConfigFile, failOnInitErr, running, h, f, cfgInit) 124 if err != nil { 125 return nil, err 126 } 127 128 return c, nil 129 } 130 131 func (c *commandeer) createLogger(cfg config.Provider) (loggers.Logger, error) { 132 var ( 133 logHandle = ioutil.Discard 134 logThreshold = jww.LevelWarn 135 logFile = cfg.GetString("logFile") 136 outHandle = ioutil.Discard 137 stdoutThreshold = jww.LevelWarn 138 ) 139 140 if !c.h.quiet { 141 outHandle = os.Stdout 142 } 143 144 if c.h.verboseLog || c.h.logging || (c.h.logFile != "") { 145 var err error 146 if logFile != "" { 147 logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) 148 if err != nil { 149 return nil, newSystemError("Failed to open log file:", logFile, err) 150 } 151 } else { 152 logHandle, err = ioutil.TempFile("", "hugo") 153 if err != nil { 154 return nil, newSystemError(err) 155 } 156 } 157 } else if !c.h.quiet && cfg.GetBool("verbose") { 158 stdoutThreshold = jww.LevelInfo 159 } 160 161 if cfg.GetBool("debug") { 162 stdoutThreshold = jww.LevelDebug 163 } 164 165 if c.h.verboseLog { 166 logThreshold = jww.LevelInfo 167 if cfg.GetBool("debug") { 168 logThreshold = jww.LevelDebug 169 } 170 } 171 172 loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle) 173 helpers.InitLoggers() 174 175 return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, c.running), nil 176 } 177 178 func initializeFlags(cmd *cobra.Command, cfg config.Provider) { 179 persFlagKeys := []string{ 180 "debug", 181 "verbose", 182 "logFile", 183 // Moved from vars 184 } 185 flagKeys := []string{ 186 "cleanDestinationDir", 187 "buildDrafts", 188 "buildFuture", 189 "buildExpired", 190 "uglyURLs", 191 "canonifyURLs", 192 "enableRobotsTXT", 193 "enableGitInfo", 194 "pluralizeListTitles", 195 "preserveTaxonomyNames", 196 "ignoreCache", 197 "forceSyncStatic", 198 "noTimes", 199 "noChmod", 200 "ignoreVendor", 201 "ignoreVendorPaths", 202 "templateMetrics", 203 "templateMetricsHints", 204 205 // Moved from vars. 206 "baseURL", 207 "buildWatch", 208 "cacheDir", 209 "cfgFile", 210 "confirm", 211 "contentDir", 212 "debug", 213 "destination", 214 "disableKinds", 215 "dryRun", 216 "force", 217 "gc", 218 "i18n-warnings", 219 "invalidateCDN", 220 "layoutDir", 221 "logFile", 222 "maxDeletes", 223 "quiet", 224 "renderToMemory", 225 "source", 226 "target", 227 "theme", 228 "themesDir", 229 "verbose", 230 "verboseLog", 231 "duplicateTargetPaths", 232 } 233 234 for _, key := range persFlagKeys { 235 setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false) 236 } 237 for _, key := range flagKeys { 238 setValueFromFlag(cmd.Flags(), key, cfg, "", false) 239 } 240 241 setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true) 242 243 // Set some "config aliases" 244 setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false) 245 setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false) 246 setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false) 247 } 248 249 func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) { 250 key = strings.TrimSpace(key) 251 if (force && flags.Lookup(key) != nil) || flags.Changed(key) { 252 f := flags.Lookup(key) 253 configKey := key 254 if targetKey != "" { 255 configKey = targetKey 256 } 257 // Gotta love this API. 258 switch f.Value.Type() { 259 case "bool": 260 bv, _ := flags.GetBool(key) 261 cfg.Set(configKey, bv) 262 case "string": 263 cfg.Set(configKey, f.Value.String()) 264 case "stringSlice": 265 bv, _ := flags.GetStringSlice(key) 266 cfg.Set(configKey, bv) 267 case "int": 268 iv, _ := flags.GetInt(key) 269 cfg.Set(configKey, iv) 270 default: 271 panic(fmt.Sprintf("update switch with %s", f.Value.Type())) 272 } 273 274 } 275 } 276 277 func isTerminal() bool { 278 return terminal.IsTerminal(os.Stdout) 279 } 280 281 func (c *commandeer) fullBuild() error { 282 var ( 283 g errgroup.Group 284 langCount map[string]uint64 285 ) 286 287 if !c.h.quiet { 288 fmt.Println("Start building sites … ") 289 fmt.Println(hugo.BuildVersionString()) 290 if isTerminal() { 291 defer func() { 292 fmt.Print(showCursor + clearLine) 293 }() 294 } 295 } 296 297 copyStaticFunc := func() error { 298 cnt, err := c.copyStatic() 299 if err != nil { 300 return errors.Wrap(err, "Error copying static files") 301 } 302 langCount = cnt 303 return nil 304 } 305 buildSitesFunc := func() error { 306 if err := c.buildSites(); err != nil { 307 return errors.Wrap(err, "Error building site") 308 } 309 return nil 310 } 311 // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled. 312 // This flag deletes all static resources in /public folder that are missing in /static, 313 // and it does so at the end of copyStatic() call. 314 if c.Cfg.GetBool("cleanDestinationDir") { 315 if err := copyStaticFunc(); err != nil { 316 return err 317 } 318 if err := buildSitesFunc(); err != nil { 319 return err 320 } 321 } else { 322 g.Go(copyStaticFunc) 323 g.Go(buildSitesFunc) 324 if err := g.Wait(); err != nil { 325 return err 326 } 327 } 328 329 for _, s := range c.hugo().Sites { 330 s.ProcessingStats.Static = langCount[s.Language().Lang] 331 } 332 333 if c.h.gc { 334 count, err := c.hugo().GC() 335 if err != nil { 336 return err 337 } 338 for _, s := range c.hugo().Sites { 339 // We have no way of knowing what site the garbage belonged to. 340 s.ProcessingStats.Cleaned = uint64(count) 341 } 342 } 343 344 return nil 345 } 346 347 func (c *commandeer) initCPUProfile() (func(), error) { 348 if c.h.cpuprofile == "" { 349 return nil, nil 350 } 351 352 f, err := os.Create(c.h.cpuprofile) 353 if err != nil { 354 return nil, errors.Wrap(err, "failed to create CPU profile") 355 } 356 if err := pprof.StartCPUProfile(f); err != nil { 357 return nil, errors.Wrap(err, "failed to start CPU profile") 358 } 359 return func() { 360 pprof.StopCPUProfile() 361 f.Close() 362 }, nil 363 } 364 365 func (c *commandeer) initMemProfile() { 366 if c.h.memprofile == "" { 367 return 368 } 369 370 f, err := os.Create(c.h.memprofile) 371 if err != nil { 372 c.logger.Errorf("could not create memory profile: ", err) 373 } 374 defer f.Close() 375 runtime.GC() // get up-to-date statistics 376 if err := pprof.WriteHeapProfile(f); err != nil { 377 c.logger.Errorf("could not write memory profile: ", err) 378 } 379 } 380 381 func (c *commandeer) initTraceProfile() (func(), error) { 382 if c.h.traceprofile == "" { 383 return nil, nil 384 } 385 386 f, err := os.Create(c.h.traceprofile) 387 if err != nil { 388 return nil, errors.Wrap(err, "failed to create trace file") 389 } 390 391 if err := trace.Start(f); err != nil { 392 return nil, errors.Wrap(err, "failed to start trace") 393 } 394 395 return func() { 396 trace.Stop() 397 f.Close() 398 }, nil 399 } 400 401 func (c *commandeer) initMutexProfile() (func(), error) { 402 if c.h.mutexprofile == "" { 403 return nil, nil 404 } 405 406 f, err := os.Create(c.h.mutexprofile) 407 if err != nil { 408 return nil, err 409 } 410 411 runtime.SetMutexProfileFraction(1) 412 413 return func() { 414 pprof.Lookup("mutex").WriteTo(f, 0) 415 f.Close() 416 }, nil 417 } 418 419 func (c *commandeer) initMemTicker() func() { 420 memticker := time.NewTicker(5 * time.Second) 421 quit := make(chan struct{}) 422 printMem := func() { 423 var m runtime.MemStats 424 runtime.ReadMemStats(&m) 425 fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC) 426 } 427 428 go func() { 429 for { 430 select { 431 case <-memticker.C: 432 printMem() 433 case <-quit: 434 memticker.Stop() 435 printMem() 436 return 437 } 438 } 439 }() 440 441 return func() { 442 close(quit) 443 } 444 } 445 446 func (c *commandeer) initProfiling() (func(), error) { 447 stopCPUProf, err := c.initCPUProfile() 448 if err != nil { 449 return nil, err 450 } 451 452 stopMutexProf, err := c.initMutexProfile() 453 if err != nil { 454 return nil, err 455 } 456 457 stopTraceProf, err := c.initTraceProfile() 458 if err != nil { 459 return nil, err 460 } 461 462 var stopMemTicker func() 463 if c.h.printm { 464 stopMemTicker = c.initMemTicker() 465 } 466 467 return func() { 468 c.initMemProfile() 469 470 if stopCPUProf != nil { 471 stopCPUProf() 472 } 473 if stopMutexProf != nil { 474 stopMutexProf() 475 } 476 477 if stopTraceProf != nil { 478 stopTraceProf() 479 } 480 481 if stopMemTicker != nil { 482 stopMemTicker() 483 } 484 }, nil 485 } 486 487 func (c *commandeer) build() error { 488 stopProfiling, err := c.initProfiling() 489 if err != nil { 490 return err 491 } 492 493 defer func() { 494 if stopProfiling != nil { 495 stopProfiling() 496 } 497 }() 498 499 if err := c.fullBuild(); err != nil { 500 return err 501 } 502 503 // TODO(bep) Feedback? 504 if !c.h.quiet { 505 fmt.Println() 506 c.hugo().PrintProcessingStats(os.Stdout) 507 fmt.Println() 508 509 if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok { 510 dupes := createCounter.ReportDuplicates() 511 if dupes != "" { 512 c.logger.Warnln("Duplicate target paths:", dupes) 513 } 514 } 515 } 516 517 if c.h.buildWatch { 518 watchDirs, err := c.getDirList() 519 if err != nil { 520 return err 521 } 522 523 baseWatchDir := c.Cfg.GetString("workingDir") 524 rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs) 525 526 c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) 527 c.logger.Println("Press Ctrl+C to stop") 528 watcher, err := c.newWatcher(c.h.poll, watchDirs...) 529 checkErr(c.Logger, err) 530 defer watcher.Close() 531 532 sigs := make(chan os.Signal, 1) 533 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 534 535 <-sigs 536 } 537 538 return nil 539 } 540 541 func (c *commandeer) serverBuild() error { 542 543 stopProfiling, err := c.initProfiling() 544 if err != nil { 545 return err 546 } 547 548 defer func() { 549 if stopProfiling != nil { 550 stopProfiling() 551 } 552 }() 553 554 if err := c.fullBuild(); err != nil { 555 return err 556 } 557 558 // TODO(bep) Feedback? 559 if !c.h.quiet { 560 fmt.Println() 561 c.hugo().PrintProcessingStats(os.Stdout) 562 fmt.Println() 563 } 564 565 return nil 566 } 567 568 func (c *commandeer) copyStatic() (map[string]uint64, error) { 569 m, err := c.doWithPublishDirs(c.copyStaticTo) 570 if err == nil || os.IsNotExist(err) { 571 return m, nil 572 } 573 return m, err 574 } 575 576 func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) { 577 langCount := make(map[string]uint64) 578 579 staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static 580 581 if len(staticFilesystems) == 0 { 582 c.logger.Infoln("No static directories found to sync") 583 return langCount, nil 584 } 585 586 for lang, fs := range staticFilesystems { 587 cnt, err := f(fs) 588 if err != nil { 589 return langCount, err 590 } 591 592 if lang == "" { 593 // Not multihost 594 for _, l := range c.languages { 595 langCount[l.Lang] = cnt 596 } 597 } else { 598 langCount[lang] = cnt 599 } 600 } 601 602 return langCount, nil 603 } 604 605 type countingStatFs struct { 606 afero.Fs 607 statCounter uint64 608 } 609 610 func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) { 611 f, err := fs.Fs.Stat(name) 612 if err == nil { 613 if !f.IsDir() { 614 atomic.AddUint64(&fs.statCounter, 1) 615 } 616 } 617 return f, err 618 } 619 620 func chmodFilter(dst, src os.FileInfo) bool { 621 // Hugo publishes data from multiple sources, potentially 622 // with overlapping directory structures. We cannot sync permissions 623 // for directories as that would mean that we might end up with write-protected 624 // directories inside /public. 625 // One example of this would be syncing from the Go Module cache, 626 // which have 0555 directories. 627 return src.IsDir() 628 } 629 630 func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { 631 publishDir := c.hugo().PathSpec.PublishDir 632 // If root, remove the second '/' 633 if publishDir == "//" { 634 publishDir = helpers.FilePathSeparator 635 } 636 637 if sourceFs.PublishFolder != "" { 638 publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) 639 } 640 641 fs := &countingStatFs{Fs: sourceFs.Fs} 642 643 syncer := fsync.NewSyncer() 644 syncer.NoTimes = c.Cfg.GetBool("noTimes") 645 syncer.NoChmod = c.Cfg.GetBool("noChmod") 646 syncer.ChmodFilter = chmodFilter 647 syncer.SrcFs = fs 648 syncer.DestFs = c.Fs.Destination 649 // Now that we are using a unionFs for the static directories 650 // We can effectively clean the publishDir on initial sync 651 syncer.Delete = c.Cfg.GetBool("cleanDestinationDir") 652 653 if syncer.Delete { 654 c.logger.Infoln("removing all files from destination that don't exist in static dirs") 655 656 syncer.DeleteFilter = func(f os.FileInfo) bool { 657 return f.IsDir() && strings.HasPrefix(f.Name(), ".") 658 } 659 } 660 c.logger.Infoln("syncing static files to", publishDir) 661 662 // because we are using a baseFs (to get the union right). 663 // set sync src to root 664 err := syncer.Sync(publishDir, helpers.FilePathSeparator) 665 if err != nil { 666 return 0, err 667 } 668 669 // Sync runs Stat 3 times for every source file (which sounds much) 670 numFiles := fs.statCounter / 3 671 672 return numFiles, err 673 } 674 675 func (c *commandeer) firstPathSpec() *helpers.PathSpec { 676 return c.hugo().Sites[0].PathSpec 677 } 678 679 func (c *commandeer) timeTrack(start time.Time, name string) { 680 elapsed := time.Since(start) 681 c.logger.Printf("%s in %v ms", name, int(1000*elapsed.Seconds())) 682 } 683 684 // getDirList provides NewWatcher() with a list of directories to watch for changes. 685 func (c *commandeer) getDirList() ([]string, error) { 686 var filenames []string 687 688 walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { 689 if err != nil { 690 c.logger.Errorln("walker: ", err) 691 return nil 692 } 693 694 if fi.IsDir() { 695 if fi.Name() == ".git" || 696 fi.Name() == "node_modules" || fi.Name() == "bower_components" { 697 return filepath.SkipDir 698 } 699 700 filenames = append(filenames, fi.Meta().Filename) 701 } 702 703 return nil 704 } 705 706 watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs() 707 for _, fi := range watchFiles { 708 if !fi.IsDir() { 709 filenames = append(filenames, fi.Meta().Filename) 710 continue 711 } 712 713 w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: fi, WalkFn: walkFn}) 714 if err := w.Walk(); err != nil { 715 c.logger.Errorln("walker: ", err) 716 } 717 } 718 719 filenames = helpers.UniqueStringsSorted(filenames) 720 721 return filenames, nil 722 } 723 724 func (c *commandeer) buildSites() (err error) { 725 return c.hugo().Build(hugolib.BuildCfg{}) 726 } 727 728 func (c *commandeer) handleBuildErr(err error, msg string) { 729 c.buildErr = err 730 731 c.logger.Errorln(msg + ":\n") 732 c.logger.Errorln(helpers.FirstUpper(err.Error())) 733 if !c.h.quiet && c.h.verbose { 734 herrors.PrintStackTraceFromErr(err) 735 } 736 } 737 738 func (c *commandeer) rebuildSites(events []fsnotify.Event) error { 739 740 c.buildErr = nil 741 visited := c.visitedURLs.PeekAllSet() 742 if c.fastRenderMode { 743 // Make sure we always render the home pages 744 for _, l := range c.languages { 745 langPath := c.hugo().PathSpec.GetLangSubDir(l.Lang) 746 if langPath != "" { 747 langPath = langPath + "/" 748 } 749 home := c.hugo().PathSpec.PrependBasePath("/"+langPath, false) 750 visited[home] = true 751 } 752 } 753 return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...) 754 } 755 756 func (c *commandeer) partialReRender(urls ...string) error { 757 defer func() { 758 c.wasError = false 759 }() 760 c.buildErr = nil 761 visited := make(map[string]bool) 762 for _, url := range urls { 763 visited[url] = true 764 } 765 return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError}) 766 } 767 768 func (c *commandeer) fullRebuild(changeType string) { 769 if changeType == configChangeGoMod { 770 // go.mod may be changed during the build itself, and 771 // we really want to prevent superfluous builds. 772 if !c.fullRebuildSem.TryAcquire(1) { 773 return 774 } 775 c.fullRebuildSem.Release(1) 776 } 777 778 c.fullRebuildSem.Acquire(context.Background(), 1) 779 780 go func() { 781 defer c.fullRebuildSem.Release(1) 782 783 c.printChangeDetected(changeType) 784 785 defer func() { 786 // Allow any file system events to arrive back. 787 // This will block any rebuild on config changes for the 788 // duration of the sleep. 789 time.Sleep(2 * time.Second) 790 }() 791 792 defer c.timeTrack(time.Now(), "Rebuilt") 793 794 c.commandeerHugoState = newCommandeerHugoState() 795 err := c.loadConfig() 796 if err != nil { 797 // Set the processing on pause until the state is recovered. 798 c.paused = true 799 c.handleBuildErr(err, "Failed to reload config") 800 801 } else { 802 c.paused = false 803 } 804 805 if !c.paused { 806 _, err := c.copyStatic() 807 if err != nil { 808 c.logger.Errorln(err) 809 return 810 } 811 812 err = c.buildSites() 813 if err != nil { 814 c.logger.Errorln(err) 815 } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { 816 livereload.ForceRefresh() 817 } 818 } 819 }() 820 } 821 822 // newWatcher creates a new watcher to watch filesystem events. 823 func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) { 824 if runtime.GOOS == "darwin" { 825 tweakLimit() 826 } 827 828 staticSyncer, err := newStaticSyncer(c) 829 if err != nil { 830 return nil, err 831 } 832 833 var pollInterval time.Duration 834 poll := pollIntervalStr != "" 835 if poll { 836 pollInterval, err = types.ToDurationE(pollIntervalStr) 837 if err != nil { 838 return nil, fmt.Errorf("invalid value for flag poll: %s", err) 839 } 840 c.logger.Printf("Use watcher with poll interval %v", pollInterval) 841 } 842 843 watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll) 844 if err != nil { 845 return nil, err 846 } 847 848 for _, d := range dirList { 849 if d != "" { 850 _ = watcher.Add(d) 851 } 852 } 853 854 // Identifies changes to config (config.toml) files. 855 configSet := make(map[string]bool) 856 857 c.logger.Println("Watching for config changes in", strings.Join(c.configFiles, ", ")) 858 for _, configFile := range c.configFiles { 859 watcher.Add(configFile) 860 configSet[configFile] = true 861 } 862 863 go func() { 864 for { 865 select { 866 case evs := <-watcher.Events: 867 c.handleEvents(watcher, staticSyncer, evs, configSet) 868 if c.showErrorInBrowser && c.errCount() > 0 { 869 // Need to reload browser to show the error 870 livereload.ForceRefresh() 871 } 872 case err := <-watcher.Errors(): 873 if err != nil { 874 c.logger.Errorln("Error while watching:", err) 875 } 876 } 877 } 878 }() 879 880 return watcher, nil 881 } 882 883 func (c *commandeer) printChangeDetected(typ string) { 884 msg := "\nChange" 885 if typ != "" { 886 msg += " of " + typ 887 } 888 msg += " detected, rebuilding site." 889 890 c.logger.Println(msg) 891 const layout = "2006-01-02 15:04:05.000 -0700" 892 c.logger.Println(time.Now().Format(layout)) 893 } 894 895 const ( 896 configChangeConfig = "config file" 897 configChangeGoMod = "go.mod file" 898 ) 899 900 func (c *commandeer) handleEvents(watcher *watcher.Batcher, 901 staticSyncer *staticSyncer, 902 evs []fsnotify.Event, 903 configSet map[string]bool) { 904 defer func() { 905 c.wasError = false 906 }() 907 908 var isHandled bool 909 910 for _, ev := range evs { 911 isConfig := configSet[ev.Name] 912 configChangeType := configChangeConfig 913 if isConfig { 914 if strings.Contains(ev.Name, "go.mod") { 915 configChangeType = configChangeGoMod 916 } 917 } 918 if !isConfig { 919 // It may be one of the /config folders 920 dirname := filepath.Dir(ev.Name) 921 if dirname != "." && configSet[dirname] { 922 isConfig = true 923 } 924 } 925 926 if isConfig { 927 isHandled = true 928 929 if ev.Op&fsnotify.Chmod == fsnotify.Chmod { 930 continue 931 } 932 933 if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename { 934 for _, configFile := range c.configFiles { 935 counter := 0 936 for watcher.Add(configFile) != nil { 937 counter++ 938 if counter >= 100 { 939 break 940 } 941 time.Sleep(100 * time.Millisecond) 942 } 943 } 944 } 945 946 // Config file(s) changed. Need full rebuild. 947 c.fullRebuild(configChangeType) 948 949 return 950 } 951 } 952 953 if isHandled { 954 return 955 } 956 957 if c.paused { 958 // Wait for the server to get into a consistent state before 959 // we continue with processing. 960 return 961 } 962 963 if len(evs) > 50 { 964 // This is probably a mass edit of the content dir. 965 // Schedule a full rebuild for when it slows down. 966 c.debounce(func() { 967 c.fullRebuild("") 968 }) 969 return 970 } 971 972 c.logger.Infoln("Received System Events:", evs) 973 974 staticEvents := []fsnotify.Event{} 975 dynamicEvents := []fsnotify.Event{} 976 977 filtered := []fsnotify.Event{} 978 for _, ev := range evs { 979 if c.hugo().ShouldSkipFileChangeEvent(ev) { 980 continue 981 } 982 // Check the most specific first, i.e. files. 983 contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name) 984 if len(contentMapped) > 0 { 985 for _, mapped := range contentMapped { 986 filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op}) 987 } 988 continue 989 } 990 991 // Check for any symbolic directory mapping. 992 993 dir, name := filepath.Split(ev.Name) 994 995 contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir) 996 997 if len(contentMapped) == 0 { 998 filtered = append(filtered, ev) 999 continue 1000 } 1001 1002 for _, mapped := range contentMapped { 1003 mappedFilename := filepath.Join(mapped, name) 1004 filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op}) 1005 } 1006 } 1007 1008 evs = filtered 1009 1010 for _, ev := range evs { 1011 ext := filepath.Ext(ev.Name) 1012 baseName := filepath.Base(ev.Name) 1013 istemp := strings.HasSuffix(ext, "~") || 1014 (ext == ".swp") || // vim 1015 (ext == ".swx") || // vim 1016 (ext == ".tmp") || // generic temp file 1017 (ext == ".DS_Store") || // OSX Thumbnail 1018 baseName == "4913" || // vim 1019 strings.HasPrefix(ext, ".goutputstream") || // gnome 1020 strings.HasSuffix(ext, "jb_old___") || // intelliJ 1021 strings.HasSuffix(ext, "jb_tmp___") || // intelliJ 1022 strings.HasSuffix(ext, "jb_bak___") || // intelliJ 1023 strings.HasPrefix(ext, ".sb-") || // byword 1024 strings.HasPrefix(baseName, ".#") || // emacs 1025 strings.HasPrefix(baseName, "#") // emacs 1026 if istemp { 1027 continue 1028 } 1029 if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) { 1030 continue 1031 } 1032 // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these 1033 if ev.Name == "" { 1034 continue 1035 } 1036 1037 // Write and rename operations are often followed by CHMOD. 1038 // There may be valid use cases for rebuilding the site on CHMOD, 1039 // but that will require more complex logic than this simple conditional. 1040 // On OS X this seems to be related to Spotlight, see: 1041 // https://github.com/go-fsnotify/fsnotify/issues/15 1042 // A workaround is to put your site(s) on the Spotlight exception list, 1043 // but that may be a little mysterious for most end users. 1044 // So, for now, we skip reload on CHMOD. 1045 // We do have to check for WRITE though. On slower laptops a Chmod 1046 // could be aggregated with other important events, and we still want 1047 // to rebuild on those 1048 if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod { 1049 continue 1050 } 1051 1052 walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error { 1053 if f.IsDir() { 1054 c.logger.Println("adding created directory to watchlist", path) 1055 if err := watcher.Add(path); err != nil { 1056 return err 1057 } 1058 } else if !staticSyncer.isStatic(path) { 1059 // Hugo's rebuilding logic is entirely file based. When you drop a new folder into 1060 // /content on OSX, the above logic will handle future watching of those files, 1061 // but the initial CREATE is lost. 1062 dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create}) 1063 } 1064 return nil 1065 } 1066 1067 // recursively add new directories to watch list 1068 // When mkdir -p is used, only the top directory triggers an event (at least on OSX) 1069 if ev.Op&fsnotify.Create == fsnotify.Create { 1070 if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { 1071 _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder) 1072 } 1073 } 1074 1075 if staticSyncer.isStatic(ev.Name) { 1076 staticEvents = append(staticEvents, ev) 1077 } else { 1078 dynamicEvents = append(dynamicEvents, ev) 1079 } 1080 } 1081 1082 if len(staticEvents) > 0 { 1083 c.printChangeDetected("Static files") 1084 1085 if c.Cfg.GetBool("forceSyncStatic") { 1086 c.logger.Printf("Syncing all static files\n") 1087 _, err := c.copyStatic() 1088 if err != nil { 1089 c.logger.Errorln("Error copying static files to publish dir:", err) 1090 return 1091 } 1092 } else { 1093 if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { 1094 c.logger.Errorln("Error syncing static files to publish dir:", err) 1095 return 1096 } 1097 } 1098 1099 if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { 1100 // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized 1101 1102 // force refresh when more than one file 1103 if !c.wasError && len(staticEvents) == 1 { 1104 ev := staticEvents[0] 1105 path := c.hugo().BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name) 1106 path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false) 1107 1108 livereload.RefreshPath(path) 1109 } else { 1110 livereload.ForceRefresh() 1111 } 1112 } 1113 } 1114 1115 if len(dynamicEvents) > 0 { 1116 partitionedEvents := partitionDynamicEvents( 1117 c.firstPathSpec().BaseFs.SourceFilesystems, 1118 dynamicEvents) 1119 1120 doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") 1121 onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents) 1122 1123 c.printChangeDetected("") 1124 c.changeDetector.PrepareNew() 1125 1126 func() { 1127 defer c.timeTrack(time.Now(), "Total") 1128 if err := c.rebuildSites(dynamicEvents); err != nil { 1129 c.handleBuildErr(err, "Rebuild failed") 1130 } 1131 }() 1132 1133 if doLiveReload { 1134 if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 { 1135 if c.wasError { 1136 livereload.ForceRefresh() 1137 return 1138 } 1139 changed := c.changeDetector.changed() 1140 if c.changeDetector != nil && len(changed) == 0 { 1141 // Nothing has changed. 1142 return 1143 } else if len(changed) == 1 { 1144 pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false) 1145 livereload.RefreshPath(pathToRefresh) 1146 } else { 1147 livereload.ForceRefresh() 1148 } 1149 } 1150 1151 if len(partitionedEvents.ContentEvents) > 0 { 1152 1153 navigate := c.Cfg.GetBool("navigateToChanged") 1154 // We have fetched the same page above, but it may have 1155 // changed. 1156 var p page.Page 1157 1158 if navigate { 1159 if onePageName != "" { 1160 p = c.hugo().GetContentPage(onePageName) 1161 } 1162 } 1163 1164 if p != nil { 1165 livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort()) 1166 } else { 1167 livereload.ForceRefresh() 1168 } 1169 } 1170 } 1171 } 1172 } 1173 1174 // dynamicEvents contains events that is considered dynamic, as in "not static". 1175 // Both of these categories will trigger a new build, but the asset events 1176 // does not fit into the "navigate to changed" logic. 1177 type dynamicEvents struct { 1178 ContentEvents []fsnotify.Event 1179 AssetEvents []fsnotify.Event 1180 } 1181 1182 func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) { 1183 for _, e := range events { 1184 if sourceFs.IsAsset(e.Name) { 1185 de.AssetEvents = append(de.AssetEvents, e) 1186 } else { 1187 de.ContentEvents = append(de.ContentEvents, e) 1188 } 1189 } 1190 return 1191 } 1192 1193 func pickOneWriteOrCreatePath(events []fsnotify.Event) string { 1194 name := "" 1195 1196 // Some editors (for example notepad.exe on Windows) triggers a change 1197 // both for directory and file. So we pick the longest path, which should 1198 // be the file itself. 1199 for _, ev := range events { 1200 if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) { 1201 name = ev.Name 1202 } 1203 } 1204 1205 return name 1206 } 1207 1208 func formatByteCount(b uint64) string { 1209 const unit = 1000 1210 if b < unit { 1211 return fmt.Sprintf("%d B", b) 1212 } 1213 div, exp := int64(unit), 0 1214 for n := b / unit; n >= unit; n /= unit { 1215 div *= unit 1216 exp++ 1217 } 1218 return fmt.Sprintf("%.1f %cB", 1219 float64(b)/float64(div), "kMGTPE"[exp]) 1220 }