/* Copyright 2012 The Camlistore Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // The buildbot is Camlistore's continuous builder. // This builder program is started by the master. It then rebuilds // Go 1, GoTip, Camlistore, and runs a battery of tests for Camlistore. // It then sends a report to the master and terminates. // It can also respond to progress requests from the master. package main import ( "bufio" "bytes" "crypto/tls" "encoding/json" "errors" "flag" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "os" "os/exec" "os/signal" "path/filepath" "regexp" "runtime" "strings" "sync" "syscall" "time" "camlistore.org/pkg/osutil" ) const ( interval = 60 * time.Second // polling frequency warmup = 30 * time.Second // duration before we test if devcam server has started properly ) var ( // TODO(mpl): use that one, same as in master. 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.") arch = flag.String("arch", "", "The arch we report the master(s). Defaults to runtime.GOARCH.") fakeTests = flag.Bool("faketests", false, "Run fast fake tests instead of the real ones, for faster debugging.") help = flag.Bool("h", false, "show this help") host = flag.String("host", "0.0.0.0:8081", "listening hostname and port") masterHosts = flag.String("masterhosts", "localhost:8080", "listening hostname and port of the master bots, i.e where to send the test suite reports. Comma separated list.") ourOS = flag.String("os", "", "The OS we report the master(s). Defaults to runtime.GOOS.") skipGo1Build = flag.Bool("skipgo1build", false, "skip initial go1 build, for debugging and quickly going to the next steps.") verbose = flag.Bool("verbose", false, "print what's going on") skipTLSCheck = flag.Bool("skiptlscheck", false, "accept any certificate presented by server when uploading results.") taskLifespan = flag.Int("timeout", 600, "Lifespan (in seconds) for each task run by this builder, after which the task automatically terminates. 0 or negative means infinite.") ) var ( testFile = []string{"AUTHORS", "CONTRIBUTORS"} cacheDir string camliHeadHash string camliRoot string camputCacheDir string client = http.DefaultClient dbg *debugger defaultPATH string go1Dir string goTipDir string goTipHash string biSuitelk sync.Mutex currentTestSuite *testSuite currentBiSuite *biTestSuite // Process of the camlistore server, so we can kill it when // we get killed ourselves. camliProc *os.Process // For "If-Modified-Since" requests asking for progress. // Updated every time a new test task/run is added to the test suite. lastModified time.Time ) var devcamBin = filepath.Join("bin", "devcam") var ( hgCloneGo1Cmd = newTask("hg", "clone", "-u", "release", "https://code.google.com/p/go") hgCloneGoTipCmd = newTask("hg", "clone", "-u", "tip", "https://code.google.com/p/go") hgPullCmd = newTask("hg", "pull") hgUpdateCmd = newTask("hg", "update", "-C", "default") hgLogCmd = newTask("hg", "log", "-r", "tip", "--template", "{node}") hgConfigCmd = newTask("hg", "--config", "extensions.purge=", "purge", "--all") gitCloneCmd = newTask("git", "clone", "https://camlistore.googlesource.com/camlistore") gitResetCmd = newTask("git", "reset", "--hard") gitCleanCmd = newTask("git", "clean", "-Xdf") gitPullCmd = newTask("git", "pull") gitRevCmd = newTask("git", "rev-parse", "HEAD") buildGoCmd = newTask("./make.bash") buildCamliCmd = newTask("go", "run", "make.go", "-v") runTestsCmd = newTask(devcamBin, "test") runCamliCmd = newTask(devcamBin, "server", "--wipe", "--mysql") camgetCmd = newTask(devcamBin, "get") camputCmd = newTask(devcamBin, "put", "file", "--permanode", testFile[0]) camputVivifyCmd = newTask(devcamBin, "put", "file", "--vivify", testFile[1]) camputFilenodesCmd = newTask(devcamBin, "put", "file", "--filenodes", "pkg") ) func usage() { fmt.Fprintf(os.Stderr, "\t builderBot \n") flag.PrintDefaults() os.Exit(2) } type debugger struct { lg *log.Logger } func (dbg *debugger) Printf(format string, v ...interface{}) { if dbg != nil && *verbose { dbg.lg.Printf(format, v...) } } func (dbg *debugger) Println(v ...interface{}) { if v == nil { return } if dbg != nil && *verbose { dbg.lg.Println(v...) } } type task struct { Program string Args []string Start time.Time Duration time.Duration Err string hidden bool } func newTask(program string, args ...string) *task { return &task{Program: program, Args: args} } // because sometimes we do not want to modify the tsk template // so we make a copy of it func newTaskFrom(tsk *task) *task { return newTask(tsk.Program, tsk.Args...) } func (t *task) String() string { return fmt.Sprintf("%v %v", t.Program, t.Args) } func (t *task) Error() string { return t.Err } func (t *task) run() (string, error) { var err error defer func() { t.Duration = time.Now().Sub(t.Start) if !t.hidden { biSuitelk.Lock() currentTestSuite.addRun(t) biSuitelk.Unlock() } }() dbg.Println(t.String()) cmd := exec.Command(t.Program, t.Args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr t.Start = time.Now() setTaskErr := func() { var sout, serr string if sout = stdout.String(); sout == "" { sout = "(empty)" } if serr = stderr.String(); serr == "" { serr = "(empty)" } t.Err = fmt.Sprintf("Stdout:\n%s\n\nStderr:\n%s", sout, serr) if err != nil { t.Err = fmt.Sprintf("%v\n\n%v", err, t.Err) } } // TODO(mpl, wathiede): make it learn about task durations. errc := make(chan error) go func() { errc <- cmd.Run() }() if *taskLifespan > 0 { select { case <-time.After(time.Duration(*taskLifespan) * time.Second): setTaskErr() t.Err = fmt.Sprintf("%v\n\nTask %q took too long. Giving up after %v seconds.\n", t.Err, t.String(), *taskLifespan) if cmd.Process != nil { if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { dbg.Printf("Could not terminate process for task %q: %v", t.String(), err) } } return "", t case err = <-errc: break } } else { err = <-errc } if err != nil { setTaskErr() return "", t } return stdout.String(), nil } type testSuite struct { Run []*task CamliHash string GoHash string Err string Start time.Time IsTip bool } func (ts *testSuite) addRun(tsk *task) { if ts == nil { panic("Tried adding a run to a nil testSuite") } if ts.Start.IsZero() && len(ts.Run) == 0 { ts.Start = tsk.Start } if tsk.Err != "" && ts.Err == "" { ts.Err = tsk.Err } ts.Run = append(ts.Run, tsk) lastModified = time.Now() } type biTestSuite struct { Local bool Go1 testSuite GoTip testSuite } func main() { flag.Usage = usage flag.Parse() if *help { usage() } if *skipTLSCheck { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client = &http.Client{Transport: tr} } go handleSignals() http.HandleFunc("/progress", progressHandler) go func() { log.Printf("Now starting to listen on %v", *host) if err := http.ListenAndServe(*host, nil); err != nil { log.Fatalf("Could not start listening on %v: %v", *host, err) } }() setup() biSuitelk.Lock() currentBiSuite = &biTestSuite{} biSuitelk.Unlock() for _, isTip := range [2]bool{false, true} { currentTestSuite = &testSuite{ Run: make([]*task, 0, 1), IsTip: isTip, Start: time.Now(), } // We prepare the Go tip tree as soon as in the Go 1 run, so // we can set GoTipHash in the test suite. if !isTip { if err := prepGoTipTree(); err != nil { endOfSuite(err) // If we failed with that in the Go 1 run, we just restart // from scratch instead of trying to cope with it in the Gotip run. // Same for buildGoTip and prepCamliTree. break } } biSuitelk.Lock() currentTestSuite.GoHash = goTipHash biSuitelk.Unlock() if isTip && !*fakeTests { if err := buildGoTip(); err != nil { endOfSuite(err) break } } if err := prepCamliTree(isTip); err != nil { endOfSuite(err) break } biSuitelk.Lock() currentTestSuite.CamliHash = camliHeadHash biSuitelk.Unlock() restorePATH() goDir := go1Dir if isTip { goDir = goTipDir } switchGo(goDir) if *fakeTests { if err := fakeRun(); err != nil { endOfSuite(err) continue } endOfSuite(nil) if isTip { break } continue } if err := buildCamli(); err != nil { endOfSuite(err) continue } if err := runTests(); err != nil { endOfSuite(err) continue } if err := runCamli(); err != nil { endOfSuite(err) continue } if err := hitCamliUi(); err != nil { endOfSuite(err) continue } doVivify := false if err := camputOne(doVivify); err != nil { endOfSuite(err) continue } doVivify = true if err := camputOne(doVivify); err != nil { endOfSuite(err) continue } if err := camputMany(); err != nil { endOfSuite(err) continue } endOfSuite(nil) } sanitizeRevs() sendReport() } func sanitizeRevs() { if currentBiSuite == nil { return } if currentBiSuite.GoTip.Start.IsZero() { return } if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" { dbg.Printf("CamliHash not set in both Go1 and GoTip test suites") return } if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" { dbg.Printf("GoHash not set in both Go1 and GoTip test suites") return } if currentBiSuite.GoTip.CamliHash != "" && currentBiSuite.Go1.CamliHash != "" && currentBiSuite.GoTip.CamliHash != currentBiSuite.Go1.CamliHash { panic("CamliHash in GoTip suite and in Go1 suite differ; should not happen.") } if currentBiSuite.GoTip.GoHash != "" && currentBiSuite.Go1.GoHash != "" && currentBiSuite.GoTip.GoHash != currentBiSuite.Go1.GoHash { panic("GoHash in GoTip suite and in Go1 suite differ; should not happen.") } if currentBiSuite.GoTip.GoHash == "" { currentBiSuite.GoTip.GoHash = currentBiSuite.Go1.GoHash } if currentBiSuite.Go1.GoHash == "" { currentBiSuite.Go1.GoHash = currentBiSuite.GoTip.GoHash } if currentBiSuite.GoTip.CamliHash == "" { currentBiSuite.GoTip.CamliHash = currentBiSuite.Go1.CamliHash } if currentBiSuite.Go1.CamliHash == "" { currentBiSuite.Go1.CamliHash = currentBiSuite.GoTip.CamliHash } } func endOfSuite(err error) { biSuitelk.Lock() defer biSuitelk.Unlock() if currentTestSuite.IsTip { currentBiSuite.GoTip = *currentTestSuite } else { currentBiSuite.Go1 = *currentTestSuite } killCamli() if err != nil { log.Printf("%v", err) } else { dbg.Println("All good.") } } func masterHostsReader(r io.Reader) ([]string, error) { hosts := []string{} scanner := bufio.NewScanner(r) for scanner.Scan() { l := scanner.Text() u, err := url.Parse(l) if err != nil { return nil, err } if u.Host == "" { return nil, fmt.Errorf("URL missing Host: %q", l) } hosts = append(hosts, u.String()) } if err := scanner.Err(); err != nil { return nil, err } return hosts, nil } var masterHostsFile = filepath.Join(osutil.CamliConfigDir(), "builderbot-config") func loadMasterHosts() error { r, err := os.Open(masterHostsFile) if err != nil { return err } defer r.Close() hosts, err := masterHostsReader(r) if err != nil { return err } if *masterHosts != "" { *masterHosts += "," } log.Println("Additional host(s) to send our build reports:", hosts) *masterHosts += strings.Join(hosts, ",") return nil } func setup() { var err error defaultPATH = os.Getenv("PATH") if defaultPATH == "" { log.Fatal("PATH not set") } log.SetPrefix("BUILDER: ") dbg = &debugger{log.New(os.Stderr, "BUILDER: ", log.LstdFlags)} err = loadMasterHosts() if err != nil { if os.IsNotExist(err) { log.Printf("%q missing. No additional remote master(s) will receive build report.", masterHostsFile) } else { log.Println("Error parsing master hosts file %q: %v", masterHostsFile, err) } } // the OS we run on if *ourOS == "" { *ourOS = runtime.GOOS if *ourOS == "" { // Can this happen? I don't think so, but just in case... panic("runtime.GOOS was not set") } } // the arch we run on if *arch == "" { *arch = runtime.GOARCH if *arch == "" { panic("runtime.GOARCH was not set") } } // cacheDir cacheDir = filepath.Join(os.TempDir(), "camlibot-cache") if err := os.MkdirAll(cacheDir, 0755); err != nil { log.Fatalf("Could not create cache dir %v: %v", cacheDir, err) } // get go1 and gotip source if err := os.Chdir(cacheDir); err != nil { log.Fatalf("Could not cd to %v: %v", cacheDir, err) } go1Dir, err = filepath.Abs("go1") if err != nil { log.Fatalf("Problem with Go 1 dir: %v", err) } goTipDir, err = filepath.Abs("gotip") if err != nil { log.Fatalf("Problem with Go tip dir: %v", err) } for _, goDir := range []string{go1Dir, goTipDir} { // if go dirs exist, just reuse them if _, err := os.Stat(goDir); err != nil { if !os.IsNotExist(err) { log.Fatalf("Could not stat %v: %v", goDir, err) } // go1/gotip dir not here, let's clone it. hgCloneCmd := hgCloneGo1Cmd if goDir == goTipDir { hgCloneCmd = hgCloneGoTipCmd } tsk := newTask(hgCloneCmd.Program, hgCloneCmd.Args...) tsk.hidden = true if _, err := tsk.run(); err != nil { log.Fatalf("Could not hg clone %v: %v", goDir, err) } if err := os.Rename("go", goDir); err != nil { log.Fatalf("Could not rename go dir into %v: %v", goDir, err) } } } if !*skipGo1Build { // build Go1 if err := buildGo1(); err != nil { log.Fatal(err) } } // get camlistore source if err := os.Chdir(cacheDir); err != nil { log.Fatal("Could not cd to %v: %v", cacheDir, err) } camliRoot, err = filepath.Abs("camlistore.org") if err != nil { log.Fatal(err) } // if camlistore dir already exists, reuse it if _, err := os.Stat(camliRoot); err != nil { if !os.IsNotExist(err) { log.Fatalf("Could not stat %v: %v", camliRoot, err) } cloneCmd := newTask(gitCloneCmd.Program, append(gitCloneCmd.Args, camliRoot)...) cloneCmd.hidden = true if _, err := cloneCmd.run(); err != nil { log.Fatalf("Could not git clone into %v: %v", camliRoot, err) } } // recording camput cache dir, so we can clean it up fast everytime homeDir := os.Getenv("HOME") if homeDir == "" { log.Fatal("HOME not set") } camputCacheDir = filepath.Join(homeDir, ".cache", "camlistore") } func buildGo1() error { if err := os.Chdir(filepath.Join(go1Dir, "src")); err != nil { log.Fatalf("Could not cd to %v: %v", go1Dir, err) } tsk := newTask(buildGoCmd.Program, buildGoCmd.Args...) tsk.hidden = true if _, err := tsk.run(); err != nil { return err } return nil } func handleSignals() { c := make(chan os.Signal) sigs := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT} signal.Notify(c, sigs...) for { sig := <-c sysSig, ok := sig.(syscall.Signal) if !ok { log.Fatal("Not a unix signal") } switch sysSig { case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: log.Printf("Received %v signal, terminating.", sig) killCamli() os.Exit(0) default: panic("should not get other signals here") } } } var plausibleHashRx = regexp.MustCompile(`^[a-f0-9]{40}$`) func prepGoTipTree() error { if err := os.Chdir(goTipDir); err != nil { return fmt.Errorf("Could not cd to %v: %v", goTipDir, err) } tasks := []*task{ newTaskFrom(hgPullCmd), newTaskFrom(hgUpdateCmd), newTaskFrom(hgLogCmd), newTaskFrom(hgConfigCmd), } hash := "" for _, t := range tasks { out, err := t.run() if err != nil { return fmt.Errorf("Could not prepare the Go tip tree with %v: %v", t.String(), err) } if t.String() == hgLogCmd.String() { hash = strings.TrimRight(out, "\n") } } if !plausibleHashRx.MatchString(hash) { return fmt.Errorf("Go rev %q does not look like an hg hash.", hash) } goTipHash = hash dbg.Println("current head in go tree: " + goTipHash) return nil } func buildGoTip() error { srcDir := filepath.Join(goTipDir, "src") if err := os.Chdir(srcDir); err != nil { return fmt.Errorf("Could not cd to %v: %v", srcDir, err) } if _, err := newTaskFrom(buildGoCmd).run(); err != nil { return err } return nil } func prepCamliTree(isTip bool) error { if err := os.Chdir(camliRoot); err != nil { return fmt.Errorf("Could not cd to %v: %v", camliRoot, err) } rev := "HEAD" if isTip { if !plausibleHashRx.MatchString(camliHeadHash) { // the run with Go 1 should have taken care of setting camliHeadHash return errors.New("camliHeadHash hasn't been set properly in the Go 1 run") } // we reset to the rev that was noted at the previous run with Go 1 // because we want to do both runs at the same rev rev = camliHeadHash } resetCmd := newTask(gitResetCmd.Program, append(gitResetCmd.Args, rev)...) tasks := []*task{ resetCmd, newTaskFrom(gitCleanCmd), } for _, t := range tasks { _, err := t.run() if err != nil { return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err) } } if isTip { // We only need to pull and get the camli head hash when in the Go 1 run return nil } tasks = []*task{ newTaskFrom(gitPullCmd), newTaskFrom(gitRevCmd), } hash := "" for _, t := range tasks { out, err := t.run() if err != nil { return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err) } hash = strings.TrimRight(out, "\n") } if !plausibleHashRx.MatchString(hash) { return fmt.Errorf("Camlistore rev %q does not look like a git hash.", hash) } camliHeadHash = hash return nil } func restorePATH() { err := os.Setenv("PATH", defaultPATH) if err != nil { log.Fatalf("Could not set PATH to %v: %v", defaultPATH, err) } } func switchGo(goDir string) { if runtime.GOOS == "plan9" { panic("plan 9 not unsupported") } gobin := filepath.Join(goDir, "bin", "go") if _, err := os.Stat(gobin); err != nil { log.Fatalf("Could not stat 'go' bin at %q: %v", gobin, err) } p := filepath.Join(goDir, "bin") + string(filepath.ListSeparator) + defaultPATH if err := os.Setenv("PATH", p); err != nil { log.Fatalf("Could not set PATH to %v: %v", p, err) } if err := os.Setenv("GOROOT", goDir); err != nil { log.Fatalf("Could not set GOROOT to %v: %v", goDir, err) } } func cleanBuildGopaths() { tmpDir := filepath.Join(camliRoot, "tmp") if _, err := os.Stat(tmpDir); err != nil { if !os.IsNotExist(err) { log.Fatalf("Could not stat %v: %v", tmpDir, err) } // Does not exist, we only have to recreate it // TODO(mpl): hmm maybe it should be an error that // it does not exist, since it also contains the // closure stuff? if err := os.MkdirAll(tmpDir, 0755); err != nil { log.Fatalf("Could not mkdir %v: %v", tmpDir, err) } return } f, err := os.Open(tmpDir) if err != nil { log.Fatalf("Could not open %v: %v", tmpDir, err) } defer f.Close() names, err := f.Readdirnames(-1) if err != nil { log.Fatal("Could not read %v: %v", tmpDir, err) } for _, v := range names { if strings.HasPrefix(v, "build-gopath") { if err := os.RemoveAll(filepath.Join(tmpDir, v)); err != nil { log.Fatalf("Could not remove %v: %v", v, err) } } } } func fakeRun() error { if _, err := newTask("sleep", "1").run(); err != nil { return err } return nil } func buildCamli() error { if err := os.Chdir(camliRoot); err != nil { log.Fatalf("Could not cd to %v: %v", camliRoot, err) } // Clean up Camlistore's hermetic gopaths cleanBuildGopaths() if *verbose { tsk := newTask("go", "version") out, err := tsk.run() tsk.hidden = true if err != nil { return fmt.Errorf("failed to run 'go version': %v", err) } out = strings.TrimRight(out, "\n") dbg.Printf("Building Camlistore with: %v\n", out) } if _, err := newTaskFrom(buildCamliCmd).run(); err != nil { return err } return nil } func runCamli() error { if err := os.Chdir(camliRoot); err != nil { log.Fatal(err) } t := newTaskFrom(runCamliCmd) dbg.Println(t.String()) cmd := exec.Command(t.Program, t.Args...) var output []byte errc := make(chan error, 1) t.Start = time.Now() go func() { var err error output, err = cmd.CombinedOutput() if err != nil { err = fmt.Errorf("%v: %v", err, string(output)) } errc <- err }() select { case err := <-errc: t.Err = fmt.Sprintf("%v terminated early:\n%v\n", t.String(), err) biSuitelk.Lock() currentTestSuite.addRun(t) biSuitelk.Unlock() log.Println(t.Err) return t case <-time.After(warmup): biSuitelk.Lock() currentTestSuite.addRun(t) camliProc = cmd.Process biSuitelk.Unlock() dbg.Printf("%v running OK so far\n", t.String()) } return nil } func killCamli() { if camliProc == nil { return } dbg.Println("killing Camlistore server") if err := camliProc.Kill(); err != nil { log.Fatalf("Could not kill server with pid %v: %v", camliProc.Pid, err) } camliProc = nil dbg.Println("") } func hitCamliUi() error { if err := hitURL("http://localhost:3179/ui/"); err != nil { return fmt.Errorf("could not reach camlistored UI page (dead server?): %v", err) } return nil } func hitURL(url string) (err error) { tsk := newTask("http.Get", url) defer func() { if err != nil { tsk.Err = fmt.Sprintf("%v", err) } biSuitelk.Lock() currentTestSuite.addRun(tsk) biSuitelk.Unlock() }() dbg.Println(tsk.String()) tsk.Start = time.Now() var resp *http.Response resp, err = http.Get(url) if err != nil { return fmt.Errorf("%v: %v\n", tsk.String(), err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("%v, got StatusCode: %d\n", tsk.String(), resp.StatusCode) } return nil } func camputOne(vivify bool) error { if err := os.Chdir(camliRoot); err != nil { log.Fatalf("Could not cd to %v: %v", camliRoot, err) } // clean up camput caches if err := os.RemoveAll(camputCacheDir); err != nil { log.Fatalf("Problem cleaning up camputCacheDir %v: %v", camputCacheDir, err) } // push the file to camli tsk := newTaskFrom(camputCmd) if vivify { tsk = newTaskFrom(camputVivifyCmd) } out, err := tsk.run() if err != nil { return err } // TODO(mpl): parsing camput output is kinda weak. firstSHA1 := regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\nsha1-[a-zA-Z0-9]+\nsha1-[a-zA-Z0-9]+\n.*`) if vivify { firstSHA1 = regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\n.*`) } m := firstSHA1.FindStringSubmatch(out) if m == nil { return fmt.Errorf("%v: unexpected camput output\n", tsk.String()) } blobref := m[1] // get the file's json to find out the file's blobref tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...) out, err = tsk.run() if err != nil { return err } blobrefPattern := regexp.MustCompile(`"blobRef": "(sha1-[a-zA-Z0-9]+)",\n.*`) m = blobrefPattern.FindStringSubmatch(out) if m == nil { return fmt.Errorf("%v: unexpected camget output\n", tsk.String()) } blobref = m[1] // finally, get the file back tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...) out, err = tsk.run() if err != nil { return err } // and compare it with the original wantFile := testFile[0] if vivify { wantFile = testFile[1] } fileContents, err := ioutil.ReadFile(wantFile) if err != nil { log.Fatalf("Could not read %v: %v", wantFile, err) } if string(fileContents) != out { return fmt.Errorf("%v: contents fetched with camget differ from %v contents", tsk.String(), wantFile) } return nil } func camputMany() error { err := os.Chdir(camliRoot) if err != nil { log.Fatalf("Could not cd to %v: %v", camliRoot, err) } // upload the full camli pkg tree if _, err := newTaskFrom(camputFilenodesCmd).run(); err != nil { return err } return nil } func runTests() error { if err := os.Chdir(camliRoot); err != nil { log.Fatal(err) } if _, err := newTaskFrom(runTestsCmd).run(); err != nil { return err } return nil } const reportPrefix = "/report" func postToURL(u string, r io.Reader) (*http.Response, error) { // Default to plain HTTP. if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) { u = "http://" + u } uri, err := url.Parse(u) if err != nil { return nil, err } // If the URL explicitly specifies "/" or something else, we'll POST to // that, otherwise default to build-time default. if uri.Path == "" { uri.Path = reportPrefix } // Save user/pass if specified in the URL. user := uri.User // But don't send user/pass in URL to server. uri.User = nil req, err := http.NewRequest("POST", uri.String(), r) if err != nil { return nil, err } req.Header.Set("Content-Type", "text/javascript") // If user/pass set on original URL, set the auth header for the request. if user != nil { pass, ok := user.Password() if !ok { log.Println("Password not set for", user.Username(), "in", u) } req.SetBasicAuth(user.Username(), pass) } return client.Do(req) } func sendReport() { biSuitelk.Lock() // we make a copy so we can release the lock quickly enough currentBiSuiteCpy := &biTestSuite{ Go1: currentBiSuite.Go1, GoTip: currentBiSuite.GoTip, } biSuitelk.Unlock() masters := strings.Split(*masterHosts, ",") OSArch := *ourOS + "_" + *arch toReport := struct { OSArch string Ts *biTestSuite }{ OSArch: OSArch, Ts: currentBiSuiteCpy, } for _, v := range masters { // TODO(mpl): ipv6 too I suppose. just make a IsLocalhost func or whatever. // probably can borrow something from camli code for that. if strings.HasPrefix(v, "localhost") || strings.HasPrefix(v, "127.0.0.1") { toReport.Ts.Local = true } else { toReport.Ts.Local = false } report, err := json.MarshalIndent(toReport, "", " ") if err != nil { log.Printf("JSON serialization error: %v", err) return } r := bytes.NewReader(report) resp, err := postToURL(v, r) if err != nil { log.Printf("Could not send report: %v", err) continue } resp.Body.Close() } } func progressHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { log.Printf("Invalid method in progress handler: %v, want GET", r.Method) http.Error(w, "Invalid method", http.StatusMethodNotAllowed) return } if checkLastModified(w, r, lastModified) { return } biSuitelk.Lock() if currentBiSuite != nil { if currentTestSuite.IsTip { currentBiSuite.GoTip = *currentTestSuite } else { currentBiSuite.Go1 = *currentTestSuite } } sanitizeRevs() report, err := json.MarshalIndent(currentBiSuite, "", " ") if err != nil { biSuitelk.Unlock() log.Printf("JSON serialization error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } biSuitelk.Unlock() _, err = io.Copy(w, bytes.NewReader(report)) if err != nil { log.Printf("Could not send progress report: %v", err) } } // modtime is the modification time of the resource to be served, or IsZero(). // return value is whether this request is now complete. func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool { if modtime.IsZero() { return false } // The Date-Modified header truncates sub-second precision, so // use mtime < t+1s instead of mtime <= t to check for unmodified. if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) { h := w.Header() delete(h, "Content-Type") delete(h, "Content-Length") w.WriteHeader(http.StatusNotModified) return true } w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) return false }