Skip to content

Commit 5bf57d6

Browse files
committed
cmd/tipgodoc: new tip.golang.org server
Rough work in progress. Don't hate. Change-Id: I9d8247005724a21bdb5d4760cc6135bceb49f2d4 Reviewed-on: https://go-review.googlesource.com/1704 Reviewed-by: Andrew Gerrand <adg@golang.org>
1 parent cf555fa commit 5bf57d6

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed

cmd/tipgodoc/tip.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
// Copyright 2014 The Go AUTHORS. All rights reserved.
2+
// Use of this source code is governed by the Apache 2.0
3+
// license that can be found in the LICENSE file.
4+
5+
// Command tipgodoc is the beginning of the new tip.golang.org server,
6+
// serving the latest HEAD straight from the Git oven.
7+
package main
8+
9+
import (
10+
"bufio"
11+
"encoding/json"
12+
"flag"
13+
"fmt"
14+
"io"
15+
"io/ioutil"
16+
"log"
17+
"net/http"
18+
"net/http/httputil"
19+
"net/url"
20+
"os"
21+
"os/exec"
22+
"path/filepath"
23+
"sync"
24+
"time"
25+
)
26+
27+
const metaURL = "https://go.googlesource.com/?b=master&format=JSON"
28+
29+
var (
30+
pollInterval = flag.Duration("poll", 10*time.Second, "Remote repo poll interval")
31+
listenAddr = flag.String("listen", "localhost:8080", "HTTP listen address")
32+
)
33+
34+
func main() {
35+
flag.Parse()
36+
p := new(Proxy)
37+
go p.run()
38+
http.Handle("/", p)
39+
log.Fatal(http.ListenAndServe(*listenAddr, nil))
40+
}
41+
42+
type Proxy struct {
43+
mu sync.Mutex // owns the followin'
44+
proxy *httputil.ReverseProxy
45+
last string // signature of gorepo+toolsrepo
46+
side string
47+
}
48+
49+
// run runs in its own goroutine.
50+
func (p *Proxy) run() {
51+
p.side = "a"
52+
for {
53+
p.poll()
54+
time.Sleep(*pollInterval)
55+
}
56+
}
57+
58+
// poll runs from the run loop goroutine.
59+
func (p *Proxy) poll() {
60+
heads := gerritMetaMap()
61+
if heads == nil {
62+
return
63+
}
64+
65+
p.mu.Lock()
66+
curSide := p.side
67+
lastSig := p.last
68+
p.mu.Unlock()
69+
70+
sig := heads["go"] + "-" + heads["tools"]
71+
if sig == lastSig {
72+
return
73+
}
74+
newSide := "b"
75+
if curSide == "b" {
76+
newSide = "a"
77+
}
78+
79+
hostport, err := initSide(newSide, heads["go"], heads["tools"])
80+
if err != nil {
81+
log.Println(err)
82+
return
83+
}
84+
85+
p.mu.Lock()
86+
defer p.mu.Unlock()
87+
u, err := url.Parse(fmt.Sprintf("http://%v/", hostport))
88+
if err != nil {
89+
log.Println(err)
90+
return
91+
}
92+
p.side = newSide
93+
p.proxy = httputil.NewSingleHostReverseProxy(u)
94+
p.last = sig
95+
}
96+
97+
func initSide(side, goHash, toolsHash string) (hostport string, err error) {
98+
dir := filepath.Join(os.TempDir(), "tipgodoc", side)
99+
if err := os.MkdirAll(dir, 0755); err != nil {
100+
return "", err
101+
}
102+
103+
goDir := filepath.Join(dir, "go")
104+
toolsDir := filepath.Join(dir, "gopath/src/golang.org/x/tools")
105+
if err := checkout("https://go.googlesource.com/go", goHash, goDir); err != nil {
106+
return "", err
107+
}
108+
if err := checkout("https://go.googlesource.com/tools", toolsHash, toolsDir); err != nil {
109+
return "", err
110+
111+
}
112+
113+
env := []string{"GOROOT=" + goDir, "GOPATH=" + filepath.Join(dir, "gopath")}
114+
115+
make := exec.Command("./make.bash")
116+
make.Stdout = os.Stdout
117+
make.Stderr = os.Stderr
118+
make.Dir = filepath.Join(goDir, "src")
119+
if err := make.Run(); err != nil {
120+
return "", err
121+
}
122+
goBin := filepath.Join(goDir, "bin/go")
123+
install := exec.Command(goBin, "install", "golang.org/x/tools/cmd/godoc")
124+
install.Stdout = os.Stdout
125+
install.Stderr = os.Stderr
126+
install.Env = env
127+
if err := install.Run(); err != nil {
128+
return "", err
129+
}
130+
131+
godocBin := filepath.Join(goDir, "bin/godoc")
132+
hostport = "localhost:8081"
133+
if side == "b" {
134+
hostport = "localhost:8082"
135+
}
136+
godoc := exec.Command(godocBin, "-http="+hostport)
137+
godoc.Env = env
138+
godoc.Stdout = os.Stdout
139+
godoc.Stderr = os.Stderr
140+
if err := godoc.Start(); err != nil {
141+
return "", err
142+
}
143+
go func() {
144+
// TODO(bradfitz): tell the proxy that this side is dead
145+
if err := godoc.Wait(); err != nil {
146+
log.Printf("side %v exited: %v", side, err)
147+
}
148+
}()
149+
150+
for i := 0; i < 15; i++ {
151+
time.Sleep(time.Second)
152+
var res *http.Response
153+
res, err = http.Get(fmt.Sprintf("http://%v/", hostport))
154+
if err != nil {
155+
continue
156+
}
157+
res.Body.Close()
158+
if res.StatusCode == http.StatusOK {
159+
return hostport, nil
160+
}
161+
}
162+
return "", fmt.Errorf("timed out waiting for side %v at %v (%v)", side, hostport, err)
163+
}
164+
165+
func checkout(repo, hash, path string) error {
166+
// Clone git repo if it doesn't exist.
167+
if _, err := os.Stat(filepath.Join(path, ".git")); os.IsNotExist(err) {
168+
if err := os.MkdirAll(filepath.Base(path), 0755); err != nil {
169+
return err
170+
}
171+
cmd := exec.Command("git", "clone", repo, path)
172+
cmd.Stdout = os.Stdout
173+
cmd.Stderr = os.Stderr
174+
if err := cmd.Run(); err != nil {
175+
// TODO(bradfitz): capture the standard error output
176+
return err
177+
}
178+
} else if err != nil {
179+
return err
180+
}
181+
182+
cmd := exec.Command("git", "fetch")
183+
cmd.Stdout = os.Stdout
184+
cmd.Stderr = os.Stderr
185+
cmd.Dir = path
186+
if err := cmd.Run(); err != nil {
187+
return err
188+
}
189+
cmd = exec.Command("git", "reset", "--hard", hash)
190+
cmd.Stdout = os.Stdout
191+
cmd.Stderr = os.Stderr
192+
cmd.Dir = path
193+
if err := cmd.Run(); err != nil {
194+
return err
195+
}
196+
cmd = exec.Command("git", "clean", "-d", "-f", "-x")
197+
cmd.Stdout = os.Stdout
198+
cmd.Stderr = os.Stderr
199+
cmd.Dir = path
200+
return cmd.Run()
201+
}
202+
203+
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
204+
if r.URL.Path == "/_tipstatus" {
205+
p.serveStatus(w, r)
206+
return
207+
}
208+
p.mu.Lock()
209+
proxy := p.proxy
210+
p.mu.Unlock()
211+
if proxy == nil {
212+
http.Error(w, "not ready", http.StatusInternalServerError)
213+
return
214+
}
215+
proxy.ServeHTTP(w, r)
216+
}
217+
218+
func (p *Proxy) serveStatus(w http.ResponseWriter, r *http.Request) {
219+
p.mu.Lock()
220+
defer p.mu.Unlock()
221+
fmt.Fprintf(w, "side=%v\nlast=%v\n", p.side, p.last)
222+
}
223+
224+
// gerritMetaMap returns the map from repo name (e.g. "go") to its
225+
// latest master hash.
226+
// The returned map is nil on any transient error.
227+
func gerritMetaMap() map[string]string {
228+
res, err := http.Get(metaURL)
229+
if err != nil {
230+
return nil
231+
}
232+
defer res.Body.Close()
233+
defer io.Copy(ioutil.Discard, res.Body) // ensure EOF for keep-alive
234+
if res.StatusCode != 200 {
235+
return nil
236+
}
237+
var meta map[string]struct {
238+
Branches map[string]string
239+
}
240+
br := bufio.NewReader(res.Body)
241+
// For security reasons or something, this URL starts with ")]}'\n" before
242+
// the JSON object. So ignore that.
243+
// Shawn Pearce says it's guaranteed to always be just one line, ending in '\n'.
244+
for {
245+
b, err := br.ReadByte()
246+
if err != nil {
247+
return nil
248+
}
249+
if b == '\n' {
250+
break
251+
}
252+
}
253+
if err := json.NewDecoder(br).Decode(&meta); err != nil {
254+
log.Printf("JSON decoding error from %v: %s", metaURL, err)
255+
return nil
256+
}
257+
m := map[string]string{}
258+
for repo, v := range meta {
259+
if master, ok := v.Branches["master"]; ok {
260+
m[repo] = master
261+
}
262+
}
263+
return m
264+
}

0 commit comments

Comments
 (0)