github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/src/cmd/godoc/godoc.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "flag" 11 "fmt" 12 "go/ast" 13 "go/build" 14 "go/doc" 15 "go/format" 16 "go/printer" 17 "go/token" 18 htmlpkg "html" 19 "io" 20 "io/ioutil" 21 "log" 22 "net/http" 23 "net/url" 24 "os" 25 pathpkg "path" 26 "path/filepath" 27 "regexp" 28 "runtime" 29 "sort" 30 "strings" 31 "text/template" 32 "time" 33 "unicode" 34 "unicode/utf8" 35 ) 36 37 // ---------------------------------------------------------------------------- 38 // Globals 39 40 type delayTime struct { 41 RWValue 42 } 43 44 func (dt *delayTime) backoff(max time.Duration) { 45 dt.mutex.Lock() 46 v := dt.value.(time.Duration) * 2 47 if v > max { 48 v = max 49 } 50 dt.value = v 51 // don't change dt.timestamp - calling backoff indicates an error condition 52 dt.mutex.Unlock() 53 } 54 55 var ( 56 verbose = flag.Bool("v", false, "verbose mode") 57 58 // file system roots 59 // TODO(gri) consider the invariant that goroot always end in '/' 60 goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") 61 testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)") 62 63 // layout control 64 tabwidth = flag.Int("tabwidth", 4, "tab width") 65 showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings") 66 templateDir = flag.String("templates", "", "directory containing alternate template files") 67 showPlayground = flag.Bool("play", false, "enable playground in web interface") 68 showExamples = flag.Bool("ex", false, "show examples in command line mode") 69 declLinks = flag.Bool("links", true, "link identifiers to their declarations") 70 71 // search index 72 indexEnabled = flag.Bool("index", false, "enable search index") 73 indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+ 74 "if not empty, the index is read from these files in sorted order") 75 maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown") 76 indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") 77 78 // file system information 79 fsTree RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now) 80 fsModified RWValue // timestamp of last call to invalidateIndex 81 docMetadata RWValue // mapping from paths to *Metadata 82 83 // http handlers 84 fileServer http.Handler // default file server 85 cmdHandler docServer 86 pkgHandler docServer 87 88 // source code notes 89 notes = flag.String("notes", "BUG", "regular expression matching note markers to show") 90 ) 91 92 func initHandlers() { 93 fileServer = http.FileServer(&httpFS{fs}) 94 cmdHandler = docServer{"/cmd/", "/src/cmd"} 95 pkgHandler = docServer{"/pkg/", "/src/pkg"} 96 } 97 98 func registerPublicHandlers(mux *http.ServeMux) { 99 mux.Handle(cmdHandler.pattern, &cmdHandler) 100 mux.Handle(pkgHandler.pattern, &pkgHandler) 101 mux.HandleFunc("/doc/codewalk/", codewalk) 102 mux.Handle("/doc/play/", fileServer) 103 mux.HandleFunc("/search", search) 104 mux.Handle("/robots.txt", fileServer) 105 mux.HandleFunc("/opensearch.xml", serveSearchDesc) 106 mux.HandleFunc("/", serveFile) 107 } 108 109 func initFSTree() { 110 dir := newDirectory(pathpkg.Join("/", *testDir), -1) 111 if dir == nil { 112 log.Println("Warning: FSTree is nil") 113 return 114 } 115 fsTree.set(dir) 116 invalidateIndex() 117 } 118 119 // ---------------------------------------------------------------------------- 120 // Tab conversion 121 122 var spaces = []byte(" ") // 32 spaces seems like a good number 123 124 const ( 125 indenting = iota 126 collecting 127 ) 128 129 // A tconv is an io.Writer filter for converting leading tabs into spaces. 130 type tconv struct { 131 output io.Writer 132 state int // indenting or collecting 133 indent int // valid if state == indenting 134 } 135 136 func (p *tconv) writeIndent() (err error) { 137 i := p.indent 138 for i >= len(spaces) { 139 i -= len(spaces) 140 if _, err = p.output.Write(spaces); err != nil { 141 return 142 } 143 } 144 // i < len(spaces) 145 if i > 0 { 146 _, err = p.output.Write(spaces[0:i]) 147 } 148 return 149 } 150 151 func (p *tconv) Write(data []byte) (n int, err error) { 152 if len(data) == 0 { 153 return 154 } 155 pos := 0 // valid if p.state == collecting 156 var b byte 157 for n, b = range data { 158 switch p.state { 159 case indenting: 160 switch b { 161 case '\t': 162 p.indent += *tabwidth 163 case '\n': 164 p.indent = 0 165 if _, err = p.output.Write(data[n : n+1]); err != nil { 166 return 167 } 168 case ' ': 169 p.indent++ 170 default: 171 p.state = collecting 172 pos = n 173 if err = p.writeIndent(); err != nil { 174 return 175 } 176 } 177 case collecting: 178 if b == '\n' { 179 p.state = indenting 180 p.indent = 0 181 if _, err = p.output.Write(data[pos : n+1]); err != nil { 182 return 183 } 184 } 185 } 186 } 187 n = len(data) 188 if pos < n && p.state == collecting { 189 _, err = p.output.Write(data[pos:]) 190 } 191 return 192 } 193 194 // ---------------------------------------------------------------------------- 195 // Templates 196 197 // Write an AST node to w. 198 func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { 199 // convert trailing tabs into spaces using a tconv filter 200 // to ensure a good outcome in most browsers (there may still 201 // be tabs in comments and strings, but converting those into 202 // the right number of spaces is much harder) 203 // 204 // TODO(gri) rethink printer flags - perhaps tconv can be eliminated 205 // with an another printer mode (which is more efficiently 206 // implemented in the printer than here with another layer) 207 mode := printer.TabIndent | printer.UseSpaces 208 err := (&printer.Config{Mode: mode, Tabwidth: *tabwidth}).Fprint(&tconv{output: w}, fset, x) 209 if err != nil { 210 log.Print(err) 211 } 212 } 213 214 func filenameFunc(path string) string { 215 _, localname := pathpkg.Split(path) 216 return localname 217 } 218 219 func fileInfoNameFunc(fi os.FileInfo) string { 220 name := fi.Name() 221 if fi.IsDir() { 222 name += "/" 223 } 224 return name 225 } 226 227 func fileInfoTimeFunc(fi os.FileInfo) string { 228 if t := fi.ModTime(); t.Unix() != 0 { 229 return t.Local().String() 230 } 231 return "" // don't return epoch if time is obviously not set 232 } 233 234 // The strings in infoKinds must be properly html-escaped. 235 var infoKinds = [nKinds]string{ 236 PackageClause: "package clause", 237 ImportDecl: "import decl", 238 ConstDecl: "const decl", 239 TypeDecl: "type decl", 240 VarDecl: "var decl", 241 FuncDecl: "func decl", 242 MethodDecl: "method decl", 243 Use: "use", 244 } 245 246 func infoKind_htmlFunc(info SpotInfo) string { 247 return infoKinds[info.Kind()] // infoKind entries are html-escaped 248 } 249 250 func infoLineFunc(info SpotInfo) int { 251 line := info.Lori() 252 if info.IsIndex() { 253 index, _ := searchIndex.get() 254 if index != nil { 255 line = index.(*Index).Snippet(line).Line 256 } else { 257 // no line information available because 258 // we don't have an index - this should 259 // never happen; be conservative and don't 260 // crash 261 line = 0 262 } 263 } 264 return line 265 } 266 267 func infoSnippet_htmlFunc(info SpotInfo) string { 268 if info.IsIndex() { 269 index, _ := searchIndex.get() 270 // Snippet.Text was HTML-escaped when it was generated 271 return index.(*Index).Snippet(info.Lori()).Text 272 } 273 return `<span class="alert">no snippet text available</span>` 274 } 275 276 func nodeFunc(info *PageInfo, node interface{}) string { 277 var buf bytes.Buffer 278 writeNode(&buf, info.FSet, node) 279 return buf.String() 280 } 281 282 func node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { 283 var buf1 bytes.Buffer 284 writeNode(&buf1, info.FSet, node) 285 286 var buf2 bytes.Buffer 287 if n, _ := node.(ast.Node); n != nil && linkify && *declLinks { 288 LinkifyText(&buf2, buf1.Bytes(), n) 289 } else { 290 FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) 291 } 292 293 return buf2.String() 294 } 295 296 func comment_htmlFunc(comment string) string { 297 var buf bytes.Buffer 298 // TODO(gri) Provide list of words (e.g. function parameters) 299 // to be emphasized by ToHTML. 300 doc.ToHTML(&buf, comment, nil) // does html-escaping 301 return buf.String() 302 } 303 304 // punchCardWidth is the number of columns of fixed-width 305 // characters to assume when wrapping text. Very few people 306 // use terminals or cards smaller than 80 characters, so 80 it is. 307 // We do not try to sniff the environment or the tty to adapt to 308 // the situation; instead, by using a constant we make sure that 309 // godoc always produces the same output regardless of context, 310 // a consistency that is lost otherwise. For example, if we sniffed 311 // the environment or tty, then http://golang.org/pkg/math/?m=text 312 // would depend on the width of the terminal where godoc started, 313 // which is clearly bogus. More generally, the Unix tools that behave 314 // differently when writing to a tty than when writing to a file have 315 // a history of causing confusion (compare `ls` and `ls | cat`), and we 316 // want to avoid that mistake here. 317 const punchCardWidth = 80 318 319 func comment_textFunc(comment, indent, preIndent string) string { 320 var buf bytes.Buffer 321 doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent)) 322 return buf.String() 323 } 324 325 func startsWithUppercase(s string) bool { 326 r, _ := utf8.DecodeRuneInString(s) 327 return unicode.IsUpper(r) 328 } 329 330 var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`) 331 332 // stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name 333 // while keeping uppercase Braz in Foo_Braz. 334 func stripExampleSuffix(name string) string { 335 if i := strings.LastIndex(name, "_"); i != -1 { 336 if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { 337 name = name[:i] 338 } 339 } 340 return name 341 } 342 343 func example_textFunc(info *PageInfo, funcName, indent string) string { 344 if !*showExamples { 345 return "" 346 } 347 348 var buf bytes.Buffer 349 first := true 350 for _, eg := range info.Examples { 351 name := stripExampleSuffix(eg.Name) 352 if name != funcName { 353 continue 354 } 355 356 if !first { 357 buf.WriteString("\n") 358 } 359 first = false 360 361 // print code 362 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} 363 var buf1 bytes.Buffer 364 writeNode(&buf1, info.FSet, cnode) 365 code := buf1.String() 366 // Additional formatting if this is a function body. 367 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { 368 // remove surrounding braces 369 code = code[1 : n-1] 370 // unindent 371 code = strings.Replace(code, "\n ", "\n", -1) 372 } 373 code = strings.Trim(code, "\n") 374 code = strings.Replace(code, "\n", "\n\t", -1) 375 376 buf.WriteString(indent) 377 buf.WriteString("Example:\n\t") 378 buf.WriteString(code) 379 buf.WriteString("\n") 380 } 381 return buf.String() 382 } 383 384 func example_htmlFunc(info *PageInfo, funcName string) string { 385 var buf bytes.Buffer 386 for _, eg := range info.Examples { 387 name := stripExampleSuffix(eg.Name) 388 389 if name != funcName { 390 continue 391 } 392 393 // print code 394 cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} 395 code := node_htmlFunc(info, cnode, true) 396 out := eg.Output 397 wholeFile := true 398 399 // Additional formatting if this is a function body. 400 if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { 401 wholeFile = false 402 // remove surrounding braces 403 code = code[1 : n-1] 404 // unindent 405 code = strings.Replace(code, "\n ", "\n", -1) 406 // remove output comment 407 if loc := exampleOutputRx.FindStringIndex(code); loc != nil { 408 code = strings.TrimSpace(code[:loc[0]]) 409 } 410 } 411 412 // Write out the playground code in standard Go style 413 // (use tabs, no comment highlight, etc). 414 play := "" 415 if eg.Play != nil && *showPlayground { 416 var buf bytes.Buffer 417 if err := format.Node(&buf, info.FSet, eg.Play); err != nil { 418 log.Print(err) 419 } else { 420 play = buf.String() 421 } 422 } 423 424 // Drop output, as the output comment will appear in the code. 425 if wholeFile && play == "" { 426 out = "" 427 } 428 429 err := exampleHTML.Execute(&buf, struct { 430 Name, Doc, Code, Play, Output string 431 }{eg.Name, eg.Doc, code, play, out}) 432 if err != nil { 433 log.Print(err) 434 } 435 } 436 return buf.String() 437 } 438 439 // example_nameFunc takes an example function name and returns its display 440 // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". 441 func example_nameFunc(s string) string { 442 name, suffix := splitExampleName(s) 443 // replace _ with . for method names 444 name = strings.Replace(name, "_", ".", 1) 445 // use "Package" if no name provided 446 if name == "" { 447 name = "Package" 448 } 449 return name + suffix 450 } 451 452 // example_suffixFunc takes an example function name and returns its suffix in 453 // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". 454 func example_suffixFunc(name string) string { 455 _, suffix := splitExampleName(name) 456 return suffix 457 } 458 459 func noteTitle(note string) string { 460 return strings.Title(strings.ToLower(note)) 461 } 462 463 func splitExampleName(s string) (name, suffix string) { 464 i := strings.LastIndex(s, "_") 465 if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { 466 name = s[:i] 467 suffix = " (" + strings.Title(s[i+1:]) + ")" 468 return 469 } 470 name = s 471 return 472 } 473 474 func pkgLinkFunc(path string) string { 475 relpath := path[1:] 476 // because of the irregular mapping under goroot 477 // we need to correct certain relative paths 478 relpath = strings.TrimPrefix(relpath, "src/pkg/") 479 return pkgHandler.pattern[1:] + relpath // remove trailing '/' for relative URL 480 } 481 482 // n must be an ast.Node or a *doc.Note 483 func posLink_urlFunc(info *PageInfo, n interface{}) string { 484 var pos, end token.Pos 485 486 switch n := n.(type) { 487 case ast.Node: 488 pos = n.Pos() 489 end = n.End() 490 case *doc.Note: 491 pos = n.Pos 492 end = n.End 493 default: 494 panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) 495 } 496 497 var relpath string 498 var line int 499 var low, high int // selection offset range 500 501 if pos.IsValid() { 502 p := info.FSet.Position(pos) 503 relpath = p.Filename 504 line = p.Line 505 low = p.Offset 506 } 507 if end.IsValid() { 508 high = info.FSet.Position(end).Offset 509 } 510 511 var buf bytes.Buffer 512 template.HTMLEscape(&buf, []byte(relpath)) 513 // selection ranges are of form "s=low:high" 514 if low < high { 515 fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping 516 // if we have a selection, position the page 517 // such that the selection is a bit below the top 518 line -= 10 519 if line < 1 { 520 line = 1 521 } 522 } 523 // line id's in html-printed source are of the 524 // form "L%d" where %d stands for the line number 525 if line > 0 { 526 fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping 527 } 528 529 return buf.String() 530 } 531 532 func srcLinkFunc(s string) string { 533 return pathpkg.Clean("/" + s) 534 } 535 536 // fmap describes the template functions installed with all godoc templates. 537 // Convention: template function names ending in "_html" or "_url" produce 538 // HTML- or URL-escaped strings; all other function results may 539 // require explicit escaping in the template. 540 var fmap = template.FuncMap{ 541 // various helpers 542 "filename": filenameFunc, 543 "repeat": strings.Repeat, 544 545 // access to FileInfos (directory listings) 546 "fileInfoName": fileInfoNameFunc, 547 "fileInfoTime": fileInfoTimeFunc, 548 549 // access to search result information 550 "infoKind_html": infoKind_htmlFunc, 551 "infoLine": infoLineFunc, 552 "infoSnippet_html": infoSnippet_htmlFunc, 553 554 // formatting of AST nodes 555 "node": nodeFunc, 556 "node_html": node_htmlFunc, 557 "comment_html": comment_htmlFunc, 558 "comment_text": comment_textFunc, 559 560 // support for URL attributes 561 "pkgLink": pkgLinkFunc, 562 "srcLink": srcLinkFunc, 563 "posLink_url": posLink_urlFunc, 564 565 // formatting of Examples 566 "example_html": example_htmlFunc, 567 "example_text": example_textFunc, 568 "example_name": example_nameFunc, 569 "example_suffix": example_suffixFunc, 570 571 // formatting of Notes 572 "noteTitle": noteTitle, 573 } 574 575 func readTemplate(name string) *template.Template { 576 path := "lib/godoc/" + name 577 578 // use underlying file system fs to read the template file 579 // (cannot use template ParseFile functions directly) 580 data, err := ReadFile(fs, path) 581 if err != nil { 582 log.Fatal("readTemplate: ", err) 583 } 584 // be explicit with errors (for app engine use) 585 t, err := template.New(name).Funcs(fmap).Parse(string(data)) 586 if err != nil { 587 log.Fatal("readTemplate: ", err) 588 } 589 return t 590 } 591 592 var ( 593 codewalkHTML, 594 codewalkdirHTML, 595 dirlistHTML, 596 errorHTML, 597 exampleHTML, 598 godocHTML, 599 packageHTML, 600 packageText, 601 searchHTML, 602 searchText, 603 searchDescXML *template.Template 604 ) 605 606 func readTemplates() { 607 // have to delay until after flags processing since paths depend on goroot 608 codewalkHTML = readTemplate("codewalk.html") 609 codewalkdirHTML = readTemplate("codewalkdir.html") 610 dirlistHTML = readTemplate("dirlist.html") 611 errorHTML = readTemplate("error.html") 612 exampleHTML = readTemplate("example.html") 613 godocHTML = readTemplate("godoc.html") 614 packageHTML = readTemplate("package.html") 615 packageText = readTemplate("package.txt") 616 searchHTML = readTemplate("search.html") 617 searchText = readTemplate("search.txt") 618 searchDescXML = readTemplate("opensearch.xml") 619 } 620 621 // ---------------------------------------------------------------------------- 622 // Generic HTML wrapper 623 624 // Page describes the contents of the top-level godoc webpage. 625 type Page struct { 626 Title string 627 Tabtitle string 628 Subtitle string 629 Query string 630 Body []byte 631 632 // filled in by servePage 633 SearchBox bool 634 Playground bool 635 Version string 636 } 637 638 func servePage(w http.ResponseWriter, page Page) { 639 if page.Tabtitle == "" { 640 page.Tabtitle = page.Title 641 } 642 page.SearchBox = *indexEnabled 643 page.Playground = *showPlayground 644 page.Version = runtime.Version() 645 if err := godocHTML.Execute(w, page); err != nil { 646 log.Printf("godocHTML.Execute: %s", err) 647 } 648 } 649 650 func serveText(w http.ResponseWriter, text []byte) { 651 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 652 w.Write(text) 653 } 654 655 // ---------------------------------------------------------------------------- 656 // Files 657 658 var ( 659 doctype = []byte("<!DOCTYPE ") 660 jsonStart = []byte("<!--{") 661 jsonEnd = []byte("}-->") 662 ) 663 664 func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { 665 // get HTML body contents 666 src, err := ReadFile(fs, abspath) 667 if err != nil { 668 log.Printf("ReadFile: %s", err) 669 serveError(w, r, relpath, err) 670 return 671 } 672 673 // if it begins with "<!DOCTYPE " assume it is standalone 674 // html that doesn't need the template wrapping. 675 if bytes.HasPrefix(src, doctype) { 676 w.Write(src) 677 return 678 } 679 680 // if it begins with a JSON blob, read in the metadata. 681 meta, src, err := extractMetadata(src) 682 if err != nil { 683 log.Printf("decoding metadata %s: %v", relpath, err) 684 } 685 686 // evaluate as template if indicated 687 if meta.Template { 688 tmpl, err := template.New("main").Funcs(templateFuncs).Parse(string(src)) 689 if err != nil { 690 log.Printf("parsing template %s: %v", relpath, err) 691 serveError(w, r, relpath, err) 692 return 693 } 694 var buf bytes.Buffer 695 if err := tmpl.Execute(&buf, nil); err != nil { 696 log.Printf("executing template %s: %v", relpath, err) 697 serveError(w, r, relpath, err) 698 return 699 } 700 src = buf.Bytes() 701 } 702 703 // if it's the language spec, add tags to EBNF productions 704 if strings.HasSuffix(abspath, "go_spec.html") { 705 var buf bytes.Buffer 706 Linkify(&buf, src) 707 src = buf.Bytes() 708 } 709 710 servePage(w, Page{ 711 Title: meta.Title, 712 Subtitle: meta.Subtitle, 713 Body: src, 714 }) 715 } 716 717 func applyTemplate(t *template.Template, name string, data interface{}) []byte { 718 var buf bytes.Buffer 719 if err := t.Execute(&buf, data); err != nil { 720 log.Printf("%s.Execute: %s", name, err) 721 } 722 return buf.Bytes() 723 } 724 725 func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) { 726 canonical := pathpkg.Clean(r.URL.Path) 727 if !strings.HasSuffix(canonical, "/") { 728 canonical += "/" 729 } 730 if r.URL.Path != canonical { 731 url := *r.URL 732 url.Path = canonical 733 http.Redirect(w, r, url.String(), http.StatusMovedPermanently) 734 redirected = true 735 } 736 return 737 } 738 739 func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) { 740 c := pathpkg.Clean(r.URL.Path) 741 c = strings.TrimRight(c, "/") 742 if r.URL.Path != c { 743 url := *r.URL 744 url.Path = c 745 http.Redirect(w, r, url.String(), http.StatusMovedPermanently) 746 redirected = true 747 } 748 return 749 } 750 751 func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) { 752 src, err := ReadFile(fs, abspath) 753 if err != nil { 754 log.Printf("ReadFile: %s", err) 755 serveError(w, r, relpath, err) 756 return 757 } 758 759 if r.FormValue("m") == "text" { 760 serveText(w, src) 761 return 762 } 763 764 var buf bytes.Buffer 765 buf.WriteString("<pre>") 766 FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) 767 buf.WriteString("</pre>") 768 fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath)) 769 770 servePage(w, Page{ 771 Title: title + " " + relpath, 772 Tabtitle: relpath, 773 Body: buf.Bytes(), 774 }) 775 } 776 777 func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { 778 if redirect(w, r) { 779 return 780 } 781 782 list, err := fs.ReadDir(abspath) 783 if err != nil { 784 serveError(w, r, relpath, err) 785 return 786 } 787 788 servePage(w, Page{ 789 Title: "Directory " + relpath, 790 Tabtitle: relpath, 791 Body: applyTemplate(dirlistHTML, "dirlistHTML", list), 792 }) 793 } 794 795 func serveFile(w http.ResponseWriter, r *http.Request) { 796 relpath := r.URL.Path 797 798 // Check to see if we need to redirect or serve another file. 799 if m := metadataFor(relpath); m != nil { 800 if m.Path != relpath { 801 // Redirect to canonical path. 802 http.Redirect(w, r, m.Path, http.StatusMovedPermanently) 803 return 804 } 805 // Serve from the actual filesystem path. 806 relpath = m.filePath 807 } 808 809 abspath := relpath 810 relpath = relpath[1:] // strip leading slash 811 812 switch pathpkg.Ext(relpath) { 813 case ".html": 814 if strings.HasSuffix(relpath, "/index.html") { 815 // We'll show index.html for the directory. 816 // Use the dir/ version as canonical instead of dir/index.html. 817 http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) 818 return 819 } 820 serveHTMLDoc(w, r, abspath, relpath) 821 return 822 823 case ".go": 824 serveTextFile(w, r, abspath, relpath, "Source file") 825 return 826 } 827 828 dir, err := fs.Lstat(abspath) 829 if err != nil { 830 log.Print(err) 831 serveError(w, r, relpath, err) 832 return 833 } 834 835 if dir != nil && dir.IsDir() { 836 if redirect(w, r) { 837 return 838 } 839 if index := pathpkg.Join(abspath, "index.html"); isTextFile(index) { 840 serveHTMLDoc(w, r, index, index) 841 return 842 } 843 serveDirectory(w, r, abspath, relpath) 844 return 845 } 846 847 if isTextFile(abspath) { 848 if redirectFile(w, r) { 849 return 850 } 851 serveTextFile(w, r, abspath, relpath, "Text file") 852 return 853 } 854 855 fileServer.ServeHTTP(w, r) 856 } 857 858 func serveSearchDesc(w http.ResponseWriter, r *http.Request) { 859 w.Header().Set("Content-Type", "application/opensearchdescription+xml") 860 data := map[string]interface{}{ 861 "BaseURL": fmt.Sprintf("http://%s", r.Host), 862 } 863 if err := searchDescXML.Execute(w, &data); err != nil { 864 log.Printf("searchDescXML.Execute: %s", err) 865 } 866 } 867 868 // ---------------------------------------------------------------------------- 869 // Packages 870 871 // Fake relative package path for built-ins. Documentation for all globals 872 // (not just exported ones) will be shown for packages in this directory. 873 const builtinPkgPath = "builtin" 874 875 type PageInfoMode uint 876 877 const ( 878 noFiltering PageInfoMode = 1 << iota // do not filter exports 879 allMethods // show all embedded methods 880 showSource // show source code, do not extract documentation 881 noHtml // show result in textual form, do not generate HTML 882 flatDir // show directory in a flat (non-indented) manner 883 ) 884 885 // modeNames defines names for each PageInfoMode flag. 886 var modeNames = map[string]PageInfoMode{ 887 "all": noFiltering, 888 "methods": allMethods, 889 "src": showSource, 890 "text": noHtml, 891 "flat": flatDir, 892 } 893 894 // getPageInfoMode computes the PageInfoMode flags by analyzing the request 895 // URL form value "m". It is value is a comma-separated list of mode names 896 // as defined by modeNames (e.g.: m=src,text). 897 func getPageInfoMode(r *http.Request) PageInfoMode { 898 var mode PageInfoMode 899 for _, k := range strings.Split(r.FormValue("m"), ",") { 900 if m, found := modeNames[strings.TrimSpace(k)]; found { 901 mode |= m 902 } 903 } 904 return adjustPageInfoMode(r, mode) 905 } 906 907 // Specialized versions of godoc may adjust the PageInfoMode by overriding 908 // this variable. 909 var adjustPageInfoMode = func(_ *http.Request, mode PageInfoMode) PageInfoMode { 910 return mode 911 } 912 913 // remoteSearchURL returns the search URL for a given query as needed by 914 // remoteSearch. If html is set, an html result is requested; otherwise 915 // the result is in textual form. 916 // Adjust this function as necessary if modeNames or FormValue parameters 917 // change. 918 func remoteSearchURL(query string, html bool) string { 919 s := "/search?m=text&q=" 920 if html { 921 s = "/search?q=" 922 } 923 return s + url.QueryEscape(query) 924 } 925 926 type PageInfo struct { 927 Dirname string // directory containing the package 928 Err error // error or nil 929 930 // package info 931 FSet *token.FileSet // nil if no package documentation 932 PDoc *doc.Package // nil if no package documentation 933 Examples []*doc.Example // nil if no example code 934 Notes map[string][]*doc.Note // nil if no package Notes 935 PAst *ast.File // nil if no AST with package exports 936 IsMain bool // true for package main 937 938 // directory info 939 Dirs *DirList // nil if no directory information 940 DirTime time.Time // directory time stamp 941 DirFlat bool // if set, show directory in a flat (non-indented) manner 942 } 943 944 func (info *PageInfo) IsEmpty() bool { 945 return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil 946 } 947 948 type docServer struct { 949 pattern string // url pattern; e.g. "/pkg/" 950 fsRoot string // file system root to which the pattern is mapped 951 } 952 953 // fsReadDir implements ReadDir for the go/build package. 954 func fsReadDir(dir string) ([]os.FileInfo, error) { 955 return fs.ReadDir(filepath.ToSlash(dir)) 956 } 957 958 // fsOpenFile implements OpenFile for the go/build package. 959 func fsOpenFile(name string) (r io.ReadCloser, err error) { 960 data, err := ReadFile(fs, filepath.ToSlash(name)) 961 if err != nil { 962 return nil, err 963 } 964 return ioutil.NopCloser(bytes.NewReader(data)), nil 965 } 966 967 // packageExports is a local implementation of ast.PackageExports 968 // which correctly updates each package file's comment list. 969 // (The ast.PackageExports signature is frozen, hence the local 970 // implementation). 971 // 972 func packageExports(fset *token.FileSet, pkg *ast.Package) { 973 for _, src := range pkg.Files { 974 cmap := ast.NewCommentMap(fset, src, src.Comments) 975 ast.FileExports(src) 976 src.Comments = cmap.Filter(src).Comments() 977 } 978 } 979 980 // addNames adds the names declared by decl to the names set. 981 // Method names are added in the form ReceiverTypeName_Method. 982 func addNames(names map[string]bool, decl ast.Decl) { 983 switch d := decl.(type) { 984 case *ast.FuncDecl: 985 name := d.Name.Name 986 if d.Recv != nil { 987 var typeName string 988 switch r := d.Recv.List[0].Type.(type) { 989 case *ast.StarExpr: 990 typeName = r.X.(*ast.Ident).Name 991 case *ast.Ident: 992 typeName = r.Name 993 } 994 name = typeName + "_" + name 995 } 996 names[name] = true 997 case *ast.GenDecl: 998 for _, spec := range d.Specs { 999 switch s := spec.(type) { 1000 case *ast.TypeSpec: 1001 names[s.Name.Name] = true 1002 case *ast.ValueSpec: 1003 for _, id := range s.Names { 1004 names[id.Name] = true 1005 } 1006 } 1007 } 1008 } 1009 } 1010 1011 // globalNames returns a set of the names declared by all package-level 1012 // declarations. Method names are returned in the form Receiver_Method. 1013 func globalNames(pkg *ast.Package) map[string]bool { 1014 names := make(map[string]bool) 1015 for _, file := range pkg.Files { 1016 for _, decl := range file.Decls { 1017 addNames(names, decl) 1018 } 1019 } 1020 return names 1021 } 1022 1023 // collectExamples collects examples for pkg from testfiles. 1024 func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example { 1025 var files []*ast.File 1026 for _, f := range testfiles { 1027 files = append(files, f) 1028 } 1029 1030 var examples []*doc.Example 1031 globals := globalNames(pkg) 1032 for _, e := range doc.Examples(files...) { 1033 name := stripExampleSuffix(e.Name) 1034 if name == "" || globals[name] { 1035 examples = append(examples, e) 1036 } else { 1037 log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name) 1038 } 1039 } 1040 1041 return examples 1042 } 1043 1044 // poorMansImporter returns a (dummy) package object named 1045 // by the last path component of the provided package path 1046 // (as is the convention for packages). This is sufficient 1047 // to resolve package identifiers without doing an actual 1048 // import. It never returns an error. 1049 // 1050 func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { 1051 pkg := imports[path] 1052 if pkg == nil { 1053 // note that strings.LastIndex returns -1 if there is no "/" 1054 pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:]) 1055 pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import 1056 imports[path] = pkg 1057 } 1058 return pkg, nil 1059 } 1060 1061 // getPageInfo returns the PageInfo for a package directory abspath. If the 1062 // parameter genAST is set, an AST containing only the package exports is 1063 // computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc) 1064 // is extracted from the AST. If there is no corresponding package in the 1065 // directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub- 1066 // directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is 1067 // set to the respective error but the error is not logged. 1068 // 1069 func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo { 1070 info := &PageInfo{Dirname: abspath} 1071 1072 // Restrict to the package files that would be used when building 1073 // the package on this system. This makes sure that if there are 1074 // separate implementations for, say, Windows vs Unix, we don't 1075 // jumble them all together. 1076 // Note: Uses current binary's GOOS/GOARCH. 1077 // To use different pair, such as if we allowed the user to choose, 1078 // set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir. 1079 ctxt := build.Default 1080 ctxt.IsAbsPath = pathpkg.IsAbs 1081 ctxt.ReadDir = fsReadDir 1082 ctxt.OpenFile = fsOpenFile 1083 pkginfo, err := ctxt.ImportDir(abspath, 0) 1084 // continue if there are no Go source files; we still want the directory info 1085 if _, nogo := err.(*build.NoGoError); err != nil && !nogo { 1086 info.Err = err 1087 return info 1088 } 1089 1090 // collect package files 1091 pkgname := pkginfo.Name 1092 pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...) 1093 if len(pkgfiles) == 0 { 1094 // Commands written in C have no .go files in the build. 1095 // Instead, documentation may be found in an ignored file. 1096 // The file may be ignored via an explicit +build ignore 1097 // constraint (recommended), or by defining the package 1098 // documentation (historic). 1099 pkgname = "main" // assume package main since pkginfo.Name == "" 1100 pkgfiles = pkginfo.IgnoredGoFiles 1101 } 1102 1103 // get package information, if any 1104 if len(pkgfiles) > 0 { 1105 // build package AST 1106 fset := token.NewFileSet() 1107 files, err := parseFiles(fset, abspath, pkgfiles) 1108 if err != nil { 1109 info.Err = err 1110 return info 1111 } 1112 1113 // ignore any errors - they are due to unresolved identifiers 1114 pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil) 1115 1116 // extract package documentation 1117 info.FSet = fset 1118 if mode&showSource == 0 { 1119 // show extracted documentation 1120 var m doc.Mode 1121 if mode&noFiltering != 0 { 1122 m = doc.AllDecls 1123 } 1124 if mode&allMethods != 0 { 1125 m |= doc.AllMethods 1126 } 1127 info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath 1128 1129 // collect examples 1130 testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...) 1131 files, err = parseFiles(fset, abspath, testfiles) 1132 if err != nil { 1133 log.Println("parsing examples:", err) 1134 } 1135 info.Examples = collectExamples(pkg, files) 1136 1137 // collect any notes that we want to show 1138 if info.PDoc.Notes != nil { 1139 // could regexp.Compile only once per godoc, but probably not worth it 1140 if rx, err := regexp.Compile(*notes); err == nil { 1141 for m, n := range info.PDoc.Notes { 1142 if rx.MatchString(m) { 1143 if info.Notes == nil { 1144 info.Notes = make(map[string][]*doc.Note) 1145 } 1146 info.Notes[m] = n 1147 } 1148 } 1149 } 1150 } 1151 1152 } else { 1153 // show source code 1154 // TODO(gri) Consider eliminating export filtering in this mode, 1155 // or perhaps eliminating the mode altogether. 1156 if mode&noFiltering == 0 { 1157 packageExports(fset, pkg) 1158 } 1159 info.PAst = ast.MergePackageFiles(pkg, 0) 1160 } 1161 info.IsMain = pkgname == "main" 1162 } 1163 1164 // get directory information, if any 1165 var dir *Directory 1166 var timestamp time.Time 1167 if tree, ts := fsTree.get(); tree != nil && tree.(*Directory) != nil { 1168 // directory tree is present; lookup respective directory 1169 // (may still fail if the file system was updated and the 1170 // new directory tree has not yet been computed) 1171 dir = tree.(*Directory).lookup(abspath) 1172 timestamp = ts 1173 } 1174 if dir == nil { 1175 // no directory tree present (too early after startup or 1176 // command-line mode); compute one level for this page 1177 // note: cannot use path filter here because in general 1178 // it doesn't contain the fsTree path 1179 dir = newDirectory(abspath, 1) 1180 timestamp = time.Now() 1181 } 1182 info.Dirs = dir.listing(true) 1183 info.DirTime = timestamp 1184 info.DirFlat = mode&flatDir != 0 1185 1186 return info 1187 } 1188 1189 func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1190 if redirect(w, r) { 1191 return 1192 } 1193 1194 relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):]) 1195 abspath := pathpkg.Join(h.fsRoot, relpath) 1196 mode := getPageInfoMode(r) 1197 if relpath == builtinPkgPath { 1198 mode = noFiltering 1199 } 1200 info := h.getPageInfo(abspath, relpath, mode) 1201 if info.Err != nil { 1202 log.Print(info.Err) 1203 serveError(w, r, relpath, info.Err) 1204 return 1205 } 1206 1207 if mode&noHtml != 0 { 1208 serveText(w, applyTemplate(packageText, "packageText", info)) 1209 return 1210 } 1211 1212 var tabtitle, title, subtitle string 1213 switch { 1214 case info.PAst != nil: 1215 tabtitle = info.PAst.Name.Name 1216 case info.PDoc != nil: 1217 tabtitle = info.PDoc.Name 1218 default: 1219 tabtitle = info.Dirname 1220 title = "Directory " 1221 if *showTimestamps { 1222 subtitle = "Last update: " + info.DirTime.String() 1223 } 1224 } 1225 if title == "" { 1226 if info.IsMain { 1227 // assume that the directory name is the command name 1228 _, tabtitle = pathpkg.Split(relpath) 1229 title = "Command " 1230 } else { 1231 title = "Package " 1232 } 1233 } 1234 title += tabtitle 1235 1236 // special cases for top-level package/command directories 1237 switch tabtitle { 1238 case "/src/pkg": 1239 tabtitle = "Packages" 1240 case "/src/cmd": 1241 tabtitle = "Commands" 1242 } 1243 1244 servePage(w, Page{ 1245 Title: title, 1246 Tabtitle: tabtitle, 1247 Subtitle: subtitle, 1248 Body: applyTemplate(packageHTML, "packageHTML", info), 1249 }) 1250 } 1251 1252 // ---------------------------------------------------------------------------- 1253 // Search 1254 1255 var searchIndex RWValue 1256 1257 type SearchResult struct { 1258 Query string 1259 Alert string // error or warning message 1260 1261 // identifier matches 1262 Pak HitList // packages matching Query 1263 Hit *LookupResult // identifier matches of Query 1264 Alt *AltWords // alternative identifiers to look for 1265 1266 // textual matches 1267 Found int // number of textual occurrences found 1268 Textual []FileLines // textual matches of Query 1269 Complete bool // true if all textual occurrences of Query are reported 1270 } 1271 1272 func lookup(query string) (result SearchResult) { 1273 result.Query = query 1274 1275 index, timestamp := searchIndex.get() 1276 if index != nil { 1277 index := index.(*Index) 1278 1279 // identifier search 1280 var err error 1281 result.Pak, result.Hit, result.Alt, err = index.Lookup(query) 1282 if err != nil && *maxResults <= 0 { 1283 // ignore the error if full text search is enabled 1284 // since the query may be a valid regular expression 1285 result.Alert = "Error in query string: " + err.Error() 1286 return 1287 } 1288 1289 // full text search 1290 if *maxResults > 0 && query != "" { 1291 rx, err := regexp.Compile(query) 1292 if err != nil { 1293 result.Alert = "Error in query regular expression: " + err.Error() 1294 return 1295 } 1296 // If we get maxResults+1 results we know that there are more than 1297 // maxResults results and thus the result may be incomplete (to be 1298 // precise, we should remove one result from the result set, but 1299 // nobody is going to count the results on the result page). 1300 result.Found, result.Textual = index.LookupRegexp(rx, *maxResults+1) 1301 result.Complete = result.Found <= *maxResults 1302 if !result.Complete { 1303 result.Found-- // since we looked for maxResults+1 1304 } 1305 } 1306 } 1307 1308 // is the result accurate? 1309 if *indexEnabled { 1310 if _, ts := fsModified.get(); timestamp.Before(ts) { 1311 // The index is older than the latest file system change under godoc's observation. 1312 result.Alert = "Indexing in progress: result may be inaccurate" 1313 } 1314 } else { 1315 result.Alert = "Search index disabled: no results available" 1316 } 1317 1318 return 1319 } 1320 1321 func search(w http.ResponseWriter, r *http.Request) { 1322 query := strings.TrimSpace(r.FormValue("q")) 1323 result := lookup(query) 1324 1325 if getPageInfoMode(r)&noHtml != 0 { 1326 serveText(w, applyTemplate(searchText, "searchText", result)) 1327 return 1328 } 1329 1330 var title string 1331 if result.Hit != nil || len(result.Textual) > 0 { 1332 title = fmt.Sprintf(`Results for query %q`, query) 1333 } else { 1334 title = fmt.Sprintf(`No results found for query %q`, query) 1335 } 1336 1337 servePage(w, Page{ 1338 Title: title, 1339 Tabtitle: query, 1340 Query: query, 1341 Body: applyTemplate(searchHTML, "searchHTML", result), 1342 }) 1343 } 1344 1345 // ---------------------------------------------------------------------------- 1346 // Documentation Metadata 1347 1348 type Metadata struct { 1349 Title string 1350 Subtitle string 1351 Template bool // execute as template 1352 Path string // canonical path for this page 1353 filePath string // filesystem path relative to goroot 1354 } 1355 1356 // extractMetadata extracts the Metadata from a byte slice. 1357 // It returns the Metadata value and the remaining data. 1358 // If no metadata is present the original byte slice is returned. 1359 // 1360 func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) { 1361 tail = b 1362 if !bytes.HasPrefix(b, jsonStart) { 1363 return 1364 } 1365 end := bytes.Index(b, jsonEnd) 1366 if end < 0 { 1367 return 1368 } 1369 b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing } 1370 if err = json.Unmarshal(b, &meta); err != nil { 1371 return 1372 } 1373 tail = tail[end+len(jsonEnd):] 1374 return 1375 } 1376 1377 // updateMetadata scans $GOROOT/doc for HTML files, reads their metadata, 1378 // and updates the docMetadata map. 1379 // 1380 func updateMetadata() { 1381 metadata := make(map[string]*Metadata) 1382 var scan func(string) // scan is recursive 1383 scan = func(dir string) { 1384 fis, err := fs.ReadDir(dir) 1385 if err != nil { 1386 log.Println("updateMetadata:", err) 1387 return 1388 } 1389 for _, fi := range fis { 1390 name := pathpkg.Join(dir, fi.Name()) 1391 if fi.IsDir() { 1392 scan(name) // recurse 1393 continue 1394 } 1395 if !strings.HasSuffix(name, ".html") { 1396 continue 1397 } 1398 // Extract metadata from the file. 1399 b, err := ReadFile(fs, name) 1400 if err != nil { 1401 log.Printf("updateMetadata %s: %v", name, err) 1402 continue 1403 } 1404 meta, _, err := extractMetadata(b) 1405 if err != nil { 1406 log.Printf("updateMetadata: %s: %v", name, err) 1407 continue 1408 } 1409 // Store relative filesystem path in Metadata. 1410 meta.filePath = name 1411 if meta.Path == "" { 1412 // If no Path, canonical path is actual path. 1413 meta.Path = meta.filePath 1414 } 1415 // Store under both paths. 1416 metadata[meta.Path] = &meta 1417 metadata[meta.filePath] = &meta 1418 } 1419 } 1420 scan("/doc") 1421 docMetadata.set(metadata) 1422 } 1423 1424 // Send a value on this channel to trigger a metadata refresh. 1425 // It is buffered so that if a signal is not lost if sent during a refresh. 1426 // 1427 var refreshMetadataSignal = make(chan bool, 1) 1428 1429 // refreshMetadata sends a signal to update docMetadata. If a refresh is in 1430 // progress the metadata will be refreshed again afterward. 1431 // 1432 func refreshMetadata() { 1433 select { 1434 case refreshMetadataSignal <- true: 1435 default: 1436 } 1437 } 1438 1439 // refreshMetadataLoop runs forever, updating docMetadata when the underlying 1440 // file system changes. It should be launched in a goroutine by main. 1441 // 1442 func refreshMetadataLoop() { 1443 for { 1444 <-refreshMetadataSignal 1445 updateMetadata() 1446 time.Sleep(10 * time.Second) // at most once every 10 seconds 1447 } 1448 } 1449 1450 // metadataFor returns the *Metadata for a given relative path or nil if none 1451 // exists. 1452 // 1453 func metadataFor(relpath string) *Metadata { 1454 if m, _ := docMetadata.get(); m != nil { 1455 meta := m.(map[string]*Metadata) 1456 // If metadata for this relpath exists, return it. 1457 if p := meta[relpath]; p != nil { 1458 return p 1459 } 1460 // Try with or without trailing slash. 1461 if strings.HasSuffix(relpath, "/") { 1462 relpath = relpath[:len(relpath)-1] 1463 } else { 1464 relpath = relpath + "/" 1465 } 1466 return meta[relpath] 1467 } 1468 return nil 1469 } 1470 1471 // ---------------------------------------------------------------------------- 1472 // Indexer 1473 1474 // invalidateIndex should be called whenever any of the file systems 1475 // under godoc's observation change so that the indexer is kicked on. 1476 // 1477 func invalidateIndex() { 1478 fsModified.set(nil) 1479 refreshMetadata() 1480 } 1481 1482 // indexUpToDate() returns true if the search index is not older 1483 // than any of the file systems under godoc's observation. 1484 // 1485 func indexUpToDate() bool { 1486 _, fsTime := fsModified.get() 1487 _, siTime := searchIndex.get() 1488 return !fsTime.After(siTime) 1489 } 1490 1491 // feedDirnames feeds the directory names of all directories 1492 // under the file system given by root to channel c. 1493 // 1494 func feedDirnames(root *RWValue, c chan<- string) { 1495 if dir, _ := root.get(); dir != nil { 1496 for d := range dir.(*Directory).iter(false) { 1497 c <- d.Path 1498 } 1499 } 1500 } 1501 1502 // fsDirnames() returns a channel sending all directory names 1503 // of all the file systems under godoc's observation. 1504 // 1505 func fsDirnames() <-chan string { 1506 c := make(chan string, 256) // buffered for fewer context switches 1507 go func() { 1508 feedDirnames(&fsTree, c) 1509 close(c) 1510 }() 1511 return c 1512 } 1513 1514 func readIndex(filenames string) error { 1515 matches, err := filepath.Glob(filenames) 1516 if err != nil { 1517 return err 1518 } else if matches == nil { 1519 return fmt.Errorf("no index files match %q", filenames) 1520 } 1521 sort.Strings(matches) // make sure files are in the right order 1522 files := make([]io.Reader, 0, len(matches)) 1523 for _, filename := range matches { 1524 f, err := os.Open(filename) 1525 if err != nil { 1526 return err 1527 } 1528 defer f.Close() 1529 files = append(files, f) 1530 } 1531 x := new(Index) 1532 if err := x.Read(io.MultiReader(files...)); err != nil { 1533 return err 1534 } 1535 searchIndex.set(x) 1536 return nil 1537 } 1538 1539 func updateIndex() { 1540 if *verbose { 1541 log.Printf("updating index...") 1542 } 1543 start := time.Now() 1544 index := NewIndex(fsDirnames(), *maxResults > 0, *indexThrottle) 1545 stop := time.Now() 1546 searchIndex.set(index) 1547 if *verbose { 1548 secs := stop.Sub(start).Seconds() 1549 stats := index.Stats() 1550 log.Printf("index updated (%gs, %d bytes of source, %d files, %d lines, %d unique words, %d spots)", 1551 secs, stats.Bytes, stats.Files, stats.Lines, stats.Words, stats.Spots) 1552 } 1553 memstats := new(runtime.MemStats) 1554 runtime.ReadMemStats(memstats) 1555 log.Printf("before GC: bytes = %d footprint = %d", memstats.HeapAlloc, memstats.Sys) 1556 runtime.GC() 1557 runtime.ReadMemStats(memstats) 1558 log.Printf("after GC: bytes = %d footprint = %d", memstats.HeapAlloc, memstats.Sys) 1559 } 1560 1561 func indexer() { 1562 // initialize the index from disk if possible 1563 if *indexFiles != "" { 1564 if err := readIndex(*indexFiles); err != nil { 1565 log.Printf("error reading index: %s", err) 1566 } 1567 } 1568 1569 // repeatedly update the index when it goes out of date 1570 for { 1571 if !indexUpToDate() { 1572 // index possibly out of date - make a new one 1573 updateIndex() 1574 } 1575 delay := 60 * time.Second // by default, try every 60s 1576 if *testDir != "" { 1577 // in test mode, try once a second for fast startup 1578 delay = 1 * time.Second 1579 } 1580 time.Sleep(delay) 1581 } 1582 }