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  }