github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/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"> </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 `