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  }