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 }