github.com/elmarschill/hugo_sample@v0.47.1/commands/server.go (about)

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