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