hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

server.go (20542B)

    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 := 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 				doRedirect := true
  411 				// This matches Netlify's behaviour and is needed for SPA behaviour.
  412 				// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
  413 				if !redirect.Force {
  414 					path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
  415 					fi, err := f.c.hugo().BaseFs.PublishFs.Stat(path)
  416 					if err == nil {
  417 						if fi.IsDir() {
  418 							// There will be overlapping directories, so we
  419 							// need to check for a file.
  420 							_, err = f.c.hugo().BaseFs.PublishFs.Stat(filepath.Join(path, "index.html"))
  421 							doRedirect = err != nil
  422 						} else {
  423 							doRedirect = false
  424 						}
  425 					}
  426 				}
  427 
  428 				if doRedirect {
  429 					if redirect.Status == 200 {
  430 						if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
  431 							requestURI = redirect.To
  432 							r = r2
  433 						}
  434 					} else {
  435 						w.Header().Set("Content-Type", "")
  436 						http.Redirect(w, r, redirect.To, redirect.Status)
  437 						return
  438 					}
  439 				}
  440 
  441 			}
  442 
  443 			if f.c.fastRenderMode && f.c.buildErr == nil {
  444 				if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
  445 					if !f.c.visitedURLs.Contains(requestURI) {
  446 						// If not already on stack, re-render that single page.
  447 						if err := f.c.partialReRender(requestURI); err != nil {
  448 							f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", requestURI))
  449 							if f.c.showErrorInBrowser {
  450 								http.Redirect(w, r, requestURI, http.StatusMovedPermanently)
  451 								return
  452 							}
  453 						}
  454 					}
  455 
  456 					f.c.visitedURLs.Add(requestURI)
  457 
  458 				}
  459 			}
  460 
  461 			h.ServeHTTP(w, r)
  462 		})
  463 	}
  464 
  465 	fileserver := decorate(http.FileServer(fs))
  466 	mu := http.NewServeMux()
  467 	if u.Path == "" || u.Path == "/" {
  468 		mu.Handle("/", fileserver)
  469 	} else {
  470 		mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
  471 	}
  472 
  473 	endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
  474 
  475 	return mu, listener, u.String(), endpoint, nil
  476 }
  477 
  478 var (
  479 	logErrorRe                    = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
  480 	logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`)
  481 	logDuplicateTemplateParseRe   = regexp.MustCompile(`: template: .*?:\d+:\d*`)
  482 )
  483 
  484 func removeErrorPrefixFromLog(content string) string {
  485 	return logErrorRe.ReplaceAllLiteralString(content, "")
  486 }
  487 
  488 var logReplacer = strings.NewReplacer(
  489 	"can't", "can’t", // Chroma lexer does'nt do well with "can't"
  490 	"*hugolib.pageState", "page.Page", // Page is the public interface.
  491 	"Rebuild failed:", "",
  492 )
  493 
  494 func cleanErrorLog(content string) string {
  495 	content = strings.ReplaceAll(content, "\n", " ")
  496 	content = logReplacer.Replace(content)
  497 	content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
  498 	content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
  499 	seen := make(map[string]bool)
  500 	parts := strings.Split(content, ": ")
  501 	keep := make([]string, 0, len(parts))
  502 	for _, part := range parts {
  503 		if seen[part] {
  504 			continue
  505 		}
  506 		seen[part] = true
  507 		keep = append(keep, part)
  508 	}
  509 	return strings.Join(keep, ": ")
  510 }
  511 
  512 func (c *commandeer) serve(s *serverCmd) error {
  513 	isMultiHost := c.hugo().IsMultihost()
  514 
  515 	var (
  516 		baseURLs []string
  517 		roots    []string
  518 	)
  519 
  520 	if isMultiHost {
  521 		for _, s := range c.hugo().Sites {
  522 			baseURLs = append(baseURLs, s.BaseURL.String())
  523 			roots = append(roots, s.Language().Lang)
  524 		}
  525 	} else {
  526 		s := c.hugo().Sites[0]
  527 		baseURLs = []string{s.BaseURL.String()}
  528 		roots = []string{""}
  529 	}
  530 
  531 	// Cache it here. The HugoSites object may be unavaialble later on due to intermitent configuration errors.
  532 	// To allow the en user to change the error template while the server is running, we use
  533 	// the freshest template we can provide.
  534 	var (
  535 		errTempl     tpl.Template
  536 		templHandler tpl.TemplateHandler
  537 	)
  538 	getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
  539 		if h == nil {
  540 			return errTempl, templHandler
  541 		}
  542 		templHandler := h.Tmpl()
  543 		errTempl, found := templHandler.Lookup("_server/error.html")
  544 		if !found {
  545 			panic("template server/error.html not found")
  546 		}
  547 		return errTempl, templHandler
  548 	}
  549 	errTempl, templHandler = getErrorTemplateAndHandler(c.hugo())
  550 
  551 	srv := &fileServer{
  552 		baseURLs: baseURLs,
  553 		roots:    roots,
  554 		c:        c,
  555 		s:        s,
  556 		errorTemplate: func(ctx any) (io.Reader, error) {
  557 			// hugoTry does not block, getErrorTemplateAndHandler will fall back
  558 			// to cached values if nil.
  559 			templ, handler := getErrorTemplateAndHandler(c.hugoTry())
  560 			b := &bytes.Buffer{}
  561 			err := handler.Execute(templ, b, ctx)
  562 			return b, err
  563 		},
  564 	}
  565 
  566 	doLiveReload := !c.Cfg.GetBool("disableLiveReload")
  567 
  568 	if doLiveReload {
  569 		livereload.Initialize()
  570 	}
  571 
  572 	sigs := make(chan os.Signal, 1)
  573 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
  574 	var servers []*http.Server
  575 
  576 	wg1, ctx := errgroup.WithContext(context.Background())
  577 
  578 	for i := range baseURLs {
  579 		mu, listener, serverURL, endpoint, err := srv.createEndpoint(i)
  580 		srv := &http.Server{
  581 			Addr:    endpoint,
  582 			Handler: mu,
  583 		}
  584 		servers = append(servers, srv)
  585 
  586 		if doLiveReload {
  587 			u, err := url.Parse(helpers.SanitizeURL(baseURLs[i]))
  588 			if err != nil {
  589 				return err
  590 			}
  591 
  592 			mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS)
  593 			mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
  594 		}
  595 		jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
  596 		wg1.Go(func() error {
  597 			err = srv.Serve(listener)
  598 			if err != nil && err != http.ErrServerClosed {
  599 				return err
  600 			}
  601 			return nil
  602 		})
  603 	}
  604 
  605 	jww.FEEDBACK.Println("Press Ctrl+C to stop")
  606 
  607 	err := func() error {
  608 		if s.stop != nil {
  609 			for {
  610 				select {
  611 				case <-sigs:
  612 					return nil
  613 				case <-s.stop:
  614 					return nil
  615 				case <-ctx.Done():
  616 					return ctx.Err()
  617 				}
  618 			}
  619 		} else {
  620 			for {
  621 				select {
  622 				case <-sigs:
  623 					return nil
  624 				case <-ctx.Done():
  625 					return ctx.Err()
  626 				}
  627 			}
  628 		}
  629 	}()
  630 
  631 	if err != nil {
  632 		jww.ERROR.Println("Error:", err)
  633 	}
  634 
  635 	if h := c.hugoTry(); h != nil {
  636 		h.Close()
  637 	}
  638 
  639 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  640 	defer cancel()
  641 	wg2, ctx := errgroup.WithContext(ctx)
  642 	for _, srv := range servers {
  643 		srv := srv
  644 		wg2.Go(func() error {
  645 			return srv.Shutdown(ctx)
  646 		})
  647 	}
  648 
  649 	err1, err2 := wg1.Wait(), wg2.Wait()
  650 	if err1 != nil {
  651 		return err1
  652 	}
  653 	return err2
  654 }
  655 
  656 // fixURL massages the baseURL into a form needed for serving
  657 // all pages correctly.
  658 func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, error) {
  659 	useLocalhost := false
  660 	if s == "" {
  661 		s = cfg.GetString("baseURL")
  662 		useLocalhost = true
  663 	}
  664 
  665 	if !strings.HasSuffix(s, "/") {
  666 		s = s + "/"
  667 	}
  668 
  669 	// do an initial parse of the input string
  670 	u, err := url.Parse(s)
  671 	if err != nil {
  672 		return "", err
  673 	}
  674 
  675 	// if no Host is defined, then assume that no schema or double-slash were
  676 	// present in the url.  Add a double-slash and make a best effort attempt.
  677 	if u.Host == "" && s != "/" {
  678 		s = "//" + s
  679 
  680 		u, err = url.Parse(s)
  681 		if err != nil {
  682 			return "", err
  683 		}
  684 	}
  685 
  686 	if useLocalhost {
  687 		if u.Scheme == "https" {
  688 			u.Scheme = "http"
  689 		}
  690 		u.Host = "localhost"
  691 	}
  692 
  693 	if sc.serverAppend {
  694 		if strings.Contains(u.Host, ":") {
  695 			u.Host, _, err = net.SplitHostPort(u.Host)
  696 			if err != nil {
  697 				return "", fmt.Errorf("Failed to split baseURL hostpost: %w", err)
  698 			}
  699 		}
  700 		u.Host += fmt.Sprintf(":%d", port)
  701 	}
  702 
  703 	return u.String(), nil
  704 }
  705 
  706 func memStats() error {
  707 	b := newCommandsBuilder()
  708 	sc := b.newServerCmd().getCommand()
  709 	memstats := sc.Flags().Lookup("memstats").Value.String()
  710 	if memstats != "" {
  711 		interval, err := time.ParseDuration(sc.Flags().Lookup("meminterval").Value.String())
  712 		if err != nil {
  713 			interval, _ = time.ParseDuration("100ms")
  714 		}
  715 
  716 		fileMemStats, err := os.Create(memstats)
  717 		if err != nil {
  718 			return err
  719 		}
  720 
  721 		fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
  722 
  723 		go func() {
  724 			var stats runtime.MemStats
  725 
  726 			start := htime.Now().UnixNano()
  727 
  728 			for {
  729 				runtime.ReadMemStats(&stats)
  730 				if fileMemStats != nil {
  731 					fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
  732 						(htime.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
  733 					time.Sleep(interval)
  734 				} else {
  735 					break
  736 				}
  737 			}
  738 		}()
  739 	}
  740 	return nil
  741 }