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  }