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