go/cmd/tipgodoc/tip.go
Brad Fitzpatrick f56df04f2d cmd/tipgodoc: make tipgodoc run on App Engine Managed VMs
At least in theory. We don't totally have it working yet. It does
run locally in the dev environment, though, which should be the same
as production, since it builds the Docker container locally.
But we're getting problems when pushing it to production.

Also some minor tweaks to the code with Andrew.

Change-Id: Id192669dbc8d3f86d9c8dad79764abd66e983895
Reviewed-on: https://go-review.googlesource.com/1761
Reviewed-by: Andrew Gerrand <adg@golang.org>
2014-12-18 06:10:19 +00:00

259 lines
5.8 KiB
Go

// 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"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
)
const metaURL = "https://go.googlesource.com/?b=master&format=JSON"
func init() {
p := new(Proxy)
go p.run()
http.Handle("/", p)
}
type Proxy struct {
mu sync.Mutex // protects the followin'
proxy *httputil.ReverseProxy
cur string // signature of gorepo+toolsrepo
side string
}
// run runs in its own goroutine.
func (p *Proxy) run() {
p.side = "a"
for {
p.poll()
time.Sleep(30 * time.Second)
}
}
// 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"
}
hostport, err := initSide(newSide, heads["go"], heads["tools"])
if err != nil {
log.Println(err)
return
}
p.mu.Lock()
defer p.mu.Unlock()
u, err := url.Parse(fmt.Sprintf("http://%v/", hostport))
if err != nil {
log.Println(err)
return
}
p.side = newSide
p.proxy = httputil.NewSingleHostReverseProxy(u)
}
func initSide(side, goHash, toolsHash string) (hostport string, err error) {
dir := filepath.Join(os.TempDir(), "tipgodoc", side)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
goDir := filepath.Join(dir, "go")
toolsDir := filepath.Join(dir, "gopath/src/golang.org/x/tools")
if err := checkout("https://go.googlesource.com/go", goHash, goDir); err != nil {
return "", err
}
if err := checkout("https://go.googlesource.com/tools", toolsHash, toolsDir); err != nil {
return "", err
}
env := []string{"GOROOT=" + goDir, "GOPATH=" + filepath.Join(dir, "gopath")}
make := exec.Command(filepath.Join(goDir, "src/make.bash"))
make.Stdout = os.Stdout
make.Stderr = os.Stderr
make.Dir = filepath.Join(goDir, "src")
if err := make.Run(); err != nil {
return "", err
}
goBin := filepath.Join(goDir, "bin/go")
install := exec.Command(goBin, "install", "golang.org/x/tools/cmd/godoc")
install.Stdout = os.Stdout
install.Stderr = os.Stderr
install.Env = env
if err := install.Run(); err != nil {
return "", err
}
godocBin := filepath.Join(goDir, "bin/godoc")
hostport = "localhost:8081"
if side == "b" {
hostport = "localhost:8082"
}
godoc := exec.Command(godocBin, "-http="+hostport)
godoc.Env = env
godoc.Stdout = os.Stdout
godoc.Stderr = os.Stderr
if err := godoc.Start(); err != nil {
return "", 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)
}
}()
for i := 0; i < 15; i++ {
time.Sleep(time.Second)
var res *http.Response
res, err = http.Get(fmt.Sprintf("http://%v/", hostport))
if err != nil {
continue
}
res.Body.Close()
if res.StatusCode == http.StatusOK {
return hostport, nil
}
}
return "", fmt.Errorf("timed out waiting for side %v at %v (%v)", side, hostport, err)
}
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.Base(path), 0755); err != nil {
return err
}
cmd := exec.Command("git", "clone", repo, path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
// TODO(bradfitz): capture the standard error output
return err
}
} else if err != nil {
return err
}
cmd := exec.Command("git", "fetch")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = path
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("git", "reset", "--hard", hash)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = path
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("git", "clean", "-d", "-f", "-x")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = path
return cmd.Run()
}
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
p.mu.Unlock()
if proxy == nil {
http.Error(w, "tip.golang.org is currently starting up, compiling tip", 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\n", p.side, p.cur)
}
// 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
}