| // 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, tutorial, etc. |
| // http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed |
| // http://godoc/cmd/ serve documentation about commands (TODO) |
| // 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 |
| |
| |
| package main |
| |
| import ( |
| "bytes"; |
| "container/vector"; |
| "flag"; |
| "fmt"; |
| "go/ast"; |
| "go/doc"; |
| "go/parser"; |
| "go/printer"; |
| "go/token"; |
| "http"; |
| "io"; |
| "log"; |
| "net"; |
| "os"; |
| pathutil "path"; |
| "sort"; |
| "strings"; |
| "sync"; |
| "syscall"; |
| "tabwriter"; |
| "template"; |
| "time"; |
| ) |
| |
| |
| const Pkg = "/pkg/" // name for auto-generated package documentation tree |
| |
| |
| type timeStamp struct { |
| mutex sync.RWMutex; |
| seconds int64; |
| } |
| |
| |
| func (ts *timeStamp) set() { |
| ts.mutex.Lock(); |
| ts.seconds = time.Seconds(); |
| ts.mutex.Unlock(); |
| } |
| |
| |
| func (ts *timeStamp) get() int64 { |
| ts.mutex.RLock(); |
| defer ts.mutex.RUnlock(); |
| return ts.seconds; |
| } |
| |
| |
| var ( |
| verbose = flag.Bool("v", false, "verbose mode"); |
| |
| // file system roots |
| goroot string; |
| pkgroot = flag.String("pkgroot", "src/pkg", "root package source directory (if unrooted, relative to goroot)"); |
| tmplroot = flag.String("tmplroot", "lib/godoc", "root template directory (if unrooted, relative to goroot)"); |
| |
| // periodic sync |
| syncCmd = flag.String("sync", "", "sync command; disabled if empty"); |
| syncMin = flag.Int("sync_minutes", 0, "sync interval in minutes; disabled if <= 0"); |
| syncTime timeStamp; // time of last p4 sync |
| |
| // layout control |
| tabwidth = flag.Int("tabwidth", 4, "tab width"); |
| html = flag.Bool("html", false, "print HTML in command-line mode"); |
| |
| // server control |
| httpaddr = flag.String("http", "", "HTTP service address (e.g., ':6060')"); |
| ) |
| |
| |
| func init() { |
| goroot = os.Getenv("GOROOT"); |
| if goroot != "" { |
| goroot = "/home/r/go-release/go"; |
| } |
| flag.StringVar(&goroot, "goroot", goroot, "Go root directory"); |
| syncTime.set(); // have a reasonable initial value |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Support |
| |
| func isDir(name string) bool { |
| d, err := os.Stat(name); |
| return err == nil && d.IsDirectory(); |
| } |
| |
| |
| func isGoFile(dir *os.Dir) bool { |
| return dir.IsRegular() && pathutil.Ext(dir.Name) == ".go"; |
| } |
| |
| |
| func isPkgDir(dir *os.Dir) bool { |
| return dir.IsDirectory() && dir.Name != "_obj"; |
| } |
| |
| |
| func makeTabwriter(writer io.Writer) *tabwriter.Writer { |
| return tabwriter.NewWriter(writer, *tabwidth, 1, byte(' '), 0); |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Parsing |
| |
| // A single error in the parsed file. |
| type parseError struct { |
| src []byte; // source before error |
| line int; // line number of error |
| msg string; // error message |
| } |
| |
| |
| // All the errors in the parsed file, plus surrounding source code. |
| // Each error has a slice giving the source text preceding it |
| // (starting where the last error occurred). The final element in list[] |
| // has msg = "", to give the remainder of the source code. |
| // This data structure is handed to the templates parseerror.txt and parseerror.html. |
| // |
| type parseErrors struct { |
| filename string; // path to file |
| list []parseError; // the errors |
| src []byte; // the file's entire source code |
| } |
| |
| |
| // Parses a file (path) and returns the corresponding AST and |
| // a sorted list (by file position) of errors, if any. |
| // |
| func parse(path string, mode uint) (*ast.Program, *parseErrors) { |
| src, err := io.ReadFile(path); |
| if err != nil { |
| log.Stderrf("ReadFile %s: %v", path, err); |
| errs := []parseError{parseError{nil, 0, err.String()}}; |
| return nil, &parseErrors{path, errs, nil}; |
| } |
| |
| prog, err := parser.Parse(src, mode); |
| if err != nil { |
| // sort and convert error list |
| if errors, ok := err.(parser.ErrorList); ok { |
| sort.Sort(errors); |
| errs := make([]parseError, len(errors) + 1); // +1 for final fragment of source |
| offs := 0; |
| for i, r := range errors { |
| // Should always be true, but check for robustness. |
| if 0 <= r.Pos.Offset && r.Pos.Offset <= len(src) { |
| errs[i].src = src[offs : r.Pos.Offset]; |
| offs = r.Pos.Offset; |
| } |
| errs[i].line = r.Pos.Line; |
| errs[i].msg = r.Msg; |
| } |
| errs[len(errors)].src = src[offs : len(src)]; |
| return nil, &parseErrors{path, errs, src}; |
| } else { |
| // TODO should have some default handling here to be more robust |
| panic("unreachable"); |
| } |
| } |
| |
| return prog, nil; |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Templates |
| |
| // Return text for an AST node. |
| func nodeText(node interface{}, mode uint) []byte { |
| var buf bytes.Buffer; |
| tw := makeTabwriter(&buf); |
| printer.Fprint(tw, node, mode); |
| tw.Flush(); |
| return buf.Data(); |
| } |
| |
| |
| // Convert x, whatever it is, to text form. |
| func toText(x interface{}) []byte { |
| type String interface { String() string } |
| |
| switch v := x.(type) { |
| case []byte: |
| return v; |
| case string: |
| return strings.Bytes(v); |
| case String: |
| return strings.Bytes(v.String()); |
| case ast.Decl: |
| return nodeText(v, printer.ExportsOnly); |
| case ast.Expr: |
| return nodeText(v, printer.ExportsOnly); |
| } |
| var buf bytes.Buffer; |
| fmt.Fprint(&buf, x); |
| return buf.Data(); |
| } |
| |
| |
| // Template formatter for "html" format. |
| func htmlFmt(w io.Writer, x interface{}, format string) { |
| template.HtmlEscape(w, toText(x)); |
| } |
| |
| |
| // Template formatter for "html-comment" format. |
| func htmlCommentFmt(w io.Writer, x interface{}, format string) { |
| doc.ToHtml(w, toText(x)); |
| } |
| |
| |
| // Template formatter for "" (default) format. |
| func textFmt(w io.Writer, x interface{}, format string) { |
| w.Write(toText(x)); |
| } |
| |
| |
| var fmap = template.FormatterMap{ |
| "": textFmt, |
| "html": htmlFmt, |
| "html-comment": htmlCommentFmt, |
| } |
| |
| |
| func readTemplate(name string) *template.Template { |
| path := pathutil.Join(*tmplroot, name); |
| data, err := io.ReadFile(path); |
| if err != nil { |
| log.Exitf("ReadFile %s: %v", path, err); |
| } |
| t, err1 := template.Parse(string(data), fmap); |
| if err1 != nil { |
| log.Exitf("%s: %v", name, err); |
| } |
| return t; |
| } |
| |
| |
| var godocHtml *template.Template |
| var packageHtml *template.Template |
| var packageText *template.Template |
| var parseerrorHtml *template.Template; |
| var parseerrorText *template.Template; |
| |
| func readTemplates() { |
| // have to delay until after flags processing, |
| // so that main has chdir'ed to goroot. |
| godocHtml = readTemplate("godoc.html"); |
| packageHtml = readTemplate("package.html"); |
| packageText = readTemplate("package.txt"); |
| parseerrorHtml = readTemplate("parseerror.html"); |
| parseerrorText = readTemplate("parseerror.txt"); |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Generic HTML wrapper |
| |
| func servePage(c *http.Conn, title, content interface{}) { |
| type Data struct { |
| title interface{}; |
| header interface{}; |
| timestamp string; |
| content interface{}; |
| } |
| |
| var d Data; |
| d.title = title; |
| d.header = title; |
| d.timestamp = time.SecondsToLocalTime(syncTime.get()).String(); |
| d.content = content; |
| godocHtml.Execute(&d, c); |
| } |
| |
| |
| func serveText(c *http.Conn, text []byte) { |
| c.SetHeader("content-type", "text/plain; charset=utf-8"); |
| c.Write(text); |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Files |
| |
| func serveParseErrors(c *http.Conn, errors *parseErrors) { |
| // format errors |
| var buf bytes.Buffer; |
| parseerrorHtml.Execute(errors, &buf); |
| servePage(c, errors.filename + " - Parse Errors", buf.Data()); |
| } |
| |
| |
| func serveGoSource(c *http.Conn, name string) { |
| prog, errors := parse(name, parser.ParseComments); |
| if errors != nil { |
| serveParseErrors(c, errors); |
| return; |
| } |
| |
| var buf bytes.Buffer; |
| fmt.Fprintln(&buf, "<pre>"); |
| template.HtmlEscape(&buf, nodeText(prog, printer.DocComments)); |
| fmt.Fprintln(&buf, "</pre>"); |
| |
| servePage(c, name + " - Go source", buf.Data()); |
| } |
| |
| |
| var fileServer = http.FileServer(".", ""); |
| |
| func serveFile(c *http.Conn, req *http.Request) { |
| // pick off special cases and hand the rest to the standard file server |
| switch { |
| case req.Url.Path == "/": |
| // serve landing page. |
| // TODO: hide page from ordinary file serving. |
| // writing doc/index.html will take care of that. |
| http.ServeFile(c, req, "doc/root.html"); |
| |
| case req.Url.Path == "/doc/root.html": |
| // hide landing page from its real name |
| // TODO why - there is no reason for this (remove eventually) |
| http.NotFound(c, req); |
| |
| case pathutil.Ext(req.Url.Path) == ".go": |
| serveGoSource(c, req.Url.Path[1 : len(req.Url.Path)]); // strip leading '/' from name |
| |
| default: |
| // TODO not good enough - don't want to download files |
| // want to see them |
| fileServer.ServeHTTP(c, req); |
| } |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Packages |
| |
| type pakDesc struct { |
| dirname string; // relative to goroot |
| pakname string; // same as last component of importpath |
| importpath string; // import "___" |
| filenames map[string] bool; // set of file (names) belonging to this package |
| } |
| |
| |
| // TODO if we don't plan to use the directory information, simplify to []string |
| type dirList []*os.Dir |
| |
| func (d dirList) Len() int { return len(d) } |
| func (d dirList) Less(i, j int) bool { return d[i].Name < d[j].Name } |
| func (d dirList) Swap(i, j int) { d[i], d[j] = d[j], d[i] } |
| |
| |
| func isPackageFile(dirname, filename, pakname string) bool { |
| // ignore test files |
| if strings.HasSuffix(filename, "_test.go") { |
| return false; |
| } |
| |
| // determine package name |
| prog, errors := parse(dirname + "/" + filename, parser.PackageClauseOnly); |
| if prog == nil { |
| return false; |
| } |
| |
| return prog != nil && prog.Name.Value == pakname; |
| } |
| |
| |
| // Returns the canonical URL path, the package denoted by path, and |
| // the list of sub-directories in the corresponding package directory. |
| // If there is no such package, the package descriptor pd is nil. |
| // If there are no sub-directories, the dirs list is nil. |
| func findPackage(path string) (canonical string, pd *pakDesc, dirs dirList) { |
| canonical = pathutil.Clean(Pkg + path) + "/"; |
| |
| // get directory contents, if possible |
| importpath := pathutil.Clean(path); // no trailing '/' |
| dirname := pathutil.Join(*pkgroot, importpath); |
| if !isDir(dirname) { |
| return; |
| } |
| |
| fd, err1 := os.Open(dirname, os.O_RDONLY, 0); |
| if err1 != nil { |
| log.Stderrf("open %s: %v", dirname, err1); |
| return; |
| } |
| |
| list, err2 := fd.Readdir(-1); |
| if err2 != nil { |
| log.Stderrf("readdir %s: %v", dirname, err2); |
| return; |
| } |
| |
| // the package name is is the directory name within its parent |
| _, pakname := pathutil.Split(dirname); |
| |
| // collect all files belonging to the package and count the |
| // number of sub-directories |
| filenames := make(map[string]bool); |
| nsub := 0; |
| for i, entry := range list { |
| switch { |
| case isGoFile(&entry) && isPackageFile(dirname, entry.Name, pakname): |
| // add file to package desc |
| if tmp, found := filenames[entry.Name]; found { |
| panic("internal error: same file added more than once: " + entry.Name); |
| } |
| filenames[entry.Name] = true; |
| case isPkgDir(&entry): |
| nsub++; |
| } |
| } |
| |
| // make the list of sub-directories, if any |
| var subdirs dirList; |
| if nsub > 0 { |
| subdirs = make(dirList, nsub); |
| nsub = 0; |
| for i, entry := range list { |
| if isPkgDir(&entry) { |
| // make a copy here so sorting (and other code) doesn't |
| // have to make one every time an entry is moved |
| copy := new(os.Dir); |
| *copy = entry; |
| subdirs[nsub] = copy; |
| nsub++; |
| } |
| } |
| sort.Sort(subdirs); |
| } |
| |
| // if there are no package files, then there is no package |
| if len(filenames) == 0 { |
| return canonical, nil, subdirs; |
| } |
| |
| return canonical, &pakDesc{dirname, pakname, importpath, filenames}, subdirs; |
| } |
| |
| |
| func (p *pakDesc) Doc() (*doc.PackageDoc, *parseErrors) { |
| if p == nil { |
| return nil, nil; |
| } |
| |
| // compute documentation |
| var r doc.DocReader; |
| i := 0; |
| for filename := range p.filenames { |
| prog, err := parse(p.dirname + "/" + filename, parser.ParseComments); |
| if err != nil { |
| return nil, err; |
| } |
| if i == 0 { |
| // first file - initialize doc |
| r.Init(prog.Name.Value, p.importpath); |
| } |
| i++; |
| r.AddProgram(prog); |
| } |
| |
| return r.Doc(), nil; |
| } |
| |
| |
| type PageInfo struct { |
| PDoc *doc.PackageDoc; |
| Dirs dirList; |
| } |
| |
| func servePkg(c *http.Conn, r *http.Request) { |
| path := r.Url.Path; |
| path = path[len(Pkg) : len(path)]; |
| canonical, desc, dirs := findPackage(path); |
| |
| if r.Url.Path != canonical { |
| http.Redirect(c, canonical, http.StatusMovedPermanently); |
| return; |
| } |
| |
| pdoc, errors := desc.Doc(); |
| if errors != nil { |
| serveParseErrors(c, errors); |
| return; |
| } |
| |
| var buf bytes.Buffer; |
| if false { // TODO req.Params["format"] == "text" |
| err := packageText.Execute(PageInfo{pdoc, dirs}, &buf); |
| if err != nil { |
| log.Stderrf("packageText.Execute: %s", err); |
| } |
| serveText(c, buf.Data()); |
| return; |
| } |
| |
| err := packageHtml.Execute(PageInfo{pdoc, dirs}, &buf); |
| if err != nil { |
| log.Stderrf("packageHtml.Execute: %s", err); |
| } |
| |
| if path == "" { |
| path = "."; // don't display an empty path |
| } |
| servePage(c, path + " - Go package documentation", buf.Data()); |
| } |
| |
| |
| // ---------------------------------------------------------------------------- |
| // Server |
| |
| func loggingHandler(h http.Handler) http.Handler { |
| return http.HandlerFunc(func(c *http.Conn, req *http.Request) { |
| log.Stderrf("%s\t%s", c.RemoteAddr, req.Url); |
| h.ServeHTTP(c, req); |
| }) |
| } |
| |
| |
| func exec(c *http.Conn, args []string) bool { |
| r, w, err := os.Pipe(); |
| if err != nil { |
| log.Stderrf("os.Pipe(): %v\n", err); |
| return false; |
| } |
| |
| bin := args[0]; |
| fds := []*os.File{nil, w, w}; |
| if *verbose { |
| log.Stderrf("executing %v", args); |
| } |
| pid, err := os.ForkExec(bin, args, os.Environ(), goroot, fds); |
| defer r.Close(); |
| w.Close(); |
| if err != nil { |
| log.Stderrf("os.ForkExec(%q): %v\n", bin, err); |
| return false; |
| } |
| |
| var buf bytes.Buffer; |
| io.Copy(r, &buf); |
| wait, err := os.Wait(pid, 0); |
| if err != nil { |
| os.Stderr.Write(buf.Data()); |
| log.Stderrf("os.Wait(%d, 0): %v\n", pid, err); |
| return false; |
| } |
| if !wait.Exited() || wait.ExitStatus() != 0 { |
| os.Stderr.Write(buf.Data()); |
| log.Stderrf("executing %v failed (exit status = %d)", args, wait.ExitStatus()); |
| return false; |
| } |
| |
| if *verbose { |
| os.Stderr.Write(buf.Data()); |
| } |
| if c != nil { |
| c.SetHeader("content-type", "text/plain; charset=utf-8"); |
| c.Write(buf.Data()); |
| } |
| |
| return true; |
| } |
| |
| |
| func sync(c *http.Conn, r *http.Request) { |
| args := []string{"/bin/sh", "-c", *syncCmd}; |
| if !exec(c, args) { |
| *syncMin = 0; // disable sync |
| return; |
| } |
| syncTime.set(); |
| } |
| |
| |
| func usage() { |
| fmt.Fprintf(os.Stderr, |
| "usage: godoc package [name ...]\n" |
| " godoc -http=:6060\n" |
| ); |
| flag.PrintDefaults(); |
| os.Exit(1); |
| } |
| |
| |
| func main() { |
| flag.Parse(); |
| |
| // Check usage first; get usage message out early. |
| switch { |
| case *httpaddr != "": |
| if flag.NArg() != 0 { |
| usage(); |
| } |
| default: |
| if flag.NArg() == 0 { |
| usage(); |
| } |
| } |
| |
| if err := os.Chdir(goroot); err != nil { |
| log.Exitf("chdir %s: %v", goroot, err); |
| } |
| |
| readTemplates(); |
| |
| if *httpaddr != "" { |
| var handler http.Handler = http.DefaultServeMux; |
| if *verbose { |
| log.Stderrf("Go Documentation Server\n"); |
| log.Stderrf("address = %s\n", *httpaddr); |
| log.Stderrf("goroot = %s\n", goroot); |
| log.Stderrf("pkgroot = %s\n", *pkgroot); |
| log.Stderrf("tmplroot = %s\n", *tmplroot); |
| handler = loggingHandler(handler); |
| } |
| |
| http.Handle(Pkg, http.HandlerFunc(servePkg)); |
| if *syncCmd != "" { |
| http.Handle("/debug/sync", http.HandlerFunc(sync)); |
| } |
| http.Handle("/", http.HandlerFunc(serveFile)); |
| |
| // The server may have been restarted; always wait 1sec to |
| // give the forking server a chance to shut down and release |
| // the http port. |
| time.Sleep(1e9); |
| |
| // Start sync goroutine, if enabled. |
| if *syncCmd != "" && *syncMin > 0 { |
| go func() { |
| if *verbose { |
| log.Stderrf("sync every %dmin", *syncMin); |
| } |
| for *syncMin > 0 { |
| sync(nil, nil); |
| time.Sleep(int64(*syncMin) * (60 * 1e9)); |
| } |
| if *verbose { |
| log.Stderrf("periodic sync stopped"); |
| } |
| }(); |
| } |
| |
| if err := http.ListenAndServe(*httpaddr, handler); err != nil { |
| log.Exitf("ListenAndServe %s: %v", *httpaddr, err) |
| } |
| return; |
| } |
| |
| if *html { |
| packageText = packageHtml; |
| parseerrorText = parseerrorHtml; |
| } |
| |
| _, desc, dirs := findPackage(flag.Arg(0)); |
| pdoc, errors := desc.Doc(); |
| if errors != nil { |
| err := parseerrorText.Execute(errors, os.Stderr); |
| if err != nil { |
| log.Stderrf("parseerrorText.Execute: %s", err); |
| } |
| os.Exit(1); |
| } |
| |
| if pdoc != nil && flag.NArg() > 1 { |
| args := flag.Args(); |
| pdoc.Filter(args[1 : len(args)]); |
| } |
| |
| packageText.Execute(PageInfo{pdoc, dirs}, os.Stdout); |
| } |