| // Copyright 2014 The Go AUTHORS. All rights reserved. |
| // Use of this source code is governed by the Apache 2.0 |
| // license that can be found in the LICENSE file. |
| |
| // Command tipgodoc is the beginning of the new tip.golang.org server, |
| // serving the latest HEAD straight from the Git oven. |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "net/http" |
| "net/http/httputil" |
| "net/url" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "sync" |
| "time" |
| ) |
| |
| const ( |
| repoURL = "https://go.googlesource.com/" |
| metaURL = "https://go.googlesource.com/?b=master&format=JSON" |
| startTimeout = 5 * time.Minute |
| ) |
| |
| var indexingMsg = []byte("Indexing in progress: result may be inaccurate") |
| |
| func main() { |
| p := new(Proxy) |
| go p.run() |
| http.Handle("/", p) |
| |
| if err := http.ListenAndServe(":8080", nil); err != nil { |
| p.stop() |
| log.Fatal(err) |
| } |
| } |
| |
| // Proxy implements the tip.golang.org server: a reverse-proxy |
| // that builds and runs godoc instances showing the latest docs. |
| type Proxy struct { |
| mu sync.Mutex // protects the followin' |
| proxy http.Handler |
| cur string // signature of gorepo+toolsrepo |
| cmd *exec.Cmd // live godoc instance, or nil for none |
| side string |
| err error |
| } |
| |
| func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| if r.URL.Path == "/_tipstatus" { |
| p.serveStatus(w, r) |
| return |
| } |
| p.mu.Lock() |
| proxy := p.proxy |
| err := p.err |
| p.mu.Unlock() |
| if proxy == nil { |
| s := "tip.golang.org is starting up" |
| if err != nil { |
| s = err.Error() |
| } |
| http.Error(w, s, http.StatusInternalServerError) |
| return |
| } |
| proxy.ServeHTTP(w, r) |
| } |
| |
| func (p *Proxy) serveStatus(w http.ResponseWriter, r *http.Request) { |
| p.mu.Lock() |
| defer p.mu.Unlock() |
| fmt.Fprintf(w, "side=%v\ncurrent=%v\nerror=%v\n", p.side, p.cur, p.err) |
| } |
| |
| // run runs in its own goroutine. |
| func (p *Proxy) run() { |
| p.side = "a" |
| for { |
| p.poll() |
| time.Sleep(30 * time.Second) |
| } |
| } |
| |
| func (p *Proxy) stop() { |
| p.mu.Lock() |
| defer p.mu.Unlock() |
| if p.cmd != nil { |
| p.cmd.Process.Kill() |
| } |
| } |
| |
| // poll runs from the run loop goroutine. |
| func (p *Proxy) poll() { |
| heads := gerritMetaMap() |
| if heads == nil { |
| return |
| } |
| |
| sig := heads["go"] + "-" + heads["tools"] |
| |
| p.mu.Lock() |
| changes := sig != p.cur |
| curSide := p.side |
| p.cur = sig |
| p.mu.Unlock() |
| |
| if !changes { |
| return |
| } |
| |
| newSide := "b" |
| if curSide == "b" { |
| newSide = "a" |
| } |
| |
| cmd, hostport, err := initSide(newSide, heads["go"], heads["tools"]) |
| |
| p.mu.Lock() |
| defer p.mu.Unlock() |
| if err != nil { |
| log.Println(err) |
| p.err = err |
| return |
| } |
| |
| u, err := url.Parse(fmt.Sprintf("http://%v/", hostport)) |
| if err != nil { |
| log.Println(err) |
| p.err = err |
| return |
| } |
| p.proxy = httputil.NewSingleHostReverseProxy(u) |
| p.side = newSide |
| if p.cmd != nil { |
| p.cmd.Process.Kill() |
| } |
| p.cmd = cmd |
| } |
| |
| func initSide(side, goHash, toolsHash string) (godoc *exec.Cmd, hostport string, err error) { |
| dir := filepath.Join(os.TempDir(), "tipgodoc", side) |
| if err := os.MkdirAll(dir, 0755); err != nil { |
| return nil, "", err |
| } |
| |
| goDir := filepath.Join(dir, "go") |
| toolsDir := filepath.Join(dir, "gopath/src/golang.org/x/tools") |
| if err := checkout(repoURL+"go", goHash, goDir); err != nil { |
| return nil, "", err |
| } |
| if err := checkout(repoURL+"tools", toolsHash, toolsDir); err != nil { |
| return nil, "", err |
| } |
| |
| make := exec.Command(filepath.Join(goDir, "src/make.bash")) |
| make.Dir = filepath.Join(goDir, "src") |
| if err := runErr(make); err != nil { |
| return nil, "", err |
| } |
| goBin := filepath.Join(goDir, "bin/go") |
| install := exec.Command(goBin, "install", "golang.org/x/tools/cmd/godoc") |
| install.Env = []string{ |
| "GOROOT=" + goDir, |
| "GOPATH=" + filepath.Join(dir, "gopath"), |
| "GOROOT_BOOTSTRAP=" + os.Getenv("GOROOT_BOOTSTRAP"), |
| } |
| if err := runErr(install); err != nil { |
| return nil, "", err |
| } |
| |
| godocBin := filepath.Join(goDir, "bin/godoc") |
| hostport = "localhost:8081" |
| if side == "b" { |
| hostport = "localhost:8082" |
| } |
| godoc = exec.Command(godocBin, "-http="+hostport, "-index", "-index_interval=-1s") |
| godoc.Env = []string{"GOROOT=" + goDir} |
| // TODO(adg): log this somewhere useful |
| godoc.Stdout = os.Stdout |
| godoc.Stderr = os.Stderr |
| if err := godoc.Start(); err != nil { |
| return nil, "", err |
| } |
| go func() { |
| // TODO(bradfitz): tell the proxy that this side is dead |
| if err := godoc.Wait(); err != nil { |
| log.Printf("side %v exited: %v", side, err) |
| } |
| }() |
| |
| deadline := time.Now().Add(startTimeout) |
| for time.Now().Before(deadline) { |
| time.Sleep(time.Second) |
| var res *http.Response |
| res, err = http.Get(fmt.Sprintf("http://%v/search?q=FALLTHROUGH", hostport)) |
| if err != nil { |
| continue |
| } |
| rbody, err := ioutil.ReadAll(res.Body) |
| res.Body.Close() |
| if err == nil && res.StatusCode == http.StatusOK && |
| !bytes.Contains(rbody, indexingMsg) { |
| return godoc, hostport, nil |
| } |
| } |
| godoc.Process.Kill() |
| return nil, "", fmt.Errorf("timed out waiting for side %v at %v (%v)", side, hostport, err) |
| } |
| |
| func runErr(cmd *exec.Cmd) error { |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| if len(out) == 0 { |
| return err |
| } |
| return fmt.Errorf("%s\n%v", out, err) |
| } |
| return nil |
| } |
| |
| func checkout(repo, hash, path string) error { |
| // Clone git repo if it doesn't exist. |
| if _, err := os.Stat(filepath.Join(path, ".git")); os.IsNotExist(err) { |
| if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { |
| return err |
| } |
| if err := runErr(exec.Command("git", "clone", repo, path)); err != nil { |
| return err |
| } |
| } else if err != nil { |
| return err |
| } |
| |
| // Pull down changes and update to hash. |
| cmd := exec.Command("git", "fetch") |
| cmd.Dir = path |
| if err := runErr(cmd); err != nil { |
| return err |
| } |
| cmd = exec.Command("git", "reset", "--hard", hash) |
| cmd.Dir = path |
| if err := runErr(cmd); err != nil { |
| return err |
| } |
| cmd = exec.Command("git", "clean", "-d", "-f", "-x") |
| cmd.Dir = path |
| return runErr(cmd) |
| } |
| |
| // gerritMetaMap returns the map from repo name (e.g. "go") to its |
| // latest master hash. |
| // The returned map is nil on any transient error. |
| func gerritMetaMap() map[string]string { |
| res, err := http.Get(metaURL) |
| if err != nil { |
| return nil |
| } |
| defer res.Body.Close() |
| defer io.Copy(ioutil.Discard, res.Body) // ensure EOF for keep-alive |
| if res.StatusCode != 200 { |
| return nil |
| } |
| var meta map[string]struct { |
| Branches map[string]string |
| } |
| br := bufio.NewReader(res.Body) |
| // For security reasons or something, this URL starts with ")]}'\n" before |
| // the JSON object. So ignore that. |
| // Shawn Pearce says it's guaranteed to always be just one line, ending in '\n'. |
| for { |
| b, err := br.ReadByte() |
| if err != nil { |
| return nil |
| } |
| if b == '\n' { |
| break |
| } |
| } |
| if err := json.NewDecoder(br).Decode(&meta); err != nil { |
| log.Printf("JSON decoding error from %v: %s", metaURL, err) |
| return nil |
| } |
| m := map[string]string{} |
| for repo, v := range meta { |
| if master, ok := v.Branches["master"]; ok { |
| m[repo] = master |
| } |
| } |
| return m |
| } |