| 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 | // godoc: Go Documentation Server |
| 6 | |
| 7 | // Web server tree: |
| 8 | // |
| 9 | // http://godoc/ redirect to /pkg/ |
| 10 | // http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed |
| 11 | // http://godoc/cmd/ serve documentation about commands |
| 12 | // http://godoc/pkg/ serve documentation about packages |
| 13 | // (idea is if you say import "compress/zlib", you go to |
| 14 | // http://godoc/pkg/compress/zlib) |
| 15 | // |
| 16 | |
| 17 | package main |
| 18 | |
| 19 | import ( |
| 20 | "archive/zip" |
| 21 | "bytes" |
| 22 | "context" |
| 23 | "encoding/json" |
| 24 | "errors" |
| 25 | _ "expvar" // to serve /debug/vars |
| 26 | "flag" |
| 27 | "fmt" |
| 28 | "go/build" |
| 29 | "io" |
| 30 | "log" |
| 31 | "net/http" |
| 32 | _ "net/http/pprof" // to serve /debug/pprof/* |
| 33 | "net/url" |
| 34 | "os" |
| 35 | "path" |
| 36 | "path/filepath" |
| 37 | "regexp" |
| 38 | "runtime" |
| 39 | "strings" |
| 40 | |
| 41 | exec "golang.org/x/sys/execabs" |
| 42 | |
| 43 | "golang.org/x/tools/godoc" |
| 44 | "golang.org/x/tools/godoc/static" |
| 45 | "golang.org/x/tools/godoc/vfs" |
| 46 | "golang.org/x/tools/godoc/vfs/gatefs" |
| 47 | "golang.org/x/tools/godoc/vfs/mapfs" |
| 48 | "golang.org/x/tools/godoc/vfs/zipfs" |
| 49 | "golang.org/x/tools/internal/gocommand" |
| 50 | ) |
| 51 | |
| 52 | const defaultAddr = "localhost:6060" // default webserver address |
| 53 | |
| 54 | var ( |
| 55 | // file system to serve |
| 56 | // (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) |
| 57 | zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty") |
| 58 | |
| 59 | // file-based index |
| 60 | writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files") |
| 61 | |
| 62 | // network |
| 63 | httpAddr = flag.String("http", defaultAddr, "HTTP service address") |
| 64 | |
| 65 | // layout control |
| 66 | urlFlag = flag.String("url", "", "print HTML for named URL") |
| 67 | |
| 68 | verbose = flag.Bool("v", false, "verbose mode") |
| 69 | |
| 70 | // file system roots |
| 71 | // TODO(gri) consider the invariant that goroot always end in '/' |
| 72 | goroot = flag.String("goroot", findGOROOT(), "Go root directory") |
| 73 | |
| 74 | // layout control |
| 75 | showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings") |
| 76 | templateDir = flag.String("templates", "", "load templates/JS/CSS from disk in this directory") |
| 77 | showPlayground = flag.Bool("play", false, "enable playground") |
| 78 | declLinks = flag.Bool("links", true, "link identifiers to their declarations") |
| 79 | |
| 80 | // search index |
| 81 | indexEnabled = flag.Bool("index", false, "enable search index") |
| 82 | indexFiles = flag.String("index_files", "", "glob pattern specifying index files; if not empty, the index is read from these files in sorted order") |
| 83 | indexInterval = flag.Duration("index_interval", 0, "interval of indexing; 0 for default (5m), negative to only index once at startup") |
| 84 | maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown") |
| 85 | indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") |
| 86 | |
| 87 | // source code notes |
| 88 | notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show") |
| 89 | ) |
| 90 | |
| 91 | // An httpResponseRecorder is an http.ResponseWriter |
| 92 | type httpResponseRecorder struct { |
| 93 | body *bytes.Buffer |
| 94 | header http.Header |
| 95 | code int |
| 96 | } |
| 97 | |
| 98 | func (w *httpResponseRecorder) Header() http.Header { return w.header } |
| 99 | func (w *httpResponseRecorder) Write(b []byte) (int, error) { return w.body.Write(b) } |
| 100 | func (w *httpResponseRecorder) WriteHeader(code int) { w.code = code } |
| 101 | |
| 102 | func usage() { |
| 103 | fmt.Fprintf(os.Stderr, "usage: godoc -http="+defaultAddr+"\n") |
| 104 | flag.PrintDefaults() |
| 105 | os.Exit(2) |
| 106 | } |
| 107 | |
| 108 | func loggingHandler(h http.Handler) http.Handler { |
| 109 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| 110 | log.Printf("%s\t%s", req.RemoteAddr, req.URL) |
| 111 | h.ServeHTTP(w, req) |
| 112 | }) |
| 113 | } |
| 114 | |
| 115 | func handleURLFlag() { |
| 116 | // Try up to 10 fetches, following redirects. |
| 117 | urlstr := *urlFlag |
| 118 | for i := 0; i < 10; i++ { |
| 119 | // Prepare request. |
| 120 | u, err := url.Parse(urlstr) |
| 121 | if err != nil { |
| 122 | log.Fatal(err) |
| 123 | } |
| 124 | req := &http.Request{ |
| 125 | URL: u, |
| 126 | } |
| 127 | |
| 128 | // Invoke default HTTP handler to serve request |
| 129 | // to our buffering httpWriter. |
| 130 | w := &httpResponseRecorder{code: 200, header: make(http.Header), body: new(bytes.Buffer)} |
| 131 | http.DefaultServeMux.ServeHTTP(w, req) |
| 132 | |
| 133 | // Return data, error, or follow redirect. |
| 134 | switch w.code { |
| 135 | case 200: // ok |
| 136 | os.Stdout.Write(w.body.Bytes()) |
| 137 | return |
| 138 | case 301, 302, 303, 307: // redirect |
| 139 | redirect := w.header.Get("Location") |
| 140 | if redirect == "" { |
| 141 | log.Fatalf("HTTP %d without Location header", w.code) |
| 142 | } |
| 143 | urlstr = redirect |
| 144 | default: |
| 145 | log.Fatalf("HTTP error %d", w.code) |
| 146 | } |
| 147 | } |
| 148 | log.Fatalf("too many redirects") |
| 149 | } |
| 150 | |
| 151 | func initCorpus(corpus *godoc.Corpus) { |
| 152 | err := corpus.Init() |
| 153 | if err != nil { |
| 154 | log.Fatal(err) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | func main() { |
| 159 | flag.Usage = usage |
| 160 | flag.Parse() |
| 161 | |
| 162 | // Check usage. |
| 163 | if flag.NArg() > 0 { |
| 164 | fmt.Fprintln(os.Stderr, `Unexpected arguments. Use "go doc" for command-line help output instead. For example, "go doc fmt.Printf".`) |
| 165 | usage() |
| 166 | } |
| 167 | if *httpAddr == "" && *urlFlag == "" && !*writeIndex { |
| 168 | fmt.Fprintln(os.Stderr, "At least one of -http, -url, or -write_index must be set to a non-zero value.") |
| 169 | usage() |
| 170 | } |
| 171 | |
| 172 | // Set the resolved goroot. |
| 173 | vfs.GOROOT = *goroot |
| 174 | |
| 175 | fsGate := make(chan bool, 20) |
| 176 | |
| 177 | // Determine file system to use. |
| 178 | if *zipfile == "" { |
| 179 | // use file system of underlying OS |
| 180 | rootfs := gatefs.New(vfs.OS(*goroot), fsGate) |
| 181 | fs.Bind("/", rootfs, "/", vfs.BindReplace) |
| 182 | } else { |
| 183 | // use file system specified via .zip file (path separator must be '/') |
| 184 | rc, err := zip.OpenReader(*zipfile) |
| 185 | if err != nil { |
| 186 | log.Fatalf("%s: %s\n", *zipfile, err) |
| 187 | } |
| 188 | defer rc.Close() // be nice (e.g., -writeIndex mode) |
| 189 | fs.Bind("/", zipfs.New(rc, *zipfile), *goroot, vfs.BindReplace) |
| 190 | } |
| 191 | if *templateDir != "" { |
| 192 | fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore) |
| 193 | fs.Bind("/favicon.ico", vfs.OS(*templateDir), "/favicon.ico", vfs.BindReplace) |
| 194 | } else { |
| 195 | fs.Bind("/lib/godoc", mapfs.New(static.Files), "/", vfs.BindReplace) |
| 196 | fs.Bind("/favicon.ico", mapfs.New(static.Files), "/favicon.ico", vfs.BindReplace) |
| 197 | } |
| 198 | |
| 199 | // Get the GOMOD value, use it to determine if godoc is being invoked in module mode. |
| 200 | goModFile, err := goMod() |
| 201 | if err != nil { |
| 202 | fmt.Fprintf(os.Stderr, "failed to determine go env GOMOD value: %v", err) |
| 203 | goModFile = "" // Fall back to GOPATH mode. |
| 204 | } |
| 205 | |
| 206 | if goModFile != "" { |
| 207 | fmt.Printf("using module mode; GOMOD=%s\n", goModFile) |
| 208 | |
| 209 | // Detect whether to use vendor mode or not. |
| 210 | vendorEnabled, mainModVendor, err := gocommand.VendorEnabled(context.Background(), gocommand.Invocation{}, &gocommand.Runner{}) |
| 211 | if err != nil { |
| 212 | fmt.Fprintf(os.Stderr, "failed to determine if vendoring is enabled: %v", err) |
| 213 | os.Exit(1) |
| 214 | } |
| 215 | if vendorEnabled { |
| 216 | // Bind the root directory of the main module. |
| 217 | fs.Bind(path.Join("/src", mainModVendor.Path), gatefs.New(vfs.OS(mainModVendor.Dir), fsGate), "/", vfs.BindAfter) |
| 218 | |
| 219 | // Bind the vendor directory. |
| 220 | // |
| 221 | // Note that in module mode, vendor directories in locations |
| 222 | // other than the main module's root directory are ignored. |
| 223 | // See https://golang.org/ref/mod#vendoring. |
| 224 | vendorDir := filepath.Join(mainModVendor.Dir, "vendor") |
| 225 | fs.Bind("/src", gatefs.New(vfs.OS(vendorDir), fsGate), "/", vfs.BindAfter) |
| 226 | |
| 227 | } else { |
| 228 | // Try to download dependencies that are not in the module cache in order to |
| 229 | // to show their documentation. |
| 230 | // This may fail if module downloading is disallowed (GOPROXY=off) or due to |
| 231 | // limited connectivity, in which case we print errors to stderr and show |
| 232 | // documentation only for packages that are available. |
| 233 | fillModuleCache(os.Stderr, goModFile) |
| 234 | |
| 235 | // Determine modules in the build list. |
| 236 | mods, err := buildList(goModFile) |
| 237 | if err != nil { |
| 238 | fmt.Fprintf(os.Stderr, "failed to determine the build list of the main module: %v", err) |
| 239 | os.Exit(1) |
| 240 | } |
| 241 | |
| 242 | // Bind module trees into Go root. |
| 243 | for _, m := range mods { |
| 244 | if m.Dir == "" { |
| 245 | // Module is not available in the module cache, skip it. |
| 246 | continue |
| 247 | } |
| 248 | dst := path.Join("/src", m.Path) |
| 249 | fs.Bind(dst, gatefs.New(vfs.OS(m.Dir), fsGate), "/", vfs.BindAfter) |
| 250 | } |
| 251 | } |
| 252 | } else { |
| 253 | fmt.Println("using GOPATH mode") |
| 254 | |
| 255 | // Bind $GOPATH trees into Go root. |
| 256 | for _, p := range filepath.SplitList(build.Default.GOPATH) { |
| 257 | fs.Bind("/src", gatefs.New(vfs.OS(p), fsGate), "/src", vfs.BindAfter) |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | var corpus *godoc.Corpus |
| 262 | if goModFile != "" { |
| 263 | corpus = godoc.NewCorpus(moduleFS{fs}) |
| 264 | } else { |
| 265 | corpus = godoc.NewCorpus(fs) |
| 266 | } |
| 267 | corpus.Verbose = *verbose |
| 268 | corpus.MaxResults = *maxResults |
| 269 | corpus.IndexEnabled = *indexEnabled |
| 270 | if *maxResults == 0 { |
| 271 | corpus.IndexFullText = false |
| 272 | } |
| 273 | corpus.IndexFiles = *indexFiles |
| 274 | corpus.IndexDirectory = func(dir string) bool { |
| 275 | return dir != "/pkg" && !strings.HasPrefix(dir, "/pkg/") |
| 276 | } |
| 277 | corpus.IndexThrottle = *indexThrottle |
| 278 | corpus.IndexInterval = *indexInterval |
| 279 | if *writeIndex || *urlFlag != "" { |
| 280 | corpus.IndexThrottle = 1.0 |
| 281 | corpus.IndexEnabled = true |
| 282 | initCorpus(corpus) |
| 283 | } else { |
| 284 | go initCorpus(corpus) |
| 285 | } |
| 286 | |
| 287 | // Initialize the version info before readTemplates, which saves |
| 288 | // the map value in a method value. |
| 289 | corpus.InitVersionInfo() |
| 290 | |
| 291 | pres = godoc.NewPresentation(corpus) |
| 292 | pres.ShowTimestamps = *showTimestamps |
| 293 | pres.ShowPlayground = *showPlayground |
| 294 | pres.DeclLinks = *declLinks |
| 295 | if *notesRx != "" { |
| 296 | pres.NotesRx = regexp.MustCompile(*notesRx) |
| 297 | } |
| 298 | |
| 299 | readTemplates(pres) |
| 300 | registerHandlers(pres) |
| 301 | |
| 302 | if *writeIndex { |
| 303 | // Write search index and exit. |
| 304 | if *indexFiles == "" { |
| 305 | log.Fatal("no index file specified") |
| 306 | } |
| 307 | |
| 308 | log.Println("initialize file systems") |
| 309 | *verbose = true // want to see what happens |
| 310 | |
| 311 | corpus.UpdateIndex() |
| 312 | |
| 313 | log.Println("writing index file", *indexFiles) |
| 314 | f, err := os.Create(*indexFiles) |
| 315 | if err != nil { |
| 316 | log.Fatal(err) |
| 317 | } |
| 318 | index, _ := corpus.CurrentIndex() |
| 319 | _, err = index.WriteTo(f) |
| 320 | if err != nil { |
| 321 | log.Fatal(err) |
| 322 | } |
| 323 | |
| 324 | log.Println("done") |
| 325 | return |
| 326 | } |
| 327 | |
| 328 | // Print content that would be served at the URL *urlFlag. |
| 329 | if *urlFlag != "" { |
| 330 | handleURLFlag() |
| 331 | return |
| 332 | } |
| 333 | |
| 334 | var handler http.Handler = http.DefaultServeMux |
| 335 | if *verbose { |
| 336 | log.Printf("Go Documentation Server") |
| 337 | log.Printf("version = %s", runtime.Version()) |
| 338 | log.Printf("address = %s", *httpAddr) |
| 339 | log.Printf("goroot = %s", *goroot) |
| 340 | switch { |
| 341 | case !*indexEnabled: |
| 342 | log.Print("search index disabled") |
| 343 | case *maxResults > 0: |
| 344 | log.Printf("full text index enabled (maxresults = %d)", *maxResults) |
| 345 | default: |
| 346 | log.Print("identifier search index enabled") |
| 347 | } |
| 348 | fs.Fprint(os.Stderr) |
| 349 | handler = loggingHandler(handler) |
| 350 | } |
| 351 | |
| 352 | // Initialize search index. |
| 353 | if *indexEnabled { |
| 354 | go corpus.RunIndexer() |
| 355 | } |
| 356 | |
| 357 | // Start http server. |
| 358 | if *verbose { |
| 359 | log.Println("starting HTTP server") |
| 360 | } |
| 361 | if err := http.ListenAndServe(*httpAddr, handler); err != nil { |
| 362 | log.Fatalf("ListenAndServe %s: %v", *httpAddr, err) |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | // goMod returns the go env GOMOD value in the current directory |
| 367 | // by invoking the go command. |
| 368 | // |
| 369 | // GOMOD is documented at https://golang.org/cmd/go/#hdr-Environment_variables: |
| 370 | // |
| 371 | // The absolute path to the go.mod of the main module, |
| 372 | // or the empty string if not using modules. |
| 373 | func goMod() (string, error) { |
| 374 | out, err := exec.Command("go", "env", "-json", "GOMOD").Output() |
| 375 | if ee := (*exec.ExitError)(nil); errors.As(err, &ee) { |
| 376 | return "", fmt.Errorf("go command exited unsuccessfully: %v\n%s", ee.ProcessState.String(), ee.Stderr) |
| 377 | } else if err != nil { |
| 378 | return "", err |
| 379 | } |
| 380 | var env struct { |
| 381 | GoMod string |
| 382 | } |
| 383 | err = json.Unmarshal(out, &env) |
| 384 | if err != nil { |
| 385 | return "", err |
| 386 | } |
| 387 | return env.GoMod, nil |
| 388 | } |
| 389 | |
| 390 | // fillModuleCache does a best-effort attempt to fill the module cache |
| 391 | // with all dependencies of the main module in the current directory |
| 392 | // by invoking the go command. Module download logs are streamed to w. |
| 393 | // If there are any problems encountered, they are also written to w. |
| 394 | // It should only be used in module mode, when vendor mode isn't on. |
| 395 | // |
| 396 | // See https://golang.org/cmd/go/#hdr-Download_modules_to_local_cache. |
| 397 | func fillModuleCache(w io.Writer, goMod string) { |
| 398 | if goMod == os.DevNull { |
| 399 | // No module requirements, nothing to do. |
| 400 | return |
| 401 | } |
| 402 | |
| 403 | cmd := exec.Command("go", "mod", "download", "-json") |
| 404 | var out bytes.Buffer |
| 405 | cmd.Stdout = &out |
| 406 | cmd.Stderr = w |
| 407 | err := cmd.Run() |
| 408 | if ee := (*exec.ExitError)(nil); errors.As(err, &ee) && ee.ExitCode() == 1 { |
| 409 | // Exit code 1 from this command means there were some |
| 410 | // non-empty Error values in the output. Print them to w. |
| 411 | fmt.Fprintf(w, "documentation for some packages is not shown:\n") |
| 412 | for dec := json.NewDecoder(&out); ; { |
| 413 | var m struct { |
| 414 | Path string // Module path. |
| 415 | Version string // Module version. |
| 416 | Error string // Error loading module. |
| 417 | } |
| 418 | err := dec.Decode(&m) |
| 419 | if err == io.EOF { |
| 420 | break |
| 421 | } else if err != nil { |
| 422 | fmt.Fprintf(w, "error decoding JSON object from go mod download -json: %v\n", err) |
| 423 | continue |
| 424 | } |
| 425 | if m.Error == "" { |
| 426 | continue |
| 427 | } |
| 428 | fmt.Fprintf(w, "\tmodule %s@%s is not in the module cache and there was a problem downloading it: %s\n", m.Path, m.Version, m.Error) |
| 429 | } |
| 430 | } else if err != nil { |
| 431 | fmt.Fprintf(w, "there was a problem filling module cache: %v\n", err) |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | type mod struct { |
| 436 | Path string // Module path. |
| 437 | Dir string // Directory holding files for this module, if any. |
| 438 | } |
| 439 | |
| 440 | // buildList determines the build list in the current directory |
| 441 | // by invoking the go command. It should only be used in module mode, |
| 442 | // when vendor mode isn't on. |
| 443 | // |
| 444 | // See https://golang.org/cmd/go/#hdr-The_main_module_and_the_build_list. |
| 445 | func buildList(goMod string) ([]mod, error) { |
| 446 | if goMod == os.DevNull { |
| 447 | // Empty build list. |
| 448 | return nil, nil |
| 449 | } |
| 450 | |
| 451 | out, err := exec.Command("go", "list", "-m", "-json", "all").Output() |
| 452 | if ee := (*exec.ExitError)(nil); errors.As(err, &ee) { |
| 453 | return nil, fmt.Errorf("go command exited unsuccessfully: %v\n%s", ee.ProcessState.String(), ee.Stderr) |
| 454 | } else if err != nil { |
| 455 | return nil, err |
| 456 | } |
| 457 | var mods []mod |
| 458 | for dec := json.NewDecoder(bytes.NewReader(out)); ; { |
| 459 | var m mod |
| 460 | err := dec.Decode(&m) |
| 461 | if err == io.EOF { |
| 462 | break |
| 463 | } else if err != nil { |
| 464 | return nil, err |
| 465 | } |
| 466 | mods = append(mods, m) |
| 467 | } |
| 468 | return mods, nil |
| 469 | } |
| 470 | |
| 471 | // moduleFS is a vfs.FileSystem wrapper used when godoc is running |
| 472 | // in module mode. It's needed so that packages inside modules are |
| 473 | // considered to be third party. |
| 474 | // |
| 475 | // It overrides the RootType method of the underlying filesystem |
| 476 | // and implements it using a heuristic based on the import path. |
| 477 | // If the first element of the import path does not contain a dot, |
| 478 | // that package is considered to be inside GOROOT. If it contains |
| 479 | // a dot, then that package is considered to be third party. |
| 480 | // |
| 481 | // TODO(dmitshur): The RootType abstraction works well when GOPATH |
| 482 | // workspaces are bound at their roots, but scales poorly in the |
| 483 | // general case. It should be replaced by a more direct solution |
| 484 | // for determining whether a package is third party or not. |
| 485 | type moduleFS struct{ vfs.FileSystem } |
| 486 | |
| 487 | func (moduleFS) RootType(path string) vfs.RootType { |
| 488 | if !strings.HasPrefix(path, "/src/") { |
| 489 | return "" |
| 490 | } |
| 491 | domain := path[len("/src/"):] |
| 492 | if i := strings.Index(domain, "/"); i >= 0 { |
| 493 | domain = domain[:i] |
| 494 | } |
| 495 | if !strings.Contains(domain, ".") { |
| 496 | // No dot in the first element of import path |
| 497 | // suggests this is a package in GOROOT. |
| 498 | return vfs.RootTypeGoRoot |
| 499 | } else { |
| 500 | // A dot in the first element of import path |
| 501 | // suggests this is a third party package. |
| 502 | return vfs.RootTypeGoPath |
| 503 | } |
| 504 | } |
| 505 | func (fs moduleFS) String() string { return "module(" + fs.FileSystem.String() + ")" } |
| 506 |
Members