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