mirror of
https://github.com/golang/go.git
synced 2025-05-07 00:23:03 +00:00
The exported Server becomes handlerServer, and part of Presentation now. Presentation is also now an http.Handler with its own internal mux (a detail, which might go away). main.go becomes ever simpler. R=golang-dev, adg CC=golang-dev https://golang.org/cl/11505043
533 lines
15 KiB
Go
533 lines
15 KiB
Go
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// godoc: Go Documentation Server
|
|
|
|
// Web server tree:
|
|
//
|
|
// http://godoc/ main landing page
|
|
// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, etc.
|
|
// http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed
|
|
// http://godoc/cmd/ serve documentation about commands
|
|
// http://godoc/pkg/ serve documentation about packages
|
|
// (idea is if you say import "compress/zlib", you go to
|
|
// http://godoc/pkg/compress/zlib)
|
|
//
|
|
// Command-line interface:
|
|
//
|
|
// godoc packagepath [name ...]
|
|
//
|
|
// godoc compress/zlib
|
|
// - prints doc for package compress/zlib
|
|
// godoc crypto/block Cipher NewCMAC
|
|
// - prints doc for Cipher and NewCMAC in package crypto/block
|
|
|
|
// +build !appengine
|
|
|
|
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
_ "expvar" // to serve /debug/vars
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/printer"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
_ "net/http/pprof" // to serve /debug/pprof/*
|
|
"net/url"
|
|
"os"
|
|
pathpkg "path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"code.google.com/p/go.tools/godoc"
|
|
"code.google.com/p/go.tools/godoc/vfs"
|
|
"code.google.com/p/go.tools/godoc/vfs/zipfs"
|
|
)
|
|
|
|
const defaultAddr = ":6060" // default webserver address
|
|
|
|
var (
|
|
// file system to serve
|
|
// (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh -i favicon.ico)
|
|
zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty")
|
|
|
|
// file-based index
|
|
writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files")
|
|
|
|
// network
|
|
httpAddr = flag.String("http", "", "HTTP service address (e.g., '"+defaultAddr+"')")
|
|
serverAddr = flag.String("server", "", "webserver address for command line searches")
|
|
|
|
// layout control
|
|
html = flag.Bool("html", false, "print HTML in command-line mode")
|
|
srcMode = flag.Bool("src", false, "print (exported) source in command-line mode")
|
|
urlFlag = flag.String("url", "", "print HTML for named URL")
|
|
|
|
// command-line searches
|
|
query = flag.Bool("q", false, "arguments are considered search queries")
|
|
|
|
verbose = flag.Bool("v", false, "verbose mode")
|
|
|
|
// file system roots
|
|
// TODO(gri) consider the invariant that goroot always end in '/'
|
|
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
|
|
|
|
// layout control
|
|
tabWidth = flag.Int("tabwidth", 4, "tab width")
|
|
showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings")
|
|
templateDir = flag.String("templates", "", "directory containing alternate template files")
|
|
showPlayground = flag.Bool("play", false, "enable playground in web interface")
|
|
showExamples = flag.Bool("ex", false, "show examples in command line mode")
|
|
declLinks = flag.Bool("links", true, "link identifiers to their declarations")
|
|
|
|
// search index
|
|
indexEnabled = flag.Bool("index", false, "enable search index")
|
|
indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+
|
|
"if not empty, the index is read from these files in sorted order")
|
|
maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown")
|
|
indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle")
|
|
|
|
// source code notes
|
|
notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show")
|
|
)
|
|
|
|
var (
|
|
pres *godoc.Presentation
|
|
fs = vfs.NameSpace{}
|
|
)
|
|
|
|
func registerPublicHandlers(mux *http.ServeMux) {
|
|
if pres == nil {
|
|
panic("nil Presentation")
|
|
}
|
|
mux.HandleFunc("/doc/codewalk/", codewalk)
|
|
mux.Handle("/doc/play/", pres.FileServer())
|
|
mux.HandleFunc("/search", pres.HandleSearch)
|
|
mux.Handle("/robots.txt", pres.FileServer())
|
|
mux.HandleFunc("/opensearch.xml", serveSearchDesc)
|
|
mux.Handle("/", pres)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Templates
|
|
|
|
func readTemplate(name string) *template.Template {
|
|
if pres == nil {
|
|
panic("no global Presentation set yet")
|
|
}
|
|
path := "lib/godoc/" + name
|
|
|
|
// use underlying file system fs to read the template file
|
|
// (cannot use template ParseFile functions directly)
|
|
data, err := vfs.ReadFile(fs, path)
|
|
if err != nil {
|
|
log.Fatal("readTemplate: ", err)
|
|
}
|
|
// be explicit with errors (for app engine use)
|
|
t, err := template.New(name).Funcs(pres.FuncMap()).Parse(string(data))
|
|
if err != nil {
|
|
log.Fatal("readTemplate: ", err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func readTemplates(p *godoc.Presentation) {
|
|
// have to delay until after flags processing since paths depend on goroot
|
|
codewalkHTML = readTemplate("codewalk.html")
|
|
codewalkdirHTML = readTemplate("codewalkdir.html")
|
|
p.DirlistHTML = readTemplate("dirlist.html")
|
|
p.ErrorHTML = readTemplate("error.html")
|
|
p.ExampleHTML = readTemplate("example.html")
|
|
p.GodocHTML = readTemplate("godoc.html")
|
|
p.PackageHTML = readTemplate("package.html")
|
|
p.PackageText = readTemplate("package.txt")
|
|
p.SearchHTML = readTemplate("search.html")
|
|
p.SearchText = readTemplate("search.txt")
|
|
p.SearchDescXML = readTemplate("opensearch.xml")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Files
|
|
|
|
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
|
var buf bytes.Buffer
|
|
if err := t.Execute(&buf, data); err != nil {
|
|
log.Printf("%s.Execute: %s", name, err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func serveSearchDesc(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
|
|
data := map[string]interface{}{
|
|
"BaseURL": fmt.Sprintf("http://%s", r.Host),
|
|
}
|
|
if err := pres.SearchDescXML.Execute(w, &data); err != nil && err != http.ErrBodyNotAllowed {
|
|
// Only log if there's an error that's not about writing on HEAD requests.
|
|
// See Issues 5451 and 5454.
|
|
log.Printf("searchDescXML.Execute: %s", err)
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Packages
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr,
|
|
"usage: godoc package [name ...]\n"+
|
|
" godoc -http="+defaultAddr+"\n")
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
|
|
func loggingHandler(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
log.Printf("%s\t%s", req.RemoteAddr, req.URL)
|
|
h.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
// Does s look like a regular expression?
|
|
func isRegexp(s string) bool {
|
|
return strings.IndexAny(s, ".(|)*+?^$[]") >= 0
|
|
}
|
|
|
|
// Make a regular expression of the form
|
|
// names[0]|names[1]|...names[len(names)-1].
|
|
// Returns nil if the regular expression is illegal.
|
|
func makeRx(names []string) (rx *regexp.Regexp) {
|
|
if len(names) > 0 {
|
|
s := ""
|
|
for i, name := range names {
|
|
if i > 0 {
|
|
s += "|"
|
|
}
|
|
if isRegexp(name) {
|
|
s += name
|
|
} else {
|
|
s += "^" + name + "$" // must match exactly
|
|
}
|
|
}
|
|
rx, _ = regexp.Compile(s) // rx is nil if there's a compilation error
|
|
}
|
|
return
|
|
}
|
|
|
|
func handleURLFlag() {
|
|
registerPublicHandlers(http.DefaultServeMux)
|
|
|
|
// Try up to 10 fetches, following redirects.
|
|
urlstr := *urlFlag
|
|
for i := 0; i < 10; i++ {
|
|
// Prepare request.
|
|
u, err := url.Parse(urlstr)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req := &http.Request{
|
|
URL: u,
|
|
}
|
|
|
|
// Invoke default HTTP handler to serve request
|
|
// to our buffering httpWriter.
|
|
w := httptest.NewRecorder()
|
|
http.DefaultServeMux.ServeHTTP(w, req)
|
|
|
|
// Return data, error, or follow redirect.
|
|
switch w.Code {
|
|
case 200: // ok
|
|
os.Stdout.Write(w.Body.Bytes())
|
|
return
|
|
case 301, 302, 303, 307: // redirect
|
|
redirect := w.HeaderMap.Get("Location")
|
|
if redirect == "" {
|
|
log.Fatalf("HTTP %d without Location header", w.Code)
|
|
}
|
|
urlstr = redirect
|
|
default:
|
|
log.Fatalf("HTTP error %d", w.Code)
|
|
}
|
|
}
|
|
log.Fatalf("too many redirects")
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
// Check usage: either server and no args, command line and args, or index creation mode
|
|
if (*httpAddr != "" || *urlFlag != "") != (flag.NArg() == 0) && !*writeIndex {
|
|
usage()
|
|
}
|
|
|
|
// Determine file system to use.
|
|
// TODO(gri) - fs and fsHttp should really be the same. Try to unify.
|
|
// - fsHttp doesn't need to be set up in command-line mode,
|
|
// same is true for the http handlers in initHandlers.
|
|
if *zipfile == "" {
|
|
// use file system of underlying OS
|
|
fs.Bind("/", vfs.OS(*goroot), "/", vfs.BindReplace)
|
|
if *templateDir != "" {
|
|
fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore)
|
|
}
|
|
} else {
|
|
// use file system specified via .zip file (path separator must be '/')
|
|
rc, err := zip.OpenReader(*zipfile)
|
|
if err != nil {
|
|
log.Fatalf("%s: %s\n", *zipfile, err)
|
|
}
|
|
defer rc.Close() // be nice (e.g., -writeIndex mode)
|
|
fs.Bind("/", zipfs.New(rc, *zipfile), *goroot, vfs.BindReplace)
|
|
}
|
|
|
|
// Bind $GOPATH trees into Go root.
|
|
for _, p := range filepath.SplitList(build.Default.GOPATH) {
|
|
fs.Bind("/src/pkg", vfs.OS(p), "/src", vfs.BindAfter)
|
|
}
|
|
|
|
corpus := godoc.NewCorpus(fs)
|
|
corpus.Verbose = *verbose
|
|
corpus.IndexEnabled = *indexEnabled
|
|
corpus.IndexFiles = *indexFiles
|
|
if *writeIndex {
|
|
corpus.IndexThrottle = 1.0
|
|
}
|
|
if err := corpus.Init(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
pres = godoc.NewPresentation(corpus)
|
|
pres.TabWidth = *tabWidth
|
|
pres.ShowTimestamps = *showTimestamps
|
|
pres.ShowPlayground = *showPlayground
|
|
pres.ShowExamples = *showExamples
|
|
pres.DeclLinks = *declLinks
|
|
if *notesRx != "" {
|
|
pres.NotesRx = regexp.MustCompile(*notesRx)
|
|
}
|
|
|
|
readTemplates(pres)
|
|
|
|
if *writeIndex {
|
|
// Write search index and exit.
|
|
if *indexFiles == "" {
|
|
log.Fatal("no index file specified")
|
|
}
|
|
|
|
log.Println("initialize file systems")
|
|
*verbose = true // want to see what happens
|
|
|
|
corpus.UpdateIndex()
|
|
|
|
log.Println("writing index file", *indexFiles)
|
|
f, err := os.Create(*indexFiles)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
index, _ := corpus.CurrentIndex()
|
|
err = index.Write(f)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
log.Println("done")
|
|
return
|
|
}
|
|
|
|
// Print content that would be served at the URL *urlFlag.
|
|
if *urlFlag != "" {
|
|
handleURLFlag()
|
|
return
|
|
}
|
|
|
|
if *httpAddr != "" {
|
|
// HTTP server mode.
|
|
var handler http.Handler = http.DefaultServeMux
|
|
if *verbose {
|
|
log.Printf("Go Documentation Server")
|
|
log.Printf("version = %s", runtime.Version())
|
|
log.Printf("address = %s", *httpAddr)
|
|
log.Printf("goroot = %s", *goroot)
|
|
log.Printf("tabwidth = %d", *tabWidth)
|
|
switch {
|
|
case !*indexEnabled:
|
|
log.Print("search index disabled")
|
|
case *maxResults > 0:
|
|
log.Printf("full text index enabled (maxresults = %d)", *maxResults)
|
|
default:
|
|
log.Print("identifier search index enabled")
|
|
}
|
|
fs.Fprint(os.Stderr)
|
|
handler = loggingHandler(handler)
|
|
}
|
|
|
|
registerPublicHandlers(http.DefaultServeMux)
|
|
|
|
// Initialize search index.
|
|
if *indexEnabled {
|
|
go corpus.RunIndexer()
|
|
}
|
|
|
|
// Start http server.
|
|
if err := http.ListenAndServe(*httpAddr, handler); err != nil {
|
|
log.Fatalf("ListenAndServe %s: %v", *httpAddr, err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
packageText := pres.PackageText
|
|
|
|
// Command line mode.
|
|
if *html {
|
|
packageText = pres.PackageHTML
|
|
}
|
|
|
|
if *query {
|
|
handleRemoteSearch()
|
|
return
|
|
}
|
|
|
|
// Determine paths.
|
|
//
|
|
// If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc,
|
|
// we need to map that path somewhere in the fs name space so that routines
|
|
// like getPageInfo will see it. We use the arbitrarily-chosen virtual path "/target"
|
|
// for this. That is, if we get passed a directory like the above, we map that
|
|
// directory so that getPageInfo sees it as /target.
|
|
const target = "/target"
|
|
const cmdPrefix = "cmd/"
|
|
path := flag.Arg(0)
|
|
var forceCmd bool
|
|
var abspath, relpath string
|
|
if filepath.IsAbs(path) {
|
|
fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
|
|
abspath = target
|
|
} else if build.IsLocalImport(path) {
|
|
cwd, _ := os.Getwd() // ignore errors
|
|
path = filepath.Join(cwd, path)
|
|
fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
|
|
abspath = target
|
|
} else if strings.HasPrefix(path, cmdPrefix) {
|
|
path = strings.TrimPrefix(path, cmdPrefix)
|
|
forceCmd = true
|
|
} else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" {
|
|
fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace)
|
|
abspath = target
|
|
relpath = bp.ImportPath
|
|
} else {
|
|
abspath = pathpkg.Join(pres.PkgFSRoot(), path)
|
|
}
|
|
if relpath == "" {
|
|
relpath = abspath
|
|
}
|
|
|
|
var mode godoc.PageInfoMode
|
|
if relpath == "builtin" {
|
|
// the fake built-in package contains unexported identifiers
|
|
mode = godoc.NoFiltering
|
|
}
|
|
if *srcMode {
|
|
// only filter exports if we don't have explicit command-line filter arguments
|
|
if flag.NArg() > 1 {
|
|
mode |= godoc.NoFiltering
|
|
}
|
|
mode |= godoc.ShowSource
|
|
}
|
|
|
|
// first, try as package unless forced as command
|
|
var info *godoc.PageInfo
|
|
if !forceCmd {
|
|
info = pres.GetPkgPageInfo(abspath, relpath, mode)
|
|
}
|
|
|
|
// second, try as command unless the path is absolute
|
|
// (the go command invokes godoc w/ absolute paths; don't override)
|
|
var cinfo *godoc.PageInfo
|
|
if !filepath.IsAbs(path) {
|
|
abspath = pathpkg.Join(pres.CmdFSRoot(), path)
|
|
cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
|
|
}
|
|
|
|
// determine what to use
|
|
if info == nil || info.IsEmpty() {
|
|
if cinfo != nil && !cinfo.IsEmpty() {
|
|
// only cinfo exists - switch to cinfo
|
|
info = cinfo
|
|
}
|
|
} else if cinfo != nil && !cinfo.IsEmpty() {
|
|
// both info and cinfo exist - use cinfo if info
|
|
// contains only subdirectory information
|
|
if info.PAst == nil && info.PDoc == nil {
|
|
info = cinfo
|
|
} else {
|
|
fmt.Printf("use 'godoc %s%s' for documentation on the %s command \n\n", cmdPrefix, relpath, relpath)
|
|
}
|
|
}
|
|
|
|
if info == nil {
|
|
log.Fatalf("%s: no such directory or package", flag.Arg(0))
|
|
}
|
|
if info.Err != nil {
|
|
log.Fatalf("%v", info.Err)
|
|
}
|
|
|
|
if info.PDoc != nil && info.PDoc.ImportPath == target {
|
|
// Replace virtual /target with actual argument from command line.
|
|
info.PDoc.ImportPath = flag.Arg(0)
|
|
}
|
|
|
|
// If we have more than one argument, use the remaining arguments for filtering.
|
|
if flag.NArg() > 1 {
|
|
args := flag.Args()[1:]
|
|
rx := makeRx(args)
|
|
if rx == nil {
|
|
log.Fatalf("illegal regular expression from %v", args)
|
|
}
|
|
|
|
filter := func(s string) bool { return rx.MatchString(s) }
|
|
switch {
|
|
case info.PAst != nil:
|
|
cmap := ast.NewCommentMap(info.FSet, info.PAst, info.PAst.Comments)
|
|
ast.FilterFile(info.PAst, filter)
|
|
// Special case: Don't use templates for printing
|
|
// so we only get the filtered declarations without
|
|
// package clause or extra whitespace.
|
|
for i, d := range info.PAst.Decls {
|
|
// determine the comments associated with d only
|
|
comments := cmap.Filter(d).Comments()
|
|
cn := &printer.CommentedNode{Node: d, Comments: comments}
|
|
if i > 0 {
|
|
fmt.Println()
|
|
}
|
|
if *html {
|
|
var buf bytes.Buffer
|
|
pres.WriteNode(&buf, info.FSet, cn)
|
|
godoc.FormatText(os.Stdout, buf.Bytes(), -1, true, "", nil)
|
|
} else {
|
|
pres.WriteNode(os.Stdout, info.FSet, cn)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
return
|
|
|
|
case info.PDoc != nil:
|
|
info.PDoc.Filter(filter)
|
|
}
|
|
}
|
|
|
|
if err := packageText.Execute(os.Stdout, info); err != nil {
|
|
log.Printf("packageText.Execute: %s", err)
|
|
}
|
|
}
|