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 }