+++ /dev/null
-// 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 proto
-// godoc compress/zlib Cipher NewCMAC
-// - prints doc for Cipher and NewCMAC in package crypto/block
-
-
-package main
-
-import (
- "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", "usr/gri/pretty", "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() {
- var err os.Error;
- goroot, err = os.Getenv("GOROOT");
- if err != nil {
- 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 io.ByteBuffer;
- 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 io.StringBytes(v);
- case String:
- return io.StringBytes(v.String());
- case ast.Decl:
- return nodeText(v, printer.ExportsOnly);
- case ast.Expr:
- return nodeText(v, printer.ExportsOnly);
- }
- var buf io.ByteBuffer;
- 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 io.ByteBuffer;
- 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 io.ByteBuffer;
- 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 io.ByteBuffer;
- 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 io.ByteBuffer;
- 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);
-}