1
0
mirror of https://github.com/golang/go synced 2024-11-22 02:04:40 -07:00

godoc: better support for directory trees for user-defined

file systems provided via -path

R=rsc
CC=golang-dev
https://golang.org/cl/2182041
This commit is contained in:
Robert Griesemer 2010-09-14 11:16:36 -07:00
parent d64a2bddf0
commit ec81b1259b
7 changed files with 347 additions and 99 deletions

View File

@ -14,5 +14,6 @@ GOFILES=\
mapping.go\ mapping.go\
snippet.go\ snippet.go\
spec.go\ spec.go\
utils.go\
include ../../Make.cmd include ../../Make.cmd

View File

@ -7,6 +7,7 @@
package main package main
import ( import (
"bytes"
"go/doc" "go/doc"
"go/parser" "go/parser"
"io/ioutil" "io/ioutil"
@ -80,8 +81,18 @@ func firstSentence(s string) string {
} }
func newDirTree(path, name string, depth, maxDepth int) *Directory { type treeBuilder struct {
if depth >= maxDepth { pathFilter func(string) bool
maxDepth int
}
func (b *treeBuilder) newDirTree(path, name string, depth int) *Directory {
if b.pathFilter != nil && !b.pathFilter(path) {
return nil
}
if depth >= b.maxDepth {
// return a dummy directory so that the parent directory // return a dummy directory so that the parent directory
// doesn't get discarded just because we reached the max // doesn't get discarded just because we reached the max
// directory depth // directory depth
@ -132,7 +143,7 @@ func newDirTree(path, name string, depth, maxDepth int) *Directory {
i := 0 i := 0
for _, d := range list { for _, d := range list {
if isPkgDir(d) { if isPkgDir(d) {
dd := newDirTree(pathutil.Join(path, d.Name), d.Name, depth+1, maxDepth) dd := b.newDirTree(pathutil.Join(path, d.Name), d.Name, depth+1)
if dd != nil { if dd != nil {
dirs[i] = dd dirs[i] = dd
i++ i++
@ -160,20 +171,41 @@ func newDirTree(path, name string, depth, maxDepth int) *Directory {
} }
// Maximum directory depth, adjust as needed.
const maxDirDepth = 24
// newDirectory creates a new package directory tree with at most maxDepth // newDirectory creates a new package directory tree with at most maxDepth
// levels, anchored at root. The result tree is pruned such that it only // levels, anchored at root. The result tree is pruned such that it only
// contains directories that contain package files or that contain // contains directories that contain package files or that contain
// subdirectories containing package files (transitively). If maxDepth is // subdirectories containing package files (transitively). If a non-nil
// pathFilter is provided, directory paths additionally must be accepted
// by the filter (i.e., pathFilter(path) must be true). If maxDepth is
// too shallow, the leaf nodes are assumed to contain package files even if // too shallow, the leaf nodes are assumed to contain package files even if
// their contents are not known (i.e., in this case the tree may contain // their contents are not known (i.e., in this case the tree may contain
// directories w/o any package files). // directories w/o any package files).
// //
func newDirectory(root string, maxDepth int) *Directory { func newDirectory(root string, pathFilter func(string) bool, maxDepth int) *Directory {
d, err := os.Lstat(root) d, err := os.Lstat(root)
if err != nil || !isPkgDir(d) { if err != nil || !isPkgDir(d) {
return nil return nil
} }
return newDirTree(root, d.Name, 0, maxDepth) b := treeBuilder{pathFilter, maxDepth}
return b.newDirTree(root, d.Name, 0)
}
func (dir *Directory) writeLeafs(buf *bytes.Buffer) {
if dir != nil {
if len(dir.Dirs) == 0 {
buf.WriteString(dir.Path)
buf.WriteByte('\n')
return
}
for _, d := range dir.Dirs {
d.writeLeafs(buf)
}
}
} }

View File

@ -61,6 +61,10 @@ The flags are:
repository holding the source files. repository holding the source files.
-sync_minutes=0 -sync_minutes=0
sync interval in minutes; sync is disabled if <= 0 sync interval in minutes; sync is disabled if <= 0
-filter=""
file containing permitted permitted directory paths
-filter_minutes=0
filter update interval in minutes; update is disabled if <= 0
The -path flag accepts a list of colon-separated paths; unrooted paths are relative The -path flag accepts a list of colon-separated paths; unrooted paths are relative
to the current working directory. Each path is considered as an additional root for to the current working directory. Each path is considered as an additional root for
@ -76,6 +80,13 @@ as follows:
/home/bar/x -> bar/x /home/bar/x -> bar/x
/public/x -> public/x /public/x -> public/x
Paths provided via -path may point to very large file systems that contain
non-Go files. Creating the subtree of directories with Go packages may take
a long amount of time. A file containing newline-separated directory paths
may be provided with the -filter flag; if it exists, only directories
on those paths are considered. If -filter_minutes is set, the filter_file is
updated regularly by walking the entire directory tree.
When godoc runs as a web server, it creates a search index from all .go files When godoc runs as a web server, it creates a search index from all .go files
under -goroot (excluding files starting with .). The index is created at startup under -goroot (excluding files starting with .). The index is created at startup
and is automatically updated every time the -sync command terminates with exit and is automatically updated every time the -sync command terminates with exit

View File

@ -22,40 +22,12 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"sync"
"template" "template"
"time" "time"
"utf8" "utf8"
) )
// ----------------------------------------------------------------------------
// Support types
// An RWValue wraps a value and permits mutually exclusive
// access to it and records the time the value was last set.
type RWValue struct {
mutex sync.RWMutex
value interface{}
timestamp int64 // time of last set(), in seconds since epoch
}
func (v *RWValue) set(value interface{}) {
v.mutex.Lock()
v.value = value
v.timestamp = time.Seconds()
v.mutex.Unlock()
}
func (v *RWValue) get() (interface{}, int64) {
v.mutex.RLock()
defer v.mutex.RUnlock()
return v.value, v.timestamp
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Globals // Globals
@ -79,15 +51,19 @@ var (
verbose = flag.Bool("v", false, "verbose mode") verbose = flag.Bool("v", false, "verbose mode")
// file system roots // file system roots
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
path = flag.String("path", "", "additional package directories (colon-separated)") path = flag.String("path", "", "additional package directories (colon-separated)")
filter = flag.String("filter", "godoc.dirlist", "file containing permitted package directory paths")
filterMin = flag.Int("filter_minutes", 0, "filter update interval in minutes; disabled if <= 0")
filterDelay delayTime // actual filter update interval in minutes; usually filterDelay == filterMin, but filterDelay may back off exponentially
// layout control // layout control
tabwidth = flag.Int("tabwidth", 4, "tab width") tabwidth = flag.Int("tabwidth", 4, "tab width")
// file system mapping // file system mapping
fsMap Mapping // user-defined mapping fsMap Mapping // user-defined mapping
fsTree RWValue // *Directory tree of packages, updated with each sync fsTree RWValue // *Directory tree of packages, updated with each sync
pathFilter RWValue // filter used when building fsMap directory trees
// http handlers // http handlers
fileServer http.Handler // default file server fileServer http.Handler // default file server
@ -113,6 +89,134 @@ func registerPublicHandlers(mux *http.ServeMux) {
} }
// ----------------------------------------------------------------------------
// Directory filters
// isParentOf returns true if p is a parent of (or the same as) q
// where p and q are directory paths.
func isParentOf(p, q string) bool {
n := len(p)
return strings.HasPrefix(q, p) && (len(q) <= n || q[n] == '/')
}
// isRelated returns true if p is a parent or child of (or the same as) q
// where p and q are directory paths.
func isRelated(p, q string) bool {
return isParentOf(p, q) || isParentOf(q, p)
}
func setPathFilter(list []string) {
if len(list) == 0 {
pathFilter.set(nil)
return
}
// TODO(gri) This leads to quadratic behavior.
// Need to find a better filter solution.
pathFilter.set(func(path string) bool {
for _, p := range list {
if isRelated(path, p) {
return true
}
}
return false
})
}
func getPathFilter() func(string) bool {
f, _ := pathFilter.get()
if f != nil {
return f.(func(string) bool)
}
return nil
}
// readDirList reads a file containing newline-separated list
// of directory paths and returns the list of paths.
func readDirList(filename string) ([]string, os.Error) {
contents, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
// create list of valid directory names
filter := func(path string) bool {
d, err := os.Lstat(path)
return err == nil && isPkgDir(d)
}
return canonicalizePaths(strings.Split(string(contents), "\n", -1), filter), nil
}
func updateFilterFile() {
// for each user-defined file system mapping, compute
// respective directory tree w/o filter for accuracy
fsMap.Iterate(func(path string, value *RWValue) bool {
value.set(newDirectory(path, nil, maxDirDepth))
return true
})
// collect directory tree leaf node paths
var buf bytes.Buffer
fsMap.Iterate(func(_ string, value *RWValue) bool {
v, _ := value.get()
if v != nil && v.(*Directory) != nil {
v.(*Directory).writeLeafs(&buf)
}
return true
})
// update filter file
// TODO(gri) should write a tmp file and atomically rename
err := ioutil.WriteFile(*filter, buf.Bytes(), 0666)
if err != nil {
log.Stderrf("ioutil.Writefile(%s): %s", *filter, err)
filterDelay.backoff(24 * 60) // back off exponentially, but try at least once a day
} else {
filterDelay.set(*filterMin) // revert to regular filter update schedule
}
}
func initDirTrees() {
// setup initial path filter
if *filter != "" {
list, err := readDirList(*filter)
if err != nil {
log.Stderrf("%s", err)
} else if len(list) == 0 {
log.Stderrf("no directory paths in file %s", *filter)
}
setPathFilter(list)
}
// for each user-defined file system mapping, compute
// respective directory tree quickly using pathFilter
go fsMap.Iterate(func(path string, value *RWValue) bool {
value.set(newDirectory(path, getPathFilter(), maxDirDepth))
return true
})
// start filter update goroutine, if enabled.
if *filter != "" && *filterMin > 0 {
filterDelay.set(*filterMin) // initial filter update delay
go func() {
for {
updateFilterFile()
delay, _ := syncDelay.get()
if *verbose {
log.Stderrf("next filter update in %dmin", delay.(int))
}
time.Sleep(int64(delay.(int)) * 60e9)
}
}()
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Path mapping // Path mapping
@ -1073,14 +1177,35 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf
// directory tree is present; lookup respective directory // directory tree is present; lookup respective directory
// (may still fail if the file system was updated and the // (may still fail if the file system was updated and the
// new directory tree has not yet been computed) // new directory tree has not yet been computed)
// TODO(gri) Need to build directory tree for fsMap entries
dir = tree.(*Directory).lookup(abspath) dir = tree.(*Directory).lookup(abspath)
} }
if dir == nil {
// the path may refer to a user-specified file system mapped
// via fsMap; lookup that mapping and corresponding RWValue
// if any
var v *RWValue
fsMap.Iterate(func(path string, value *RWValue) bool {
if isParentOf(path, abspath) {
// mapping found
v = value
return false
}
return true
})
if v != nil {
// found a RWValue associated with a user-specified file
// system; a non-nil RWValue stores a (possibly out-of-date)
// directory tree for that file system
if tree, _ := v.get(); tree != nil && tree.(*Directory) != nil {
dir = tree.(*Directory).lookup(abspath)
}
}
}
if dir == nil { if dir == nil {
// no directory tree present (either early after startup // no directory tree present (either early after startup
// or command-line mode, or we don't build a tree for the // or command-line mode, or we don't have a tree for the
// directory; e.g. google3); compute one level for this page // directory yet; e.g. google3); compute one level for this page
dir = newDirectory(abspath, 1) dir = newDirectory(abspath, getPathFilter(), 1)
} }
return PageInfo{abspath, plist, past, pdoc, dir.listing(true), h.isPkg, nil} return PageInfo{abspath, plist, past, pdoc, dir.listing(true), h.isPkg, nil}

View File

@ -49,7 +49,7 @@ var (
// periodic sync // periodic sync
syncCmd = flag.String("sync", "", "sync command; disabled if empty") syncCmd = flag.String("sync", "", "sync command; disabled if empty")
syncMin = flag.Int("sync_minutes", 0, "sync interval in minutes; disabled if <= 0") syncMin = flag.Int("sync_minutes", 0, "sync interval in minutes; disabled if <= 0")
syncDelay delayTime // actual sync delay in minutes; usually syncDelay == syncMin, but delay may back off exponentially syncDelay delayTime // actual sync interval in minutes; usually syncDelay == syncMin, but syncDelay may back off exponentially
// network // network
httpAddr = flag.String("http", "", "HTTP service address (e.g., '"+defaultAddr+"')") httpAddr = flag.String("http", "", "HTTP service address (e.g., '"+defaultAddr+"')")
@ -118,9 +118,6 @@ func exec(c *http.Conn, args []string) (status int) {
} }
// Maximum directory depth, adjust as needed.
const maxDirDepth = 24
func dosync(c *http.Conn, r *http.Request) { func dosync(c *http.Conn, r *http.Request) {
args := []string{"/bin/sh", "-c", *syncCmd} args := []string{"/bin/sh", "-c", *syncCmd}
switch exec(c, args) { switch exec(c, args) {
@ -130,7 +127,7 @@ func dosync(c *http.Conn, r *http.Request) {
// TODO(gri): The directory tree may be temporarily out-of-sync. // TODO(gri): The directory tree may be temporarily out-of-sync.
// Consider keeping separate time stamps so the web- // Consider keeping separate time stamps so the web-
// page can indicate this discrepancy. // page can indicate this discrepancy.
fsTree.set(newDirectory(*goroot, maxDirDepth)) fsTree.set(newDirectory(*goroot, nil, maxDirDepth))
fallthrough fallthrough
case 1: case 1:
// sync failed because no files changed; // sync failed because no files changed;
@ -257,12 +254,15 @@ func main() {
http.Handle("/debug/sync", http.HandlerFunc(dosync)) http.Handle("/debug/sync", http.HandlerFunc(dosync))
} }
// Initialize directory tree with corresponding timestamp. // Initialize default directory tree with corresponding timestamp.
// Do it in two steps: // Do it in two steps:
// 1) set timestamp right away so that the indexer is kicked on // 1) set timestamp right away so that the indexer is kicked on
fsTree.set(nil) fsTree.set(nil)
// 2) compute initial directory tree in a goroutine so that launch is quick // 2) compute initial directory tree in a goroutine so that launch is quick
go func() { fsTree.set(newDirectory(*goroot, maxDirDepth)) }() go func() { fsTree.set(newDirectory(*goroot, nil, maxDirDepth)) }()
// Initialize directory trees for user-defined file systems (-path flag).
initDirTrees()
// Start sync goroutine, if enabled. // Start sync goroutine, if enabled.
if *syncCmd != "" && *syncMin > 0 { if *syncCmd != "" && *syncMin > 0 {

View File

@ -11,6 +11,7 @@ import (
"io" "io"
"os" "os"
pathutil "path" pathutil "path"
"sort"
"strings" "strings"
) )
@ -42,14 +43,19 @@ import (
// //
// (assuming that file exists). // (assuming that file exists).
// //
// Each individual mapping also has a RWValue associated with it that
// may be used to store mapping-specific information. See the Iterate
// method.
//
type Mapping struct { type Mapping struct {
list []mapping list []mapping
prefixes []string prefixes []string // lazily computed from list
} }
type mapping struct { type mapping struct {
prefix, path string prefix, path string
value *RWValue
} }
@ -75,43 +81,16 @@ type mapping struct {
// public -> /home/build/public // public -> /home/build/public
// //
func (m *Mapping) Init(paths string) { func (m *Mapping) Init(paths string) {
cwd, _ := os.Getwd() // ignore errors pathlist := canonicalizePaths(strings.Split(paths, ":", -1), nil)
pathlist := strings.Split(paths, ":", -1)
list := make([]mapping, len(pathlist)) list := make([]mapping, len(pathlist))
n := 0 // number of mappings
for _, path := range pathlist { // create mapping list
if len(path) == 0 { for i, path := range pathlist {
// ignore empty paths (don't assume ".") _, prefix := pathutil.Split(path)
continue list[i] = mapping{prefix, path, new(RWValue)}
}
// len(path) > 0: normalize path
if path[0] != '/' {
path = pathutil.Join(cwd, path)
} else {
path = pathutil.Clean(path)
}
// check if mapping exists already
var i int
for i = 0; i < n; i++ {
if path == list[i].path {
break
}
}
// add mapping if it is new
if i >= n {
_, prefix := pathutil.Split(path)
list[n] = mapping{prefix, path}
n++
}
} }
m.list = list[0:n] m.list = list
} }
@ -134,24 +113,25 @@ func (m *Mapping) PrefixList() []string {
// compute the list lazily // compute the list lazily
if m.prefixes == nil { if m.prefixes == nil {
list := make([]string, len(m.list)) list := make([]string, len(m.list))
n := 0 // nuber of prefixes
for _, e := range m.list { // populate list
// check if prefix exists already for i, e := range m.list {
var i int list[i] = e.prefix
for i = 0; i < n; i++ { }
if e.prefix == list[i] {
break
}
}
// add prefix if it is new // sort the list and remove duplicate entries
if i >= n { sort.SortStrings(list)
list[n] = e.prefix i := 0
n++ prev := ""
for _, path := range list {
if path != prev {
list[i] = path
i++
prev = path
} }
} }
m.prefixes = list[0:n]
m.prefixes = list[0:i]
} }
return m.prefixes return m.prefixes
@ -166,7 +146,7 @@ func (m *Mapping) Fprint(w io.Writer) {
} }
func split(path string) (head, tail string) { func splitFirst(path string) (head, tail string) {
i := strings.Index(path, "/") i := strings.Index(path, "/")
if i > 0 { if i > 0 {
// 0 < i < len(path) // 0 < i < len(path)
@ -181,7 +161,7 @@ func split(path string) (head, tail string) {
// string is returned. // string is returned.
// //
func (m *Mapping) ToAbsolute(path string) string { func (m *Mapping) ToAbsolute(path string) string {
prefix, tail := split(path) prefix, tail := splitFirst(path)
for _, e := range m.list { for _, e := range m.list {
switch { switch {
case e.prefix == prefix: case e.prefix == prefix:
@ -214,3 +194,15 @@ func (m *Mapping) ToRelative(path string) string {
} }
return "" // no match return "" // no match
} }
// Iterate calls f for each path and RWValue in the mapping (in uspecified order)
// until f returns false.
//
func (m *Mapping) Iterate(f func(path string, value *RWValue) bool) {
for _, e := range m.list {
if !f(e.path, e.value) {
return
}
}
}

87
src/cmd/godoc/utils.go Normal file
View File

@ -0,0 +1,87 @@
// Copyright 2010 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.
// This file contains support functionality for godoc.
package main
import (
"os"
pathutil "path"
"sort"
"strings"
"sync"
"time"
)
// An RWValue wraps a value and permits mutually exclusive
// access to it and records the time the value was last set.
type RWValue struct {
mutex sync.RWMutex
value interface{}
timestamp int64 // time of last set(), in seconds since epoch
}
func (v *RWValue) set(value interface{}) {
v.mutex.Lock()
v.value = value
v.timestamp = time.Seconds()
v.mutex.Unlock()
}
func (v *RWValue) get() (interface{}, int64) {
v.mutex.RLock()
defer v.mutex.RUnlock()
return v.value, v.timestamp
}
var cwd, _ = os.Getwd() // ignore errors
// canonicalizePaths takes a list of (directory/file) paths and returns
// the list of corresponding absolute paths in sorted (increasing) order.
// Relative paths are assumed to be relative to the current directory,
// empty and duplicate paths as well as paths for which filter(path) is
// false are discarded. filter may be nil in which case it is not used.
//
func canonicalizePaths(list []string, filter func(path string) bool) []string {
i := 0
for _, path := range list {
path = strings.TrimSpace(path)
if len(path) == 0 {
continue // ignore empty paths (don't assume ".")
}
// len(path) > 0: normalize path
if path[0] != '/' {
path = pathutil.Join(cwd, path)
} else {
path = pathutil.Clean(path)
}
// we have a non-empty absolute path
if filter != nil && !filter(path) {
continue
}
// keep the path
list[i] = path
i++
}
list = list[0:i]
// sort the list and remove duplicate entries
sort.SortStrings(list)
i = 0
prev := ""
for _, path := range list {
if path != prev {
list[i] = path
i++
prev = path
}
}
return list[0:i]
}