dashboard/env: add go-commit-watcher image

LGTM=bradfitz
R=bradfitz, adg
CC=adg, golang-codereviews
https://golang.org/cl/179370043
diff --git a/coordinator/main.go b/coordinator/main.go
index a855000..07565d0 100644
--- a/coordinator/main.go
+++ b/coordinator/main.go
@@ -37,6 +37,7 @@
 var (
 	startTime = time.Now()
 	builders  = map[string]buildConfig{} // populated once at startup
+	watchers  = map[string]watchConfig{} // populated once at startup
 	donec     = make(chan builderRev)    // reports of finished builders
 
 	statusMu sync.Mutex
@@ -51,6 +52,7 @@
 }
 
 var images = map[string]*imageInfo{
+	"go-commit-watcher":          {url: "https://storage.googleapis.com/go-builder-data/docker-commit-watcher.tar.gz"},
 	"gobuilders/linux-x86-base":  {url: "https://storage.googleapis.com/go-builder-data/docker-linux.base.tar.gz"},
 	"gobuilders/linux-x86-clang": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.clang.tar.gz"},
 	"gobuilders/linux-x86-gccgo": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.gccgo.tar.gz"},
@@ -67,6 +69,12 @@
 	tool    string   // the tool this configuration is for
 }
 
+type watchConfig struct {
+	repo     string        // "https://code.google.com/p/go"
+	dash     string        // "https://build.golang.org/" (must end in /)
+	interval time.Duration // Polling interval
+}
+
 func main() {
 	flag.Parse()
 	addBuilder(buildConfig{name: "linux-386"})
@@ -96,6 +104,9 @@
 	addBuilder(buildConfig{name: "linux-386-clang", image: "gobuilders/linux-x86-clang"})
 	addBuilder(buildConfig{name: "linux-amd64-clang", image: "gobuilders/linux-x86-clang"})
 
+	addWatcher(watchConfig{repo: "https://code.google.com/p/go", dash: "https://build.golang.org/"})
+	addWatcher(watchConfig{repo: "https://code.google.com/p/gofrontend", dash: "https://build.golang.org/gccgo/"})
+
 	if (*just != "") != (*rev != "") {
 		log.Fatalf("--just and --rev must be used together")
 	}
@@ -117,6 +128,12 @@
 	http.HandleFunc("/logs", handleLogs)
 	go http.ListenAndServe(":80", nil)
 
+	for _, watcher := range watchers {
+		if err := startWatching(watchers[watcher.repo]); err != nil {
+			log.Printf("Error starting watcher for %s: %v", watcher.repo, err)
+		}
+	}
+
 	workc := make(chan builderRev)
 	for name, builder := range builders {
 		go findWorkLoop(name, builder.dashURL, workc)
@@ -129,10 +146,7 @@
 			log.Printf("workc received %+v; len(status) = %v, maxBuilds = %v; cur = %p", work, len(status), *maxBuilds, status[work])
 			mayBuild := mayBuildRev(work)
 			if mayBuild {
-				out, _ := exec.Command("docker", "ps").Output()
-				numBuilds := bytes.Count(out, []byte("\n")) - 1
-				log.Printf("num current docker builds: %d", numBuilds)
-				if numBuilds > *maxBuilds {
+				if numBuilds() > *maxBuilds {
 					mayBuild = false
 				}
 			}
@@ -333,6 +347,40 @@
 	builders[c.name] = c
 }
 
+// returns the part after "docker run"
+func (conf watchConfig) dockerRunArgs() (args []string) {
+	if key := builderKey("watcher"); key != "" {
+		tmpKey := "/tmp/watcher.buildkey"
+		if _, err := os.Stat(tmpKey); err != nil {
+			if err := ioutil.WriteFile(tmpKey, []byte(key), 0600); err != nil {
+				log.Fatal(err)
+			}
+		}
+		args = append(args, "-v", tmpKey+":/.gobuildkey")
+	}
+	args = append(args,
+		"go-commit-watcher",
+		"/usr/local/bin/watcher",
+		"-repo="+conf.repo,
+		"-dash="+conf.dash,
+		"-poll="+conf.interval.String(),
+	)
+	return
+}
+
+func addWatcher(c watchConfig) {
+	if c.repo == "" {
+		c.repo = "https://code.google.com/p/go"
+	}
+	if c.dash == "" {
+		c.dash = "https://build.golang.org/"
+	}
+	if c.interval == 0 {
+		c.interval = 10 * time.Second
+	}
+	watchers[c.repo] = c
+}
+
 func condUpdateImage(img string) error {
 	ii := images[img]
 	if ii == nil {
@@ -373,6 +421,20 @@
 	return nil
 }
 
+// numBuilds finds the number of go builder instances currently running.
+func numBuilds() int {
+	out, _ := exec.Command("docker", "ps").Output()
+	numBuilds := 0
+	ps := bytes.Split(out, []byte("\n"))
+	for _, p := range ps {
+		if bytes.HasPrefix(p, []byte("gobuilders/")) {
+			numBuilds++
+		}
+	}
+	log.Printf("num current docker builds: %d", numBuilds)
+	return numBuilds
+}
+
 func startBuilding(conf buildConfig, rev string) (*buildStatus, error) {
 	if err := condUpdateImage(conf.image); err != nil {
 		log.Printf("Failed to setup container for %v %v: %v", conf.name, rev, err)
@@ -411,6 +473,18 @@
 	// ...
 }
 
+func startWatching(conf watchConfig) error {
+	if err := condUpdateImage("go-commit-watcher"); err != nil {
+		log.Printf("Failed to setup container for commit watcher: %v", err)
+		return err
+	}
+
+	cmd := exec.Command("docker", append([]string{"run", "-d"}, conf.dockerRunArgs()...)...)
+	all, err := cmd.CombinedOutput()
+	log.Printf("Docker run for commit watcher = err:%v, output: %s", err, all)
+	return err
+}
+
 func builderKey(builder string) string {
 	master := masterKey()
 	if len(master) == 0 {