github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/cmd/tip/tip.go (about) 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 "errors" 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 ( 28 repoURL = "https://go.googlesource.com/" 29 metaURL = "https://go.googlesource.com/?b=master&format=JSON" 30 startTimeout = 5 * time.Minute 31 ) 32 33 func main() { 34 const k = "TIP_BUILDER" 35 var b Builder 36 switch os.Getenv(k) { 37 case "godoc": 38 b = godocBuilder{} 39 case "talks": 40 b = talksBuilder{} 41 default: 42 log.Fatalf("Unknown %v value: %q", k, os.Getenv(k)) 43 } 44 45 p := &Proxy{builder: b} 46 go p.run() 47 http.Handle("/", p) 48 http.HandleFunc("/_ah/health", p.serveHealthCheck) 49 50 if err := http.ListenAndServe(":8080", nil); err != nil { 51 p.stop() 52 log.Fatal(err) 53 } 54 } 55 56 // Proxy implements the tip.golang.org server: a reverse-proxy 57 // that builds and runs godoc instances showing the latest docs. 58 type Proxy struct { 59 builder Builder 60 61 mu sync.Mutex // protects the followin' 62 proxy http.Handler 63 cur string // signature of gorepo+toolsrepo 64 cmd *exec.Cmd // live godoc instance, or nil for none 65 side string 66 hostport string // host and port of the live instance 67 err error 68 } 69 70 type Builder interface { 71 Signature(heads map[string]string) string 72 Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error) 73 HealthCheck(hostport string) error 74 } 75 76 func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 77 if r.URL.Path == "/_tipstatus" { 78 p.serveStatus(w, r) 79 return 80 } 81 p.mu.Lock() 82 proxy := p.proxy 83 err := p.err 84 p.mu.Unlock() 85 if proxy == nil { 86 s := "starting up" 87 if err != nil { 88 s = err.Error() 89 } 90 http.Error(w, s, http.StatusInternalServerError) 91 return 92 } 93 if r.URL.Path == "/_ah/health" { 94 if err := p.builder.HealthCheck(p.hostport); err != nil { 95 http.Error(w, "Health check failde: "+err.Error(), http.StatusInternalServerError) 96 return 97 } 98 fmt.Fprintln(w, "OK") 99 return 100 } 101 proxy.ServeHTTP(w, r) 102 } 103 104 func (p *Proxy) serveStatus(w http.ResponseWriter, r *http.Request) { 105 p.mu.Lock() 106 defer p.mu.Unlock() 107 fmt.Fprintf(w, "side=%v\ncurrent=%v\nerror=%v\n", p.side, p.cur, p.err) 108 } 109 110 func (p *Proxy) serveHealthCheck(w http.ResponseWriter, r *http.Request) { 111 p.mu.Lock() 112 defer p.mu.Unlock() 113 if p.proxy == nil { 114 http.Error(w, "not ready", 500) 115 return 116 } 117 io.WriteString(w, "ok") 118 } 119 120 // run runs in its own goroutine. 121 func (p *Proxy) run() { 122 p.side = "a" 123 for { 124 p.poll() 125 time.Sleep(30 * time.Second) 126 } 127 } 128 129 func (p *Proxy) stop() { 130 p.mu.Lock() 131 defer p.mu.Unlock() 132 if p.cmd != nil { 133 p.cmd.Process.Kill() 134 } 135 } 136 137 // poll runs from the run loop goroutine. 138 func (p *Proxy) poll() { 139 heads := gerritMetaMap() 140 if heads == nil { 141 return 142 } 143 144 sig := p.builder.Signature(heads) 145 146 p.mu.Lock() 147 changes := sig != p.cur 148 curSide := p.side 149 p.cur = sig 150 p.mu.Unlock() 151 152 if !changes { 153 return 154 } 155 156 newSide := "b" 157 if curSide == "b" { 158 newSide = "a" 159 } 160 161 dir := filepath.Join(os.TempDir(), "tip", newSide) 162 if err := os.MkdirAll(dir, 0755); err != nil { 163 p.err = err 164 return 165 } 166 hostport := "localhost:8081" 167 if newSide == "b" { 168 hostport = "localhost:8082" 169 } 170 cmd, err := p.builder.Init(dir, hostport, heads) 171 if err == nil { 172 go func() { 173 // TODO(adg,bradfitz): be smarter about dead processes 174 if err := cmd.Wait(); err != nil { 175 log.Printf("process in %v exited: %v", dir, err) 176 } 177 }() 178 err = waitReady(p.builder, hostport) 179 } 180 181 p.mu.Lock() 182 defer p.mu.Unlock() 183 if err != nil { 184 log.Println(err) 185 p.err = err 186 return 187 } 188 189 u, err := url.Parse(fmt.Sprintf("http://%v/", hostport)) 190 if err != nil { 191 log.Println(err) 192 p.err = err 193 return 194 } 195 p.proxy = httputil.NewSingleHostReverseProxy(u) 196 p.side = newSide 197 p.hostport = hostport 198 if p.cmd != nil { 199 p.cmd.Process.Kill() 200 } 201 p.cmd = cmd 202 } 203 204 func waitReady(b Builder, hostport string) error { 205 var err error 206 deadline := time.Now().Add(startTimeout) 207 for time.Now().Before(deadline) { 208 if err = b.HealthCheck(hostport); err == nil { 209 return nil 210 } 211 time.Sleep(time.Second) 212 } 213 return fmt.Errorf("timed out waiting for process at %v: %v", hostport, err) 214 } 215 216 func runErr(cmd *exec.Cmd) error { 217 out, err := cmd.CombinedOutput() 218 if err != nil { 219 if len(out) == 0 { 220 return err 221 } 222 return fmt.Errorf("%s\n%v", out, err) 223 } 224 return nil 225 } 226 227 func checkout(repo, hash, path string) error { 228 // Clone git repo if it doesn't exist. 229 if _, err := os.Stat(filepath.Join(path, ".git")); os.IsNotExist(err) { 230 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 231 return err 232 } 233 if err := runErr(exec.Command("git", "clone", repo, path)); err != nil { 234 return err 235 } 236 } else if err != nil { 237 return err 238 } 239 240 // Pull down changes and update to hash. 241 cmd := exec.Command("git", "fetch") 242 cmd.Dir = path 243 if err := runErr(cmd); err != nil { 244 return err 245 } 246 cmd = exec.Command("git", "reset", "--hard", hash) 247 cmd.Dir = path 248 if err := runErr(cmd); err != nil { 249 return err 250 } 251 cmd = exec.Command("git", "clean", "-d", "-f", "-x") 252 cmd.Dir = path 253 return runErr(cmd) 254 } 255 256 // gerritMetaMap returns the map from repo name (e.g. "go") to its 257 // latest master hash. 258 // The returned map is nil on any transient error. 259 func gerritMetaMap() map[string]string { 260 res, err := http.Get(metaURL) 261 if err != nil { 262 return nil 263 } 264 defer res.Body.Close() 265 defer io.Copy(ioutil.Discard, res.Body) // ensure EOF for keep-alive 266 if res.StatusCode != 200 { 267 return nil 268 } 269 var meta map[string]struct { 270 Branches map[string]string 271 } 272 br := bufio.NewReader(res.Body) 273 // For security reasons or something, this URL starts with ")]}'\n" before 274 // the JSON object. So ignore that. 275 // Shawn Pearce says it's guaranteed to always be just one line, ending in '\n'. 276 for { 277 b, err := br.ReadByte() 278 if err != nil { 279 return nil 280 } 281 if b == '\n' { 282 break 283 } 284 } 285 if err := json.NewDecoder(br).Decode(&meta); err != nil { 286 log.Printf("JSON decoding error from %v: %s", metaURL, err) 287 return nil 288 } 289 m := map[string]string{} 290 for repo, v := range meta { 291 if master, ok := v.Branches["master"]; ok { 292 m[repo] = master 293 } 294 } 295 return m 296 } 297 298 func getOK(url string) (body []byte, err error) { 299 res, err := http.Get(url) 300 if err != nil { 301 return nil, err 302 } 303 body, err = ioutil.ReadAll(res.Body) 304 res.Body.Close() 305 if err != nil { 306 return nil, err 307 } 308 if res.StatusCode != http.StatusOK { 309 return nil, errors.New(res.Status) 310 } 311 return body, nil 312 }