github.com/gohugoio/hugo@v0.88.1/commands/server.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 "fmt" 19 "io" 20 "net" 21 "net/http" 22 "net/url" 23 "os" 24 "os/signal" 25 "path/filepath" 26 "regexp" 27 "runtime" 28 "strconv" 29 "strings" 30 "sync" 31 "syscall" 32 "time" 33 34 "github.com/gohugoio/hugo/common/paths" 35 36 "github.com/pkg/errors" 37 38 "github.com/gohugoio/hugo/livereload" 39 40 "github.com/gohugoio/hugo/config" 41 "github.com/gohugoio/hugo/helpers" 42 "github.com/spf13/afero" 43 "github.com/spf13/cobra" 44 jww "github.com/spf13/jwalterweatherman" 45 ) 46 47 type serverCmd struct { 48 // Can be used to stop the server. Useful in tests 49 stop <-chan bool 50 51 disableLiveReload bool 52 navigateToChanged bool 53 renderToDisk bool 54 serverAppend bool 55 serverInterface string 56 serverPort int 57 liveReloadPort int 58 serverWatch bool 59 noHTTPCache bool 60 61 disableFastRender bool 62 disableBrowserError bool 63 64 *baseBuilderCmd 65 } 66 67 func (b *commandsBuilder) newServerCmd() *serverCmd { 68 return b.newServerCmdSignaled(nil) 69 } 70 71 func (b *commandsBuilder) newServerCmdSignaled(stop <-chan bool) *serverCmd { 72 cc := &serverCmd{stop: stop} 73 74 cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{ 75 Use: "server", 76 Aliases: []string{"serve"}, 77 Short: "A high performance webserver", 78 Long: `Hugo provides its own webserver which builds and serves the site. 79 While hugo server is high performance, it is a webserver with limited options. 80 Many run it in production, but the standard behavior is for people to use it 81 in development and use a more full featured server such as Nginx or Caddy. 82 83 'hugo server' will avoid writing the rendered and served content to disk, 84 preferring to store it in memory. 85 86 By default hugo will also watch your files for any changes you make and 87 automatically rebuild the site. It will then live reload any open browser pages 88 and push the latest content to them. As most Hugo sites are built in a fraction 89 of a second, you will be able to save and see your changes nearly instantly.`, 90 RunE: cc.server, 91 }) 92 93 cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen") 94 cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)") 95 cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind") 96 cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed") 97 cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching") 98 cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL") 99 cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild") 100 cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload") 101 cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)") 102 cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes") 103 cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser") 104 105 cc.cmd.Flags().String("memstats", "", "log memory usage to this file") 106 cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") 107 108 return cc 109 } 110 111 type filesOnlyFs struct { 112 fs http.FileSystem 113 } 114 115 type noDirFile struct { 116 http.File 117 } 118 119 func (fs filesOnlyFs) Open(name string) (http.File, error) { 120 f, err := fs.fs.Open(name) 121 if err != nil { 122 return nil, err 123 } 124 return noDirFile{f}, nil 125 } 126 127 func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) { 128 return nil, nil 129 } 130 131 var serverPorts []int 132 133 func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { 134 // If a Destination is provided via flag write to disk 135 destination, _ := cmd.Flags().GetString("destination") 136 if destination != "" { 137 sc.renderToDisk = true 138 } 139 140 var serverCfgInit sync.Once 141 142 cfgInit := func(c *commandeer) error { 143 c.Set("renderToMemory", !sc.renderToDisk) 144 if cmd.Flags().Changed("navigateToChanged") { 145 c.Set("navigateToChanged", sc.navigateToChanged) 146 } 147 if cmd.Flags().Changed("disableLiveReload") { 148 c.Set("disableLiveReload", sc.disableLiveReload) 149 } 150 if cmd.Flags().Changed("disableFastRender") { 151 c.Set("disableFastRender", sc.disableFastRender) 152 } 153 if cmd.Flags().Changed("disableBrowserError") { 154 c.Set("disableBrowserError", sc.disableBrowserError) 155 } 156 if sc.serverWatch { 157 c.Set("watch", true) 158 } 159 160 // TODO(bep) yes, we should fix. 161 if !c.languagesConfigured { 162 return nil 163 } 164 165 var err error 166 167 // We can only do this once. 168 serverCfgInit.Do(func() { 169 serverPorts = make([]int, 1) 170 171 if c.languages.IsMultihost() { 172 if !sc.serverAppend { 173 err = newSystemError("--appendPort=false not supported when in multihost mode") 174 } 175 serverPorts = make([]int, len(c.languages)) 176 } 177 178 currentServerPort := sc.serverPort 179 180 for i := 0; i < len(serverPorts); i++ { 181 l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort))) 182 if err == nil { 183 l.Close() 184 serverPorts[i] = currentServerPort 185 } else { 186 if i == 0 && sc.cmd.Flags().Changed("port") { 187 // port set explicitly by user -- he/she probably meant it! 188 err = newSystemErrorF("Server startup failed: %s", err) 189 } 190 c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port") 191 sp, err := helpers.FindAvailablePort() 192 if err != nil { 193 err = newSystemError("Unable to find alternative port to use:", err) 194 } 195 serverPorts[i] = sp.Port 196 } 197 198 currentServerPort = serverPorts[i] + 1 199 } 200 }) 201 202 c.serverPorts = serverPorts 203 204 c.Set("port", sc.serverPort) 205 if sc.liveReloadPort != -1 { 206 c.Set("liveReloadPort", sc.liveReloadPort) 207 } else { 208 c.Set("liveReloadPort", serverPorts[0]) 209 } 210 211 isMultiHost := c.languages.IsMultihost() 212 for i, language := range c.languages { 213 var serverPort int 214 if isMultiHost { 215 serverPort = serverPorts[i] 216 } else { 217 serverPort = serverPorts[0] 218 } 219 220 baseURL, err := sc.fixURL(language, sc.baseURL, serverPort) 221 if err != nil { 222 return nil 223 } 224 if isMultiHost { 225 language.Set("baseURL", baseURL) 226 } 227 if i == 0 { 228 c.Set("baseURL", baseURL) 229 } 230 } 231 232 return err 233 } 234 235 if err := memStats(); err != nil { 236 jww.WARN.Println("memstats error:", err) 237 } 238 239 // silence errors in cobra so we can handle them here 240 cmd.SilenceErrors = true 241 242 c, err := initializeConfig(true, true, true, &sc.hugoBuilderCommon, sc, cfgInit) 243 if err != nil { 244 cmd.PrintErrln("Error:", err.Error()) 245 return err 246 } 247 248 err = func() error { 249 defer c.timeTrack(time.Now(), "Built") 250 err := c.serverBuild() 251 if err != nil { 252 cmd.PrintErrln("Error:", err.Error()) 253 } 254 return err 255 }() 256 if err != nil { 257 return err 258 } 259 260 for _, s := range c.hugo().Sites { 261 s.RegisterMediaTypes() 262 } 263 264 // Watch runs its own server as part of the routine 265 if sc.serverWatch { 266 267 watchDirs, err := c.getDirList() 268 if err != nil { 269 return err 270 } 271 272 watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs) 273 274 for _, group := range watchGroups { 275 jww.FEEDBACK.Printf("Watching for changes in %s\n", group) 276 } 277 watcher, err := c.newWatcher(sc.poll, watchDirs...) 278 if err != nil { 279 return err 280 } 281 282 defer watcher.Close() 283 284 } 285 286 return c.serve(sc) 287 } 288 289 func getRootWatchDirsStr(baseDir string, watchDirs []string) string { 290 relWatchDirs := make([]string, len(watchDirs)) 291 for i, dir := range watchDirs { 292 relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir) 293 } 294 295 return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",") 296 } 297 298 type fileServer struct { 299 baseURLs []string 300 roots []string 301 errorTemplate func(err interface{}) (io.Reader, error) 302 c *commandeer 303 s *serverCmd 304 } 305 306 func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request { 307 r2 := new(http.Request) 308 *r2 = *r 309 r2.URL = new(url.URL) 310 *r2.URL = *r.URL 311 r2.URL.Path = toPath 312 r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI()) 313 314 return r2 315 } 316 317 func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) { 318 baseURL := f.baseURLs[i] 319 root := f.roots[i] 320 port := f.c.serverPorts[i] 321 322 publishDir := f.c.Cfg.GetString("publishDir") 323 324 if root != "" { 325 publishDir = filepath.Join(publishDir, root) 326 } 327 328 absPublishDir := f.c.hugo().PathSpec.AbsPathify(publishDir) 329 330 jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment) 331 332 if i == 0 { 333 if f.s.renderToDisk { 334 jww.FEEDBACK.Println("Serving pages from " + absPublishDir) 335 } else { 336 jww.FEEDBACK.Println("Serving pages from memory") 337 } 338 } 339 340 httpFs := afero.NewHttpFs(f.c.destinationFs) 341 fs := filesOnlyFs{httpFs.Dir(absPublishDir)} 342 343 if i == 0 && f.c.fastRenderMode { 344 jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") 345 } 346 347 // We're only interested in the path 348 u, err := url.Parse(baseURL) 349 if err != nil { 350 return nil, "", "", errors.Wrap(err, "Invalid baseURL") 351 } 352 353 decorate := func(h http.Handler) http.Handler { 354 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 355 if f.c.showErrorInBrowser { 356 // First check the error state 357 err := f.c.getErrorWithContext() 358 if err != nil { 359 f.c.wasError = true 360 w.WriteHeader(500) 361 r, err := f.errorTemplate(err) 362 if err != nil { 363 f.c.logger.Errorln(err) 364 } 365 366 port = 1313 367 if !f.c.paused { 368 port = f.c.Cfg.GetInt("liveReloadPort") 369 } 370 lr := *u 371 lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port) 372 fmt.Fprint(w, injectLiveReloadScript(r, lr)) 373 374 return 375 } 376 } 377 378 if f.s.noHTTPCache { 379 w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") 380 w.Header().Set("Pragma", "no-cache") 381 } 382 383 // Ignore any query params for the operations below. 384 requestURI := strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery) 385 386 for _, header := range f.c.serverConfig.MatchHeaders(requestURI) { 387 w.Header().Set(header.Key, header.Value) 388 } 389 390 if redirect := f.c.serverConfig.MatchRedirect(requestURI); !redirect.IsZero() { 391 doRedirect := true 392 // This matches Netlify's behaviour and is needed for SPA behaviour. 393 // See https://docs.netlify.com/routing/redirects/rewrites-proxies/ 394 if !redirect.Force { 395 path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path)) 396 fi, err := f.c.hugo().BaseFs.PublishFs.Stat(path) 397 if err == nil { 398 if fi.IsDir() { 399 // There will be overlapping directories, so we 400 // need to check for a file. 401 _, err = f.c.hugo().BaseFs.PublishFs.Stat(filepath.Join(path, "index.html")) 402 doRedirect = err != nil 403 } else { 404 doRedirect = false 405 } 406 } 407 } 408 409 if doRedirect { 410 if redirect.Status == 200 { 411 if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil { 412 requestURI = redirect.To 413 r = r2 414 } 415 } else { 416 w.Header().Set("Content-Type", "") 417 http.Redirect(w, r, redirect.To, redirect.Status) 418 return 419 } 420 } 421 422 } 423 424 if f.c.fastRenderMode && f.c.buildErr == nil { 425 if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") { 426 if !f.c.visitedURLs.Contains(requestURI) { 427 // If not already on stack, re-render that single page. 428 if err := f.c.partialReRender(requestURI); err != nil { 429 f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", requestURI)) 430 if f.c.showErrorInBrowser { 431 http.Redirect(w, r, requestURI, http.StatusMovedPermanently) 432 return 433 } 434 } 435 } 436 437 f.c.visitedURLs.Add(requestURI) 438 439 } 440 } 441 442 h.ServeHTTP(w, r) 443 }) 444 } 445 446 fileserver := decorate(http.FileServer(fs)) 447 mu := http.NewServeMux() 448 if u.Path == "" || u.Path == "/" { 449 mu.Handle("/", fileserver) 450 } else { 451 mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver)) 452 } 453 454 endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port)) 455 456 return mu, u.String(), endpoint, nil 457 } 458 459 var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) 460 461 func removeErrorPrefixFromLog(content string) string { 462 return logErrorRe.ReplaceAllLiteralString(content, "") 463 } 464 465 func (c *commandeer) serve(s *serverCmd) error { 466 isMultiHost := c.hugo().IsMultihost() 467 468 var ( 469 baseURLs []string 470 roots []string 471 ) 472 473 if isMultiHost { 474 for _, s := range c.hugo().Sites { 475 baseURLs = append(baseURLs, s.BaseURL.String()) 476 roots = append(roots, s.Language().Lang) 477 } 478 } else { 479 s := c.hugo().Sites[0] 480 baseURLs = []string{s.BaseURL.String()} 481 roots = []string{""} 482 } 483 484 templ, err := c.hugo().TextTmpl().Parse("__default_server_error", buildErrorTemplate) 485 if err != nil { 486 return err 487 } 488 489 srv := &fileServer{ 490 baseURLs: baseURLs, 491 roots: roots, 492 c: c, 493 s: s, 494 errorTemplate: func(ctx interface{}) (io.Reader, error) { 495 b := &bytes.Buffer{} 496 err := c.hugo().Tmpl().Execute(templ, b, ctx) 497 return b, err 498 }, 499 } 500 501 doLiveReload := !c.Cfg.GetBool("disableLiveReload") 502 503 if doLiveReload { 504 livereload.Initialize() 505 } 506 507 sigs := make(chan os.Signal, 1) 508 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 509 510 for i := range baseURLs { 511 mu, serverURL, endpoint, err := srv.createEndpoint(i) 512 513 if doLiveReload { 514 u, err := url.Parse(helpers.SanitizeURL(baseURLs[i])) 515 if err != nil { 516 return err 517 } 518 519 mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS) 520 mu.HandleFunc(u.Path+"/livereload", livereload.Handler) 521 } 522 jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface) 523 go func() { 524 err = http.ListenAndServe(endpoint, mu) 525 if err != nil { 526 c.logger.Errorf("Error: %s\n", err.Error()) 527 os.Exit(1) 528 } 529 }() 530 } 531 532 jww.FEEDBACK.Println("Press Ctrl+C to stop") 533 534 if s.stop != nil { 535 select { 536 case <-sigs: 537 case <-s.stop: 538 } 539 } else { 540 <-sigs 541 } 542 543 c.hugo().Close() 544 545 return nil 546 } 547 548 // fixURL massages the baseURL into a form needed for serving 549 // all pages correctly. 550 func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, error) { 551 useLocalhost := false 552 if s == "" { 553 s = cfg.GetString("baseURL") 554 useLocalhost = true 555 } 556 557 if !strings.HasSuffix(s, "/") { 558 s = s + "/" 559 } 560 561 // do an initial parse of the input string 562 u, err := url.Parse(s) 563 if err != nil { 564 return "", err 565 } 566 567 // if no Host is defined, then assume that no schema or double-slash were 568 // present in the url. Add a double-slash and make a best effort attempt. 569 if u.Host == "" && s != "/" { 570 s = "//" + s 571 572 u, err = url.Parse(s) 573 if err != nil { 574 return "", err 575 } 576 } 577 578 if useLocalhost { 579 if u.Scheme == "https" { 580 u.Scheme = "http" 581 } 582 u.Host = "localhost" 583 } 584 585 if sc.serverAppend { 586 if strings.Contains(u.Host, ":") { 587 u.Host, _, err = net.SplitHostPort(u.Host) 588 if err != nil { 589 return "", errors.Wrap(err, "Failed to split baseURL hostpost") 590 } 591 } 592 u.Host += fmt.Sprintf(":%d", port) 593 } 594 595 return u.String(), nil 596 } 597 598 func memStats() error { 599 b := newCommandsBuilder() 600 sc := b.newServerCmd().getCommand() 601 memstats := sc.Flags().Lookup("memstats").Value.String() 602 if memstats != "" { 603 interval, err := time.ParseDuration(sc.Flags().Lookup("meminterval").Value.String()) 604 if err != nil { 605 interval, _ = time.ParseDuration("100ms") 606 } 607 608 fileMemStats, err := os.Create(memstats) 609 if err != nil { 610 return err 611 } 612 613 fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n") 614 615 go func() { 616 var stats runtime.MemStats 617 618 start := time.Now().UnixNano() 619 620 for { 621 runtime.ReadMemStats(&stats) 622 if fileMemStats != nil { 623 fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n", 624 (time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased)) 625 time.Sleep(interval) 626 } else { 627 break 628 } 629 } 630 }() 631 } 632 return nil 633 }