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