github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/misc/buildbot/master/master.go (about)

     1  /*
     2  Copyright 2013 The Camlistore Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // The buildbot is Camlistore's continuous builder.
    18  // This master program monitors changes to the Go and Camlistore trees,
    19  // then rebuilds and restarts a builder when a change dictates as much.
    20  // It receives a report from a builder when it has finished running
    21  // a test suite, but it can also poll a builder before completion
    22  // to get a progress report.
    23  // It also serves the web requests.
    24  package main
    25  
    26  import (
    27  	"bytes"
    28  	"crypto/sha1"
    29  	"encoding/hex"
    30  	"encoding/json"
    31  	"flag"
    32  	"fmt"
    33  	"html/template"
    34  	"io"
    35  	"io/ioutil"
    36  	"log"
    37  	"net"
    38  	"net/http"
    39  	"os"
    40  	"os/exec"
    41  	"os/signal"
    42  	"path/filepath"
    43  	"regexp"
    44  	"runtime"
    45  	"strings"
    46  	"sync"
    47  	"syscall"
    48  	"time"
    49  
    50  	"camlistore.org/pkg/httputil"
    51  	"camlistore.org/pkg/osutil"
    52  )
    53  
    54  const (
    55  	interval      = 60 * time.Second // polling frequency
    56  	historySize   = 30
    57  	maxStderrSize = 1 << 20 // Keep last 1 MB of logging.
    58  )
    59  
    60  var (
    61  	altCamliRevURL = flag.String("camlirevurl", "", "alternative URL to query about the latest camlistore revision hash (e.g camlistore.org/latesthash), to alleviate hitting too often the Camlistore git repo.")
    62  	builderOpts    = flag.String("builderopts", "", "list of comma separated options that will be passed to the builders (ex: '-verbose=true,-faketests=true,-skipgo1build=true'). Mainly for debugging.")
    63  	builderPort    = flag.String("builderport", "8081", "listening port for the builder bot")
    64  	builderSrc     = flag.String("buildersrc", "", "Go source file for the builder bot. For testing changes to the builder bot that haven't been committed yet.")
    65  	getGo          = flag.Bool("getgo", false, "Do not use the system's Go to build the builder, use the downloaded gotip instead.")
    66  	help           = flag.Bool("h", false, "show this help")
    67  	host           = flag.String("host", "0.0.0.0:8080", "listening hostname and port")
    68  	peers          = flag.String("peers", "", "comma separated list of host:port masters (besides this one) our builders will report to.")
    69  	verbose        = flag.Bool("verbose", false, "print what's going on")
    70  	certFile       = flag.String("tlsCertFile", "", "TLS public key in PEM format.  Must be used with -tlsKeyFile")
    71  	keyFile        = flag.String("tlsKeyFile", "", "TLS private key in PEM format.  Must be used with -tlsCertFile")
    72  )
    73  
    74  var (
    75  	camliHeadHash           string
    76  	camliRoot               string
    77  	dbg                     *debugger
    78  	defaultDir              string
    79  	doBuildGo, doBuildCamli bool
    80  	goTipDir                string
    81  	goTipHash               string
    82  
    83  	historylk sync.Mutex
    84  	history   = make(map[string][]*biTestSuite) // key is the OS_Arch on which the tests were run
    85  
    86  	inProgresslk sync.Mutex
    87  	inProgress   *testSuite
    88  	// Process of the local builder bot, so we can kill it
    89  	// when we get killed.
    90  	builderProc *os.Process
    91  
    92  	// For "If-Modified-Since" requests on the status page.
    93  	// Updated every time a new test suite starts or ends.
    94  	lastModified time.Time
    95  
    96  	// Override the os.Stderr used by the default logger so we can provide
    97  	// more debug info on status page.
    98  	logStderr   = newLockedBuffer()
    99  	multiWriter io.Writer
   100  
   101  	// Set after flag parsing based on certFile & keyFile.
   102  	useTLS bool
   103  )
   104  
   105  // lockedBuffer protects all Write calls with a mutex.  Users of lockedBuffer
   106  // must wrap any calls to Bytes, and use of the resulting slice with calls to
   107  // Lock/Unlock.
   108  type lockedBuffer struct {
   109  	sync.Mutex // guards ringBuffer
   110  	*ringBuffer
   111  }
   112  
   113  func newLockedBuffer() *lockedBuffer {
   114  	return &lockedBuffer{ringBuffer: newRingBuffer(maxStderrSize)}
   115  }
   116  
   117  func (lb *lockedBuffer) Write(b []byte) (int, error) {
   118  	lb.Lock()
   119  	defer lb.Unlock()
   120  	return lb.ringBuffer.Write(b)
   121  }
   122  
   123  type ringBuffer struct {
   124  	buf []byte
   125  	off int // End of ring buffer.
   126  	l   int // Length of ring buffer filled.
   127  }
   128  
   129  func newRingBuffer(maxSize int) *ringBuffer {
   130  	return &ringBuffer{
   131  		buf: make([]byte, maxSize),
   132  	}
   133  }
   134  
   135  func (rb *ringBuffer) Bytes() []byte {
   136  	if (rb.off - rb.l) >= 0 {
   137  		// Partially full buffer with no wrap.
   138  		return rb.buf[rb.off-rb.l : rb.off]
   139  	}
   140  
   141  	// Buffer has been wrapped, copy second half then first half.
   142  	start := rb.off - rb.l
   143  	if start < 0 {
   144  		start = rb.off
   145  	}
   146  	b := make([]byte, 0, cap(rb.buf))
   147  	b = append(b, rb.buf[start:]...)
   148  	b = append(b, rb.buf[:start]...)
   149  	return b
   150  }
   151  
   152  func (rb *ringBuffer) Write(buf []byte) (int, error) {
   153  	ringLen := cap(rb.buf)
   154  	for i, b := range buf {
   155  		rb.buf[(rb.off+i)%ringLen] = b
   156  	}
   157  	rb.off = (rb.off + len(buf)) % ringLen
   158  	rb.l = rb.l + len(buf)
   159  	if rb.l > ringLen {
   160  		rb.l = ringLen
   161  	}
   162  	return len(buf), nil
   163  }
   164  
   165  var userAuthFile = filepath.Join(osutil.CamliConfigDir(), "masterbot-config.json")
   166  
   167  type userAuth struct {
   168  	sync.Mutex   // guards userPass map.
   169  	userPass     map[string]string
   170  	configFile   string
   171  	pollInterval time.Duration
   172  	lastModTime  time.Time
   173  }
   174  
   175  func newUserAuth(configFile string) (*userAuth, error) {
   176  	ua := &userAuth{
   177  		configFile:   configFile,
   178  		pollInterval: time.Minute,
   179  	}
   180  	if _, err := os.Stat(configFile); err != nil {
   181  		if !os.IsNotExist(err) {
   182  			return nil, err
   183  		}
   184  		// It is okay to have no remote users configured.
   185  		log.Printf("no user config file found %q, remote reporting disabled",
   186  			configFile)
   187  	}
   188  
   189  	go ua.pollUsers()
   190  	return ua, nil
   191  }
   192  
   193  func (ua *userAuth) resetMissing(err error) error {
   194  	if os.IsNotExist(err) {
   195  		ua.Lock()
   196  		if ua.userPass != nil {
   197  			log.Printf("%q disappeared, remote reporting disabled",
   198  				ua.configFile)
   199  		}
   200  		ua.userPass = nil
   201  		ua.Unlock()
   202  		return nil
   203  	}
   204  	return err
   205  }
   206  
   207  func (ua *userAuth) loadUsers() error {
   208  	s, err := os.Stat(ua.configFile)
   209  	if err != nil {
   210  		return ua.resetMissing(err)
   211  	}
   212  
   213  	defer func() {
   214  		ua.lastModTime = s.ModTime()
   215  	}()
   216  
   217  	if ua.lastModTime.Before(s.ModTime()) {
   218  		r, err := os.Open(ua.configFile)
   219  		if err != nil {
   220  			return ua.resetMissing(err)
   221  		}
   222  		defer r.Close()
   223  
   224  		dec := json.NewDecoder(r)
   225  		// Use tmp map so failed parsing doesn't accidentally wipe out user
   226  		// list.
   227  		tmp := make(map[string]string)
   228  		err = dec.Decode(&tmp)
   229  		if err != nil {
   230  			return err
   231  		}
   232  
   233  		ua.Lock()
   234  		ua.userPass = tmp
   235  		ua.Unlock()
   236  
   237  		log.Println("Found", len(ua.userPass), "remote users in config",
   238  			ua.configFile)
   239  	}
   240  	return nil
   241  }
   242  
   243  func (ua *userAuth) pollUsers() {
   244  	for {
   245  		if err := ua.loadUsers(); err != nil {
   246  			log.Fatalf("Error loading user file %q: %v", ua.configFile, err)
   247  		}
   248  		time.Sleep(ua.pollInterval)
   249  	}
   250  }
   251  
   252  func hashPassword(pw string) string {
   253  	h := sha1.New()
   254  	fmt.Fprint(h, pw)
   255  	return hex.EncodeToString(h.Sum(nil))
   256  }
   257  
   258  func (ua *userAuth) auth(r *http.Request) bool {
   259  	user, pass, err := httputil.BasicAuth(r)
   260  	if user == "" || pass == "" || err != nil {
   261  		return false
   262  	}
   263  
   264  	ua.Lock()
   265  	defer ua.Unlock()
   266  	passHash, ok := ua.userPass[user]
   267  	if !ok {
   268  		return false
   269  	}
   270  
   271  	return passHash == hashPassword(pass)
   272  }
   273  
   274  var devcamBin = filepath.Join("bin", "devcam")
   275  var (
   276  	hgCloneGoTipCmd = newTask("hg", "clone", "-u", "tip", "https://code.google.com/p/go")
   277  	hgPullCmd       = newTask("hg", "pull")
   278  	hgLogCmd        = newTask("hg", "log", "-r", "tip", "--template", "{node}")
   279  	gitCloneCmd     = newTask("git", "clone", "https://camlistore.googlesource.com/camlistore")
   280  	gitPullCmd      = newTask("git", "pull")
   281  	gitRevCmd       = newTask("git", "rev-parse", "HEAD")
   282  	buildGoCmd      = newTask("./make.bash")
   283  )
   284  
   285  func usage() {
   286  	fmt.Fprintf(os.Stderr, "\t masterBot \n")
   287  	flag.PrintDefaults()
   288  	os.Exit(2)
   289  }
   290  
   291  type debugger struct {
   292  	lg *log.Logger
   293  }
   294  
   295  func (dbg *debugger) Printf(format string, v ...interface{}) {
   296  	if dbg != nil && *verbose {
   297  		dbg.lg.Printf(format, v...)
   298  	}
   299  }
   300  
   301  func (dbg *debugger) Println(v ...interface{}) {
   302  	if v == nil {
   303  		return
   304  	}
   305  	if dbg != nil && *verbose {
   306  		dbg.lg.Println(v...)
   307  	}
   308  }
   309  
   310  type task struct {
   311  	Program  string
   312  	Args     []string
   313  	Start    time.Time
   314  	Duration time.Duration
   315  	Err      string
   316  }
   317  
   318  func newTask(program string, args ...string) *task {
   319  	return &task{Program: program, Args: args}
   320  }
   321  
   322  func (t *task) String() string {
   323  	return fmt.Sprintf("%v %v", t.Program, t.Args)
   324  }
   325  
   326  func (t *task) run() (string, error) {
   327  	var err error
   328  	dbg.Println(t.String())
   329  	var stdout, stderr bytes.Buffer
   330  	cmd := exec.Command(t.Program, t.Args...)
   331  	cmd.Stdout = &stdout
   332  	cmd.Stderr = &stderr
   333  	if err = cmd.Run(); err != nil {
   334  		return "", fmt.Errorf("%v: %v", err, stderr.String())
   335  	}
   336  	return stdout.String(), nil
   337  }
   338  
   339  type testSuite struct {
   340  	Run       []*task
   341  	CamliHash string
   342  	GoHash    string
   343  	Err       string
   344  	Start     time.Time
   345  	IsTip     bool
   346  }
   347  
   348  type biTestSuite struct {
   349  	Local bool
   350  	Go1   *testSuite
   351  	GoTip *testSuite
   352  }
   353  
   354  func addTestSuites(OSArch string, ts *biTestSuite) {
   355  	if ts == nil {
   356  		return
   357  	}
   358  	historylk.Lock()
   359  	if ts.Local {
   360  		inProgresslk.Lock()
   361  		defer inProgresslk.Unlock()
   362  	}
   363  	defer historylk.Unlock()
   364  	historyOSArch := history[OSArch]
   365  	if len(historyOSArch) > historySize {
   366  		historyOSArch = append(historyOSArch[1:historySize], ts)
   367  	} else {
   368  		historyOSArch = append(historyOSArch, ts)
   369  	}
   370  	history[OSArch] = historyOSArch
   371  	if ts.Local {
   372  		inProgress = nil
   373  	}
   374  	lastModified = time.Now()
   375  }
   376  
   377  func main() {
   378  	flag.Usage = usage
   379  	flag.Parse()
   380  	if *help {
   381  		usage()
   382  	}
   383  	useTLS = *certFile != "" && *keyFile != ""
   384  
   385  	go handleSignals()
   386  	ua, err := newUserAuth(userAuthFile)
   387  	if err != nil {
   388  		log.Fatalf("Error creating user auth wrapper: %v", err)
   389  	}
   390  
   391  	authWrapper := func(f http.HandlerFunc) http.HandlerFunc {
   392  		return func(w http.ResponseWriter, r *http.Request) {
   393  			if !(httputil.IsLocalhost(r) || ua.auth(r)) {
   394  				w.Header().Set("WWW-Authenticate", `Basic realm="buildbot master"`)
   395  				http.Error(w, "Unauthorized access", http.StatusUnauthorized)
   396  				return
   397  			}
   398  			f(w, r)
   399  		}
   400  	}
   401  
   402  	http.HandleFunc(okPrefix, okHandler)
   403  	http.HandleFunc(failPrefix, failHandler)
   404  	http.HandleFunc(progressPrefix, progressHandler)
   405  	http.HandleFunc(stderrPrefix, logHandler)
   406  	http.HandleFunc("/", statusHandler)
   407  	http.HandleFunc(reportPrefix, authWrapper(reportHandler))
   408  	go func() {
   409  		log.Printf("Now starting to listen on %v", *host)
   410  		if useTLS {
   411  			if err := http.ListenAndServeTLS(*host, *certFile, *keyFile, nil); err != nil {
   412  				log.Fatalf("Could not start listening (TLS) on %v: %v", *host, err)
   413  			}
   414  		} else {
   415  			if err := http.ListenAndServe(*host, nil); err != nil {
   416  				log.Fatalf("Could not start listening on %v: %v", *host, err)
   417  			}
   418  		}
   419  	}()
   420  	setup()
   421  
   422  	for {
   423  		if err := pollGoChange(); err != nil {
   424  			log.Print(err)
   425  			goto Sleep
   426  		}
   427  		if err := pollCamliChange(); err != nil {
   428  			log.Print(err)
   429  			goto Sleep
   430  		}
   431  		if doBuildGo || doBuildCamli {
   432  			if err := buildBuilder(); err != nil {
   433  				log.Printf("Could not build builder bot: %v", err)
   434  				goto Sleep
   435  			}
   436  			cmd, err := startBuilder(goTipHash, camliHeadHash)
   437  			if err != nil {
   438  				log.Printf("Could not start builder bot: %v", err)
   439  				goto Sleep
   440  			}
   441  			dbg.Println("Waiting for builder to finish")
   442  			if err := cmd.Wait(); err != nil {
   443  				log.Printf("builder finished with error: %v", err)
   444  			}
   445  			resetBuilderState()
   446  		}
   447  	Sleep:
   448  		tsk := newTask("time.Sleep", interval.String())
   449  		dbg.Println(tsk.String())
   450  		time.Sleep(interval)
   451  	}
   452  }
   453  
   454  func resetBuilderState() {
   455  	inProgresslk.Lock()
   456  	defer inProgresslk.Unlock()
   457  	builderProc = nil
   458  	inProgress = nil
   459  }
   460  
   461  func setup() {
   462  	// Install custom stderr for display in status webpage.
   463  	multiWriter = io.MultiWriter(logStderr, os.Stderr)
   464  	log.SetOutput(multiWriter)
   465  
   466  	var err error
   467  	defaultDir, err = os.Getwd()
   468  	if err != nil {
   469  		log.Fatalf("Could not get current dir: %v", err)
   470  	}
   471  	dbg = &debugger{log.New(multiWriter, "", log.LstdFlags)}
   472  
   473  	goTipDir, err = filepath.Abs("gotip")
   474  	if err != nil {
   475  		log.Fatal(err)
   476  	}
   477  	// if gotip dir exist, just reuse it
   478  	if _, err := os.Stat(goTipDir); err != nil {
   479  		if !os.IsNotExist(err) {
   480  			log.Fatalf("Could not stat %v: %v", goTipDir, err)
   481  		}
   482  		if _, err := hgCloneGoTipCmd.run(); err != nil {
   483  			log.Fatalf("Could not hg clone %v: %v", goTipDir, err)
   484  		}
   485  		if err := os.Rename("go", goTipDir); err != nil {
   486  			log.Fatalf("Could not rename go dir into %v: %v", goTipDir, err)
   487  		}
   488  	}
   489  
   490  	if _, err := exec.LookPath("go"); err != nil {
   491  		// Go was not found on this machine, but we've already
   492  		// downloaded gotip anyway, so let's install it and
   493  		// use it to build the builder bot.
   494  		*getGo = true
   495  	}
   496  
   497  	if *getGo {
   498  		// set PATH
   499  		splitter := ":"
   500  		switch runtime.GOOS {
   501  		case "windows":
   502  			splitter = ";"
   503  		case "plan9":
   504  			panic("unsupported OS")
   505  		}
   506  		p := os.Getenv("PATH")
   507  		if p == "" {
   508  			log.Fatal("PATH not set")
   509  		}
   510  		p = filepath.Join(goTipDir, "bin") + splitter + p
   511  		if err := os.Setenv("PATH", p); err != nil {
   512  			log.Fatalf("Could not set PATH to %v: %v", p, err)
   513  		}
   514  		// and check if we already have a gotip binary
   515  		if _, err := exec.LookPath("go"); err != nil {
   516  			// if not, build gotip
   517  			if err := buildGo(); err != nil {
   518  				log.Fatal(err)
   519  			}
   520  		}
   521  	}
   522  
   523  	// get camlistore source
   524  	if err := os.Chdir(defaultDir); err != nil {
   525  		log.Fatalf("Could not cd to %v: %v", defaultDir, err)
   526  	}
   527  	camliRoot, err = filepath.Abs("src/camlistore.org")
   528  	if err != nil {
   529  		log.Fatal(err)
   530  	}
   531  	// if camlistore dir already exists, reuse it
   532  	if _, err := os.Stat(camliRoot); err != nil {
   533  		if !os.IsNotExist(err) {
   534  			log.Fatalf("Could not stat %v: %v", camliRoot, err)
   535  		}
   536  		cloneCmd := newTask(gitCloneCmd.Program, append(gitCloneCmd.Args, camliRoot)...)
   537  		if _, err := cloneCmd.run(); err != nil {
   538  			log.Fatalf("Could not git clone into %v: %v", camliRoot, err)
   539  		}
   540  	}
   541  	// override GOPATH to only point to our freshly updated camlistore source.
   542  	if err := os.Setenv("GOPATH", defaultDir); err != nil {
   543  		log.Fatalf("Could not set GOPATH to %v: %v", defaultDir, err)
   544  	}
   545  }
   546  
   547  func buildGo() error {
   548  	if err := os.Chdir(filepath.Join(goTipDir, "src")); err != nil {
   549  		log.Fatalf("Could not cd to %v: %v", goTipDir, err)
   550  	}
   551  	if _, err := buildGoCmd.run(); err != nil {
   552  		return err
   553  	}
   554  	return nil
   555  }
   556  
   557  func handleSignals() {
   558  	c := make(chan os.Signal)
   559  	sigs := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
   560  	signal.Notify(c, sigs...)
   561  	for {
   562  		sig := <-c
   563  		sysSig, ok := sig.(syscall.Signal)
   564  		if !ok {
   565  			log.Fatal("Not a unix signal")
   566  		}
   567  		switch sysSig {
   568  		case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
   569  			log.Printf("Received %v signal; cleaning up and terminating.", sig)
   570  			if builderProc != nil {
   571  				if err := builderProc.Kill(); err != nil {
   572  					log.Fatalf("Failed to kill our builder bot with pid %v: %v.", builderProc.Pid, err)
   573  				}
   574  			}
   575  			os.Exit(0)
   576  		default:
   577  			panic("should not get other signals here")
   578  		}
   579  	}
   580  }
   581  
   582  func pollGoChange() error {
   583  	doBuildGo = false
   584  	if err := os.Chdir(goTipDir); err != nil {
   585  		log.Fatalf("Could not cd to %v: %v", goTipDir, err)
   586  	}
   587  	tasks := []*task{
   588  		hgPullCmd,
   589  		hgLogCmd,
   590  	}
   591  	hash := ""
   592  	for _, t := range tasks {
   593  		out, err := t.run()
   594  		if err != nil {
   595  			if t.String() == hgPullCmd.String() {
   596  				log.Printf("Could not pull from Go repo with %v: %v", t.String(), err)
   597  				continue
   598  			}
   599  			return fmt.Errorf("Could not prepare the Go tree with %v: %v", t.String(), err)
   600  		}
   601  		hash = strings.TrimRight(out, "\n")
   602  	}
   603  	dbg.Println("previous head in go tree: " + goTipHash)
   604  	dbg.Println("current head in go tree: " + hash)
   605  	if hash != goTipHash {
   606  		if !plausibleHashRx.MatchString(hash) {
   607  			log.Printf("Go rev %q does not look like an hg hash.", hash)
   608  		} else {
   609  			goTipHash = hash
   610  			doBuildGo = true
   611  			dbg.Println("Changes in go tree detected; a builder will be started.")
   612  		}
   613  	}
   614  	// Should never happen, but be paranoid.
   615  	if !plausibleHashRx.MatchString(goTipHash) {
   616  		return fmt.Errorf("goTipHash %q does not look like an hg hash.", goTipHash)
   617  	}
   618  	return nil
   619  }
   620  
   621  var plausibleHashRx = regexp.MustCompile(`^[a-f0-9]{40}$`)
   622  
   623  func altCamliPolling() (string, error) {
   624  	resp, err := http.Get(*altCamliRevURL)
   625  	if err != nil {
   626  		return "", fmt.Errorf("Could not get camliHash from %v: %v", *altCamliRevURL, err)
   627  	}
   628  	defer resp.Body.Close()
   629  	body, err := ioutil.ReadAll(resp.Body)
   630  	if err != nil {
   631  		return "", fmt.Errorf("Could not read camliHash from %v's response: %v", *altCamliRevURL, err)
   632  	}
   633  	hash := strings.TrimSpace(string(body))
   634  	if !plausibleHashRx.MatchString(hash) {
   635  		return "", fmt.Errorf("%v's response does not look like a git hash.", *altCamliRevURL)
   636  	}
   637  	return hash, nil
   638  }
   639  
   640  func pollCamliChange() error {
   641  	doBuildCamli = false
   642  	altDone := false
   643  	var err error
   644  	rev := ""
   645  	if *altCamliRevURL != "" {
   646  		rev, err = altCamliPolling()
   647  		if err != nil {
   648  			log.Print(err)
   649  			dbg.Println("Defaulting to the camli repo instead")
   650  		} else {
   651  			dbg.Printf("Got camli rev %v from %v\n", rev, *altCamliRevURL)
   652  			altDone = true
   653  		}
   654  	}
   655  	if !altDone {
   656  		if err := os.Chdir(camliRoot); err != nil {
   657  			log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   658  		}
   659  		tasks := []*task{
   660  			gitPullCmd,
   661  			gitRevCmd,
   662  		}
   663  		for _, t := range tasks {
   664  			out, err := t.run()
   665  			if err != nil {
   666  				if t.String() == gitPullCmd.String() {
   667  					log.Printf("Could not pull from Camli repo with %v: %v", t.String(), err)
   668  					continue
   669  				}
   670  				return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
   671  			}
   672  			rev = strings.TrimRight(out, "\n")
   673  		}
   674  	}
   675  	dbg.Println("previous head in camli tree: " + camliHeadHash)
   676  	dbg.Println("current head in camli tree: " + rev)
   677  	if rev != camliHeadHash {
   678  		if !plausibleHashRx.MatchString(rev) {
   679  			return fmt.Errorf("Camlistore rev %q does not look like a git hash.", rev)
   680  		} else {
   681  			camliHeadHash = rev
   682  			doBuildCamli = true
   683  			dbg.Println("Changes in camli tree detected; a builder will be started.")
   684  		}
   685  	}
   686  	if !plausibleHashRx.MatchString(camliHeadHash) {
   687  		return fmt.Errorf("camliHeadHash %q does not look like a git hash.", camliHeadHash)
   688  	}
   689  	return nil
   690  }
   691  
   692  const builderBotBin = "builderBot"
   693  
   694  func buildBuilder() error {
   695  	source := *builderSrc
   696  	if source == "" {
   697  		if *altCamliRevURL != "" {
   698  			// since we used altCamliRevURL (and not git pull), our camli tree
   699  			// and hence our buildbot source code, might not be up to date.
   700  			if err := os.Chdir(camliRoot); err != nil {
   701  				log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   702  			}
   703  			out, err := gitRevCmd.run()
   704  			if err != nil {
   705  				return fmt.Errorf("Could not get camli tree revision with %v: %v\n", gitRevCmd.String(), err)
   706  			}
   707  			rev := strings.TrimRight(out, "\n")
   708  			if rev != camliHeadHash {
   709  				// camli tree needs to be updated
   710  				_, err := gitPullCmd.run()
   711  				if err != nil {
   712  					log.Printf("Could not update the Camli repo with %v: %v\n", gitPullCmd.String(), err)
   713  				}
   714  			}
   715  		}
   716  		source = filepath.Join(camliRoot, filepath.FromSlash("misc/buildbot/builder/builder.go"))
   717  	}
   718  	if err := os.Chdir(defaultDir); err != nil {
   719  		log.Fatalf("Could not cd to %v: %v", defaultDir, err)
   720  	}
   721  	tsk := newTask(
   722  		"go",
   723  		"build",
   724  		"-o",
   725  		builderBotBin,
   726  		source,
   727  	)
   728  	if _, err := tsk.run(); err != nil {
   729  		return err
   730  	}
   731  	return nil
   732  
   733  }
   734  
   735  func startBuilder(goHash, camliHash string) (*exec.Cmd, error) {
   736  	if err := os.Chdir(defaultDir); err != nil {
   737  		log.Fatalf("Could not cd to %v: %v", defaultDir, err)
   738  	}
   739  	dbg.Println("Starting builder bot")
   740  	builderHost := "localhost:" + *builderPort
   741  	ourHost, ourPort, err := net.SplitHostPort(*host)
   742  	if err != nil {
   743  		return nil, fmt.Errorf("Could not find out our host/port: %v", err)
   744  	}
   745  	if ourHost == "0.0.0.0" {
   746  		ourHost = "localhost"
   747  	}
   748  	masterHosts := ourHost + ":" + ourPort
   749  	if useTLS {
   750  		masterHosts = "https://" + masterHosts
   751  	}
   752  	if *peers != "" {
   753  		masterHosts += "," + *peers
   754  	}
   755  	args := []string{
   756  		"-host",
   757  		builderHost,
   758  		"-masterhosts",
   759  		masterHosts,
   760  	}
   761  	if *builderOpts != "" {
   762  		moreOpts := strings.Split(*builderOpts, ",")
   763  		args = append(args, moreOpts...)
   764  	}
   765  	cmd := exec.Command("./"+builderBotBin, args...)
   766  	cmd.Stdout = multiWriter
   767  	cmd.Stderr = multiWriter
   768  	if err := cmd.Start(); err != nil {
   769  		return nil, err
   770  	}
   771  	inProgresslk.Lock()
   772  	defer inProgresslk.Unlock()
   773  	builderProc = cmd.Process
   774  	inProgress = &testSuite{
   775  		Start:     time.Now(),
   776  		GoHash:    goHash,
   777  		CamliHash: camliHash,
   778  	}
   779  	return cmd, nil
   780  }
   781  
   782  var (
   783  	okPrefix       = "/ok/"
   784  	failPrefix     = "/fail/"
   785  	progressPrefix = "/progress"
   786  	currentPrefix  = "/current"
   787  	stderrPrefix   = "/stderr"
   788  	reportPrefix   = "/report"
   789  
   790  	statusTpl    = template.Must(template.New("status").Funcs(tmplFuncs).Parse(statusHTML))
   791  	taskTpl      = template.Must(template.New("task").Parse(taskHTML))
   792  	testSuiteTpl = template.Must(template.New("ok").Parse(testSuiteHTML))
   793  )
   794  
   795  var tmplFuncs = template.FuncMap{
   796  	"camliRepoURL": camliRepoURL,
   797  	"goRepoURL":    goRepoURL,
   798  	"shortHash":    shortHash,
   799  }
   800  
   801  var OSArchVersionTime = regexp.MustCompile(`(.*_.*)/(gotip|go1)/(.*)`)
   802  
   803  // unlocked; history needs to be protected from the caller.
   804  func getPastTestSuite(key string) (*testSuite, error) {
   805  	parts := OSArchVersionTime.FindStringSubmatch(key)
   806  	if parts == nil || len(parts) != 4 {
   807  		return nil, fmt.Errorf("bogus osArch/goversion/time url path: %v", key)
   808  	}
   809  	isGoTip := false
   810  	switch parts[2] {
   811  	case "gotip":
   812  		isGoTip = true
   813  	case "go1":
   814  	default:
   815  		return nil, fmt.Errorf("bogus go version in url path: %v", parts[2])
   816  	}
   817  	historyOSArch, ok := history[parts[1]]
   818  	if !ok {
   819  		return nil, fmt.Errorf("os %v not found in history", parts[1])
   820  	}
   821  	for _, v := range historyOSArch {
   822  		ts := v.Go1
   823  		if isGoTip {
   824  			ts = v.GoTip
   825  		}
   826  		if ts.Start.String() == parts[3] {
   827  			return ts, nil
   828  		}
   829  	}
   830  	return nil, fmt.Errorf("date %v not found in history for osArch %v", parts[3], parts[1])
   831  }
   832  
   833  // modtime is the modification time of the resource to be served, or IsZero().
   834  // return value is whether this request is now complete.
   835  func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {
   836  	if modtime.IsZero() {
   837  		return false
   838  	}
   839  
   840  	// The Date-Modified header truncates sub-second precision, so
   841  	// use mtime < t+1s instead of mtime <= t to check for unmodified.
   842  	if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
   843  		h := w.Header()
   844  		delete(h, "Content-Type")
   845  		delete(h, "Content-Length")
   846  		w.WriteHeader(http.StatusNotModified)
   847  		return true
   848  	}
   849  	w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
   850  	return false
   851  }
   852  
   853  func reportHandler(w http.ResponseWriter, r *http.Request) {
   854  	if r.Method != "POST" {
   855  		log.Println("Invalid method for report handler")
   856  		http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
   857  		return
   858  	}
   859  	body, err := ioutil.ReadAll(r.Body)
   860  	if err != nil {
   861  		log.Println("Invalid request for report handler")
   862  		http.Error(w, "Invalid method", http.StatusBadRequest)
   863  		return
   864  	}
   865  	defer r.Body.Close()
   866  	var report struct {
   867  		OSArch string
   868  		Ts     *biTestSuite
   869  	}
   870  	err = json.Unmarshal(body, &report)
   871  	if err != nil {
   872  		log.Printf("Could not decode builder report: %v", err)
   873  		http.Error(w, "internal error", http.StatusInternalServerError)
   874  		return
   875  	}
   876  	addTestSuites(report.OSArch, report.Ts)
   877  	fmt.Fprintf(w, "Report ok")
   878  }
   879  
   880  func logHandler(w http.ResponseWriter, r *http.Request) {
   881  	fmt.Fprintln(w, `<!doctype html>
   882  <html>
   883  <body><pre>`)
   884  	switch r.URL.Path {
   885  	case stderrPrefix:
   886  		logStderr.Lock()
   887  		_, err := w.Write(logStderr.Bytes())
   888  		logStderr.Unlock()
   889  		if err != nil {
   890  			log.Println("Error serving logStderr:", err)
   891  		}
   892  	default:
   893  		fmt.Fprintln(w, "Unknown log file path passed to logHandler:", r.URL.Path)
   894  		log.Println("Unknown log file path passed to logHandler:", r.URL.Path)
   895  	}
   896  }
   897  
   898  func okHandler(w http.ResponseWriter, r *http.Request) {
   899  	t := strings.Replace(r.URL.Path, okPrefix, "", -1)
   900  	historylk.Lock()
   901  	defer historylk.Unlock()
   902  	ts, err := getPastTestSuite(t)
   903  	if err != nil || len(ts.Run) == 0 {
   904  		http.NotFound(w, r)
   905  		return
   906  	}
   907  	lastTask := ts.Run[len(ts.Run)-1]
   908  	lastModTime := lastTask.Start.Add(lastTask.Duration)
   909  	if checkLastModified(w, r, lastModTime) {
   910  		return
   911  	}
   912  	var dat struct {
   913  		BiTs [2]*testSuite
   914  	}
   915  	if ts.IsTip {
   916  		dat.BiTs[1] = ts
   917  	} else {
   918  		dat.BiTs[0] = ts
   919  	}
   920  	err = testSuiteTpl.Execute(w, &dat)
   921  	if err != nil {
   922  		log.Printf("ok template: %v\n", err)
   923  	}
   924  }
   925  
   926  func progressHandler(w http.ResponseWriter, r *http.Request) {
   927  	if inProgress == nil {
   928  		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
   929  		return
   930  	}
   931  	// We only display the progress link and ask for progress for
   932  	// our local builder. The remote ones simply send their full report
   933  	// when they're done.
   934  	resp, err := http.Get("http://localhost:" + *builderPort + "/progress")
   935  	if err != nil {
   936  		log.Printf("Could not get a progress response from builder: %v", err)
   937  		http.Error(w, "internal error", http.StatusInternalServerError)
   938  		return
   939  	}
   940  	defer resp.Body.Close()
   941  	body, err := ioutil.ReadAll(resp.Body)
   942  	if err != nil {
   943  		log.Printf("Could not read progress response from builder: %v", err)
   944  		http.Error(w, "internal error", http.StatusInternalServerError)
   945  		return
   946  	}
   947  	var ts biTestSuite
   948  	err = json.Unmarshal(body, &ts)
   949  	if err != nil {
   950  		log.Printf("Could not decode builder progress report: %v", err)
   951  		http.Error(w, "internal error", http.StatusInternalServerError)
   952  		return
   953  	}
   954  	lastModified = time.Now()
   955  	var dat struct {
   956  		BiTs [2]*testSuite
   957  	}
   958  	dat.BiTs[0] = ts.Go1
   959  	if ts.GoTip != nil && !ts.GoTip.Start.IsZero() {
   960  		dat.BiTs[1] = ts.GoTip
   961  	}
   962  	err = testSuiteTpl.Execute(w, &dat)
   963  	if err != nil {
   964  		log.Printf("progress template: %v\n", err)
   965  	}
   966  }
   967  
   968  func failHandler(w http.ResponseWriter, r *http.Request) {
   969  	t := strings.Replace(r.URL.Path, failPrefix, "", -1)
   970  	historylk.Lock()
   971  	defer historylk.Unlock()
   972  	ts, err := getPastTestSuite(t)
   973  	if err != nil || len(ts.Run) == 0 {
   974  		http.NotFound(w, r)
   975  		return
   976  	}
   977  	var failedTask *task
   978  	for _, v := range ts.Run {
   979  		if v.Err != "" {
   980  			failedTask = v
   981  			break
   982  		}
   983  	}
   984  	if failedTask == nil {
   985  		http.NotFound(w, r)
   986  		return
   987  	}
   988  	lastModTime := failedTask.Start.Add(failedTask.Duration)
   989  	if checkLastModified(w, r, lastModTime) {
   990  		return
   991  	}
   992  	failReport := struct {
   993  		TaskErr string
   994  		TsErr   string
   995  	}{
   996  		TaskErr: failedTask.String() + "\n" + failedTask.Err,
   997  		TsErr:   ts.Err,
   998  	}
   999  	err = taskTpl.Execute(w, &failReport)
  1000  	if err != nil {
  1001  		log.Printf("fail template: %v\n", err)
  1002  	}
  1003  }
  1004  
  1005  // unprotected read to history, caller needs to lock.
  1006  func invertedHistory(OSArch string) (inverted []*biTestSuite) {
  1007  	historyOSArch, ok := history[OSArch]
  1008  	if !ok {
  1009  		return nil
  1010  	}
  1011  	inverted = make([]*biTestSuite, len(historyOSArch))
  1012  	endpos := len(historyOSArch) - 1
  1013  	for k, v := range historyOSArch {
  1014  		inverted[endpos-k] = v
  1015  	}
  1016  	return inverted
  1017  }
  1018  
  1019  type statusReport struct {
  1020  	OSArch   string
  1021  	Hs       []*biTestSuite
  1022  	Progress *testSuite
  1023  }
  1024  
  1025  func statusHandler(w http.ResponseWriter, r *http.Request) {
  1026  	historylk.Lock()
  1027  	inProgresslk.Lock()
  1028  	defer inProgresslk.Unlock()
  1029  	defer historylk.Unlock()
  1030  	var localOne *statusReport
  1031  	if inProgress != nil {
  1032  		localOne = &statusReport{
  1033  			Progress: &testSuite{
  1034  				Start:     inProgress.Start,
  1035  				CamliHash: inProgress.CamliHash,
  1036  				GoHash:    inProgress.GoHash,
  1037  			},
  1038  		}
  1039  	}
  1040  	var reports []*statusReport
  1041  	for OSArch, historyOSArch := range history {
  1042  		if len(historyOSArch) == 0 {
  1043  			continue
  1044  		}
  1045  		hs := invertedHistory(OSArch)
  1046  		if historyOSArch[0].Local {
  1047  			if localOne == nil {
  1048  				localOne = &statusReport{}
  1049  			}
  1050  			localOne.OSArch = OSArch
  1051  			localOne.Hs = hs
  1052  			continue
  1053  		}
  1054  		reports = append(reports, &statusReport{
  1055  			OSArch: OSArch,
  1056  			Hs:     hs,
  1057  		})
  1058  	}
  1059  	if localOne != nil {
  1060  		reports = append([]*statusReport{localOne}, reports...)
  1061  	}
  1062  	if checkLastModified(w, r, lastModified) {
  1063  		return
  1064  	}
  1065  	err := statusTpl.Execute(w, reports)
  1066  	if err != nil {
  1067  		log.Printf("status template: %v\n", err)
  1068  	}
  1069  }
  1070  
  1071  // shortHash returns a short version of a hash.
  1072  func shortHash(hash string) string {
  1073  	if len(hash) > 12 {
  1074  		hash = hash[:12]
  1075  	}
  1076  	return hash
  1077  }
  1078  
  1079  func goRepoURL(hash string) string {
  1080  	return "https://code.google.com/p/go/source/detail?r=" + hash
  1081  }
  1082  
  1083  func camliRepoURL(hash string) string {
  1084  	return "https://camlistore.googlesource.com/camlistore/+/" + hash
  1085  }
  1086  
  1087  // style inspired from $GOROOT/misc/dashboard/app/build/ui.html
  1088  var styleHTML = `
  1089  <style>
  1090  	body {
  1091  		font-family: sans-serif;
  1092  		padding: 0; margin: 0;
  1093  	}
  1094  	h1, h2 {
  1095  		margin: 0;
  1096  		padding: 5px;
  1097  	}
  1098  	h1 {
  1099  		background: #eee;
  1100  	}
  1101  	h2 {
  1102  		margin-top: 20px;
  1103  	}
  1104  	.build, .packages {
  1105  		margin: 5px;
  1106  		border-collapse: collapse;
  1107  	}
  1108  	.build td, .build th, .packages td, .packages th {
  1109  		vertical-align: top;
  1110  		padding: 2px 4px;
  1111  		font-size: 10pt;
  1112  	}
  1113  	.build tr.commit:nth-child(2n) {
  1114  		background-color: #f0f0f0;
  1115  	}
  1116  	.build .hash {
  1117  		font-family: monospace;
  1118  		font-size: 9pt;
  1119  	}
  1120  	.build .result {
  1121  		text-align: center;
  1122  		width: 2em;
  1123  	}
  1124  	.col-hash, .col-result {
  1125  		border-right: solid 1px #ccc;
  1126  	}
  1127  	.build .arch {
  1128  		font-size: 66%;
  1129  		font-weight: normal;
  1130  	}
  1131  	.build .time {
  1132  		color: #666;
  1133  	}
  1134  	.build .ok {
  1135  		font-size: 83%;
  1136  	}
  1137  	a.ok {
  1138  		text-decoration:none;
  1139  	}
  1140  	.build .desc, .build .time, .build .user {
  1141  		white-space: nowrap;
  1142  	}
  1143  	.paginate {
  1144  		padding: 0.5em;
  1145  	}
  1146  	.paginate a {
  1147  		padding: 0.5em;
  1148  		background: #eee;
  1149  		color: blue;
  1150  	}
  1151  	.paginate a.inactive {
  1152  		color: #999;
  1153  	}
  1154  	.pull-right {
  1155  		float: right;
  1156  	}
  1157  	.fail {
  1158  		color: #C00;
  1159  	}
  1160  </style>
  1161  `
  1162  
  1163  var statusHTML = `
  1164  <!DOCTYPE HTML>
  1165  <html>
  1166  	<head>
  1167  		<title>Camlistore tests Dashboard</title>` +
  1168  	styleHTML + `
  1169  	</head>
  1170  	<body>
  1171  
  1172  	<h1>Camlibot status<span class="pull-right"><a href="` + stderrPrefix + `">stderr</a></span></h1>
  1173  
  1174  	<table class="build">
  1175  	<colgroup class="col-hash" span="1"></colgroup>
  1176  	<colgroup class="build" span="1"></colgroup>
  1177  	<colgroup class="build" span="1"></colgroup>
  1178  	<colgroup class="user" span="1"></colgroup>
  1179  	<colgroup class="user" span="1"></colgroup>
  1180  	<tr>
  1181  	<!-- extra row to make alternating colors use dark for first result -->
  1182  	</tr>
  1183  	{{range $report := .}}
  1184  	<tr>
  1185  	<th>{{$report.OSArch}}</th>
  1186  	<th colspan="1">Go tip hash</th>
  1187  	<th colspan="1">Camli HEAD hash</th>
  1188  	<th colspan="1">Go1</th>
  1189  	<th colspan="1">Gotip</th>
  1190  	</tr>
  1191  	{{if $report.Progress}}
  1192  		<tr class="commit">
  1193  			<td class="hash">{{$report.Progress.Start}}</td>
  1194  			<td class="hash">
  1195  				<a href="{{goRepoURL $report.Progress.GoHash}}">{{shortHash $report.Progress.GoHash}}</a>
  1196  			</td>
  1197  			<td class="hash">
  1198  				<a href="{{camliRepoURL $report.Progress.CamliHash}}">{{shortHash $report.Progress.CamliHash}}</a>
  1199  			</td>
  1200  			<td class="result" colspan="2">
  1201  				<a href="` + progressPrefix + `" class="ok">In progress</a>
  1202  			</td>
  1203  		</tr>
  1204  	{{end}}
  1205  	{{if $report.Hs}}
  1206  		{{range $bits := $report.Hs}}
  1207  			<tr class="commit">
  1208  				<td class="hash">{{$bits.Go1.Start}}</td>
  1209  				<td class="hash">
  1210  					<a href="{{goRepoURL $bits.Go1.GoHash}}">{{shortHash $bits.Go1.GoHash}}</a>
  1211  				</td>
  1212  				<td class="hash">
  1213  					<a href="{{camliRepoURL $bits.Go1.CamliHash}}">{{shortHash $bits.Go1.CamliHash}}</a>
  1214  				</td>
  1215  				<td class="result">
  1216  				{{if $bits.Go1.Err}}
  1217  					<a href="` + failPrefix + `{{$report.OSArch}}/go1/{{$bits.Go1.Start}}" class="fail">fail</a>
  1218  				{{else}}
  1219  					<a href="` + okPrefix + `{{$report.OSArch}}/go1/{{$bits.Go1.Start}}" class="ok">ok</a>
  1220  				{{end}}
  1221  				</td>
  1222  				<td class="result">
  1223  				{{if $bits.GoTip}}
  1224  					{{if $bits.GoTip.Err}}
  1225  						<a href="` + failPrefix + `{{$report.OSArch}}/gotip/{{$bits.GoTip.Start}}" class="fail">fail</a>
  1226  					{{else}}
  1227  						<a href="` + okPrefix + `{{$report.OSArch}}/gotip/{{$bits.GoTip.Start}}" class="ok">ok</a>
  1228  					{{end}}
  1229  				{{else}}
  1230  					<a href="` + currentPrefix + `" class="ok">In progress</a>
  1231  				{{end}}
  1232  				</td>
  1233  			</tr>
  1234  		{{end}}
  1235  	{{end}}
  1236  	<tr>
  1237  	<td colspan="5">&nbsp;</td>
  1238  	</tr>
  1239  	{{end}}
  1240  	</table>
  1241  
  1242  	</body>
  1243  </html>
  1244  `
  1245  
  1246  var testSuiteHTML = `
  1247  <!DOCTYPE HTML>
  1248  <html>
  1249  	<head>
  1250  		<title>Camlistore tests Dashboard</title>` +
  1251  	styleHTML + `
  1252  	</head>
  1253  	<body>
  1254  	{{range $ts := .BiTs}}
  1255  		{{if $ts}}
  1256  		<h2> Testsuite for {{if $ts.IsTip}}Go tip{{else}}Go 1{{end}} at {{$ts.Start}} </h2>
  1257  		<table class="build">
  1258  		<colgroup class="col-result" span="1"></colgroup>
  1259  		<colgroup class="col-result" span="1"></colgroup>
  1260  		<tr>
  1261  			<!-- extra row to make alternating colors use dark for first result -->
  1262  		</tr>
  1263  		<tr>
  1264  			<th colspan="1">Step</th>
  1265  			<th colspan="1">Duration</th>
  1266  		</tr>
  1267  		{{range $k, $tsk := $ts.Run}}
  1268  		<tr>
  1269  			<td>{{printf "%v" $tsk}}</td>
  1270  			<td>{{$tsk.Duration}}</td>
  1271  		</tr>
  1272  		{{end}}
  1273  		</table>
  1274  		{{end}}
  1275  	{{end}}
  1276  	</body>
  1277  </html>
  1278  `
  1279  
  1280  var taskHTML = `
  1281  <!DOCTYPE HTML>
  1282  <html>
  1283  	<head>
  1284  		<title>Camlistore tests Dashboard</title>
  1285  	</head>
  1286  	<body>
  1287  {{if .TaskErr}}
  1288  	<h2>Task:</h2>
  1289  	<pre>{{.TaskErr}}</pre>
  1290  {{end}}
  1291  {{if .TsErr}}
  1292  	<h2>Error:</h2>
  1293  	<pre>
  1294  	{{.TsErr}}
  1295  	</pre>
  1296  {{end}}
  1297  	</body>
  1298  </html>
  1299  `