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