golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gitmirror/gitmirror.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The gitmirror binary watches the specified Gerrit repositories for
     6  // new commits and syncs them to mirror repositories.
     7  //
     8  // It also serves tarballs over HTTP for the build system.
     9  package main
    10  
    11  import (
    12  	"bytes"
    13  	"context"
    14  	"errors"
    15  	"flag"
    16  	"fmt"
    17  	"log"
    18  	"net/http"
    19  	"os"
    20  	"os/exec"
    21  	"os/signal"
    22  	"path/filepath"
    23  	"runtime"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"golang.org/x/build/gerrit"
    31  	"golang.org/x/build/internal/envutil"
    32  	"golang.org/x/build/internal/gitauth"
    33  	"golang.org/x/build/internal/secret"
    34  	"golang.org/x/build/maintner"
    35  	"golang.org/x/build/maintner/godata"
    36  	repospkg "golang.org/x/build/repos"
    37  	"golang.org/x/sync/errgroup"
    38  )
    39  
    40  var (
    41  	flagHTTPAddr     = flag.String("http", "", "If non-empty, the listen address to run an HTTP server on")
    42  	flagCacheDir     = flag.String("cachedir", "", "git cache directory. If empty a temp directory is made.")
    43  	flagPollInterval = flag.Duration("poll", 60*time.Second, "Remote repo poll interval")
    44  	flagMirror       = flag.Bool("mirror", false, "whether to mirror to mirror repos; if disabled, it only runs in HTTP archive server mode")
    45  	flagMirrorGitHub = flag.Bool("mirror-github", true, "whether to mirror to GitHub when mirroring is enabled")
    46  	flagMirrorCSR    = flag.Bool("mirror-csr", true, "whether to mirror to Cloud Source Repositories when mirroring is enabled")
    47  	flagSecretsDir   = flag.String("secretsdir", "", "directory to load secrets from instead of GCP")
    48  )
    49  
    50  func main() {
    51  	flag.Parse()
    52  
    53  	if *flagHTTPAddr != "" {
    54  		go func() {
    55  			err := http.ListenAndServe(*flagHTTPAddr, nil)
    56  			log.Fatalf("http server failed: %v", err)
    57  		}()
    58  	}
    59  	http.HandleFunc("/debug/env", handleDebugEnv)
    60  	http.HandleFunc("/debug/goroutines", handleDebugGoroutines)
    61  
    62  	if err := gitauth.Init(); err != nil {
    63  		log.Fatalf("gitauth: %v", err)
    64  	}
    65  
    66  	cacheDir, err := createCacheDir()
    67  	if err != nil {
    68  		log.Fatalf("creating cache dir: %v", err)
    69  	}
    70  	credsDir, err := os.MkdirTemp("", "gitmirror-credentials")
    71  	if err != nil {
    72  		log.Fatalf("creating credentials dir: %v", err)
    73  	}
    74  	defer os.RemoveAll(credsDir)
    75  
    76  	m := &gitMirror{
    77  		mux:          http.DefaultServeMux,
    78  		repos:        map[string]*repo{},
    79  		cacheDir:     cacheDir,
    80  		homeDir:      credsDir,
    81  		goBase:       "https://go.googlesource.com/",
    82  		gerritClient: gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth),
    83  		mirrorGitHub: *flagMirrorGitHub,
    84  		mirrorCSR:    *flagMirrorCSR,
    85  		timeoutScale: 1,
    86  	}
    87  
    88  	var eg errgroup.Group
    89  	for _, repo := range repospkg.ByGerritProject {
    90  		r := m.addRepo(repo)
    91  		eg.Go(r.init)
    92  	}
    93  
    94  	http.HandleFunc("/", m.handleRoot)
    95  	http.HandleFunc("/healthz", m.handleHealth)
    96  
    97  	if err := eg.Wait(); err != nil {
    98  		log.Fatalf("initializing repos: %v", err)
    99  	}
   100  
   101  	if *flagMirror {
   102  		if err := writeCredentials(credsDir); err != nil {
   103  			log.Fatalf("writing git credentials: %v", err)
   104  		}
   105  		if err := m.addMirrors(); err != nil {
   106  			log.Fatalf("configuring mirrors: %v", err)
   107  		}
   108  	}
   109  
   110  	for _, repo := range m.repos {
   111  		go repo.loop()
   112  	}
   113  	go m.pollGerritAndTickleLoop()
   114  	go m.subscribeToMaintnerAndTickleLoop()
   115  
   116  	shutdown := make(chan os.Signal, 1)
   117  	signal.Notify(shutdown, os.Interrupt)
   118  	<-shutdown
   119  }
   120  
   121  func writeCredentials(home string) error {
   122  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   123  	defer cancel()
   124  
   125  	sshConfig := &bytes.Buffer{}
   126  	gitConfig := &bytes.Buffer{}
   127  	sshConfigPath := filepath.Join(home, "ssh_config")
   128  	// ssh ignores $HOME in favor of /etc/passwd, so we need to override ssh_config explicitly.
   129  	fmt.Fprintf(gitConfig, "[core]\n  sshCommand=\"ssh -F %v\"\n", sshConfigPath)
   130  
   131  	// GitHub key, used as the default SSH private key.
   132  	if *flagMirrorGitHub {
   133  		privKey, err := retrieveSecret(ctx, secret.NameGitHubSSHKey)
   134  		if err != nil {
   135  			return fmt.Errorf("reading github key from secret manager: %v", err)
   136  		}
   137  		privKeyPath := filepath.Join(home, secret.NameGitHubSSHKey)
   138  		if err := os.WriteFile(privKeyPath, []byte(privKey+"\n"), 0600); err != nil {
   139  			return err
   140  		}
   141  		fmt.Fprintf(sshConfig, "Host github.com\n  IdentityFile %v\n", privKeyPath)
   142  	}
   143  
   144  	// The gitmirror service account should already be available via GKE workload identity.
   145  	if *flagMirrorCSR {
   146  		fmt.Fprintf(gitConfig, "[credential \"https://source.developers.google.com\"]\n  helper=gcloud.sh\n")
   147  	}
   148  
   149  	if err := os.WriteFile(filepath.Join(home, ".gitconfig"), gitConfig.Bytes(), 0600); err != nil {
   150  		return err
   151  	}
   152  	if err := os.WriteFile(sshConfigPath, sshConfig.Bytes(), 0600); err != nil {
   153  		return err
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func retrieveSecret(ctx context.Context, name string) (string, error) {
   160  	if *flagSecretsDir != "" {
   161  		secret, err := os.ReadFile(filepath.Join(*flagSecretsDir, name))
   162  		return string(secret), err
   163  	}
   164  	sc := secret.MustNewClient()
   165  	defer sc.Close()
   166  	return sc.Retrieve(ctx, name)
   167  }
   168  
   169  func createCacheDir() (string, error) {
   170  	if *flagCacheDir == "" {
   171  		dir, err := os.MkdirTemp("", "gitmirror")
   172  		if err != nil {
   173  			log.Fatal(err)
   174  		}
   175  		defer os.RemoveAll(dir)
   176  		return dir, nil
   177  	}
   178  
   179  	fi, err := os.Stat(*flagCacheDir)
   180  	if os.IsNotExist(err) {
   181  		if err := os.MkdirAll(*flagCacheDir, 0755); err != nil {
   182  			return "", fmt.Errorf("failed to create watcher's git cache dir: %v", err)
   183  		}
   184  	} else {
   185  		if err != nil {
   186  			return "", fmt.Errorf("invalid -cachedir: %v", err)
   187  		}
   188  		if !fi.IsDir() {
   189  			return "", fmt.Errorf("invalid -cachedir=%q; not a directory", *flagCacheDir)
   190  		}
   191  	}
   192  	return *flagCacheDir, nil
   193  }
   194  
   195  // A gitMirror watches Gerrit repositories, fetching the latest commits and
   196  // optionally mirroring them.
   197  type gitMirror struct {
   198  	mux      *http.ServeMux
   199  	repos    map[string]*repo
   200  	cacheDir string
   201  	// homeDir is used as $HOME for all commands, allowing easy configuration overrides.
   202  	homeDir                 string
   203  	goBase                  string // Base URL/path for Go upstream repos.
   204  	gerritClient            *gerrit.Client
   205  	mirrorGitHub, mirrorCSR bool
   206  	timeoutScale            int
   207  }
   208  
   209  func (m *gitMirror) addRepo(meta *repospkg.Repo) *repo {
   210  	name := meta.GoGerritProject
   211  	r := &repo{
   212  		name:    name,
   213  		url:     m.goBase + name,
   214  		meta:    meta,
   215  		root:    filepath.Join(m.cacheDir, name),
   216  		changed: make(chan bool, 1),
   217  		mirror:  m,
   218  	}
   219  	m.mux.Handle("/"+name+".tar.gz", r)
   220  	m.mux.Handle("/debug/watcher/"+r.name, r)
   221  	m.repos[name] = r
   222  	return r
   223  }
   224  
   225  // addMirrors sets up mirroring for repositories that need it.
   226  func (m *gitMirror) addMirrors() error {
   227  	for _, repo := range m.repos {
   228  		if m.mirrorGitHub && repo.meta.MirrorToGitHub {
   229  			if err := repo.addRemote("github", "git@github.com:"+repo.meta.GitHubRepo+".git", ""); err != nil {
   230  				return fmt.Errorf("adding GitHub remote: %v", err)
   231  			}
   232  		}
   233  		if m.mirrorCSR && repo.meta.MirrorToCSRProject != "" {
   234  			// Option "nokeycheck" skips Cloud Source Repositories' private
   235  			// key checking. We have dummy keys checked in as test data.
   236  			if err := repo.addRemote("csr", "https://source.developers.google.com/p/"+repo.meta.MirrorToCSRProject+"/r/"+repo.name, "nokeycheck"); err != nil {
   237  				return fmt.Errorf("adding CSR remote: %v", err)
   238  			}
   239  		}
   240  	}
   241  	return nil
   242  }
   243  
   244  // GET /
   245  // or:
   246  // GET /debug/watcher/
   247  func (m *gitMirror) handleRoot(w http.ResponseWriter, r *http.Request) {
   248  	if r.URL.Path != "/" && r.URL.Path != "/debug/watcher/" {
   249  		http.NotFound(w, r)
   250  		return
   251  	}
   252  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   253  	fmt.Fprint(w, "<html><body><pre>")
   254  	var names []string
   255  	for name := range m.repos {
   256  		names = append(names, name)
   257  	}
   258  	sort.Strings(names)
   259  	for _, name := range names {
   260  		fmt.Fprintf(w, "<a href='/debug/watcher/%s'>%s</a> - %s\n", name, name, m.repos[name].statusLine())
   261  	}
   262  	fmt.Fprint(w, "</pre></body></html>")
   263  }
   264  
   265  func (m *gitMirror) handleHealth(w http.ResponseWriter, r *http.Request) {
   266  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   267  	for _, r := range m.repos {
   268  		r.mu.Lock()
   269  		err := r.err
   270  		r.mu.Unlock()
   271  
   272  		if err != nil {
   273  			w.WriteHeader(http.StatusInternalServerError)
   274  			fmt.Fprintf(w, "%v: %v\n", r.name, err)
   275  			return
   276  		}
   277  	}
   278  
   279  	w.WriteHeader(http.StatusOK)
   280  }
   281  
   282  // a statusEntry is a status string at a specific time.
   283  type statusEntry struct {
   284  	status string
   285  	t      time.Time
   286  }
   287  
   288  // statusRing is a ring buffer of timestamped status messages.
   289  type statusRing struct {
   290  	mu   sync.Mutex      // guards rest
   291  	head int             // next position to fill
   292  	ent  [50]statusEntry // ring buffer of entries; zero time means unpopulated
   293  }
   294  
   295  func (r *statusRing) add(status string) {
   296  	r.mu.Lock()
   297  	defer r.mu.Unlock()
   298  
   299  	r.ent[r.head] = statusEntry{status, time.Now()}
   300  	r.head++
   301  	if r.head == len(r.ent) {
   302  		r.head = 0
   303  	}
   304  }
   305  
   306  func (r *statusRing) foreachDesc(fn func(statusEntry)) {
   307  	r.mu.Lock()
   308  	defer r.mu.Unlock()
   309  
   310  	i := r.head
   311  	for {
   312  		i--
   313  		if i < 0 {
   314  			i = len(r.ent) - 1
   315  		}
   316  		if i == r.head || r.ent[i].t.IsZero() {
   317  			return
   318  		}
   319  		fn(r.ent[i])
   320  	}
   321  }
   322  
   323  type remote struct {
   324  	name       string // name as configured in the repo.
   325  	pushOption string // optional extra push option (--push-option).
   326  }
   327  
   328  // repo represents a repository to be watched.
   329  type repo struct {
   330  	name    string
   331  	url     string
   332  	root    string // on-disk location of the git repo, *cacheDir/name
   333  	meta    *repospkg.Repo
   334  	changed chan bool // sent to when a change comes in
   335  	status  statusRing
   336  	dests   []remote // destination remotes to mirror to
   337  	mirror  *gitMirror
   338  
   339  	mu        sync.Mutex
   340  	err       error
   341  	firstBad  time.Time
   342  	lastBad   time.Time
   343  	firstGood time.Time
   344  	lastGood  time.Time
   345  }
   346  
   347  // init sets up the repo, cloning the repository to the local root.
   348  func (r *repo) init() error {
   349  	canReuse := true
   350  	if _, err := os.Stat(filepath.Join(r.root, "FETCH_HEAD")); err != nil {
   351  		canReuse = false
   352  		r.logf("can't reuse git dir, no FETCH_HEAD: %v", err)
   353  	}
   354  	if canReuse {
   355  		r.setStatus("reusing git dir; running git fetch")
   356  		_, _, err := r.runGitLogged("fetch", "--prune", "origin")
   357  		if err != nil {
   358  			canReuse = false
   359  			r.logf("git fetch failed; proceeding to wipe + clone instead")
   360  		}
   361  	}
   362  	if !canReuse {
   363  		r.setStatus("need clone; removing cache root")
   364  		os.RemoveAll(r.root)
   365  		_, _, err := r.runGitLogged("clone", "--mirror", r.url, r.root)
   366  		if err != nil {
   367  			return fmt.Errorf("cloning %s: %v", r.url, err)
   368  		}
   369  		r.setStatus("cloned")
   370  	}
   371  	return nil
   372  }
   373  
   374  func (r *repo) runGitLogged(args ...string) ([]byte, []byte, error) {
   375  	start := time.Now()
   376  	r.logf("running git %s", args)
   377  	stdout, stderr, err := r.runGitQuiet(args...)
   378  	if err == nil {
   379  		r.logf("ran git %s in %v", args, time.Since(start))
   380  	} else {
   381  		r.logf("git %s failed after %v: %v\nstderr: %v\n", args, time.Since(start), err, string(stderr))
   382  	}
   383  	return stdout, stderr, err
   384  }
   385  
   386  func (r *repo) runGitQuiet(args ...string) ([]byte, []byte, error) {
   387  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
   388  	defer cancel()
   389  
   390  	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
   391  	cmd := exec.Command("git", args...)
   392  	if args[0] == "clone" {
   393  		// Small hack: if we're cloning, the root doesn't exist yet.
   394  		envutil.SetDir(cmd, "/")
   395  	} else {
   396  		envutil.SetDir(cmd, r.root)
   397  	}
   398  	envutil.SetEnv(cmd, "HOME="+r.mirror.homeDir)
   399  	cmd.Stdout, cmd.Stderr = stdout, stderr
   400  	err := runCmdContext(ctx, cmd)
   401  	return stdout.Bytes(), stderr.Bytes(), err
   402  }
   403  
   404  func (r *repo) setErr(err error) {
   405  	r.mu.Lock()
   406  	defer r.mu.Unlock()
   407  	change := (r.err != nil) != (err != nil)
   408  	now := time.Now()
   409  	if err != nil {
   410  		if change {
   411  			r.firstBad = now
   412  		}
   413  		r.lastBad = now
   414  	} else {
   415  		if change {
   416  			r.firstGood = now
   417  		}
   418  		r.lastGood = now
   419  	}
   420  	r.err = err
   421  }
   422  
   423  var startTime = time.Now()
   424  
   425  func (r *repo) statusLine() string {
   426  	r.mu.Lock()
   427  	defer r.mu.Unlock()
   428  
   429  	if r.lastGood.IsZero() {
   430  		if r.err != nil {
   431  			return fmt.Sprintf("broken; permanently? always failing, for %v", time.Since(r.firstBad))
   432  		}
   433  		if time.Since(startTime) < 5*time.Minute {
   434  			return "ok; starting up, no report yet"
   435  		}
   436  		return fmt.Sprintf("hung; hang at start-up? no report since start %v ago", time.Since(startTime))
   437  	}
   438  	if r.err == nil {
   439  		if sinceGood := time.Since(r.lastGood); sinceGood > 6*time.Minute {
   440  			return fmt.Sprintf("hung? no activity since last success %v ago", sinceGood)
   441  		}
   442  		if r.lastBad.After(time.Now().Add(-1 * time.Hour)) {
   443  			return fmt.Sprintf("ok; recent failure %v ago", time.Since(r.lastBad))
   444  		}
   445  		return "ok"
   446  	}
   447  	return fmt.Sprintf("broken for %v", time.Since(r.lastGood))
   448  }
   449  
   450  func (r *repo) setStatus(status string) {
   451  	r.status.add(status)
   452  }
   453  
   454  func (r *repo) addRemote(name, url, pushOption string) error {
   455  	r.dests = append(r.dests, remote{
   456  		name:       name,
   457  		pushOption: pushOption,
   458  	})
   459  	if err := os.MkdirAll(filepath.Join(r.root, "remotes"), 0777); err != nil {
   460  		return err
   461  	}
   462  	// We want to include only the refs/heads/* and refs/tags/* namespaces
   463  	// in the mirrors. They correspond to published branches and tags.
   464  	// Leave out internal Gerrit namespaces such as refs/changes/*,
   465  	// refs/users/*, etc., because they're not helpful on other hosts.
   466  	remote := "URL: " + url + "\n" +
   467  		"Push: +refs/heads/*:refs/heads/*\n" +
   468  		"Push: +refs/tags/*:refs/tags/*\n"
   469  	return os.WriteFile(filepath.Join(r.root, "remotes", name), []byte(remote), 0777)
   470  }
   471  
   472  // loop continuously runs "git fetch" in the repo, checks for new
   473  // commits and mirrors commits to a destination repo (if enabled).
   474  func (r *repo) loop() {
   475  	for {
   476  		if err := r.loopOnce(); err != nil {
   477  			time.Sleep(10 * time.Second * time.Duration(r.mirror.timeoutScale))
   478  			continue
   479  		}
   480  
   481  		// We still run a timer but a very slow one, just
   482  		// in case the mechanism updating the repo tickler
   483  		// breaks for some reason.
   484  		timer := time.NewTimer(5 * time.Minute)
   485  		select {
   486  		case <-r.changed:
   487  			r.setStatus("got update tickle")
   488  			timer.Stop()
   489  		case <-timer.C:
   490  			r.setStatus("poll timer fired")
   491  		}
   492  	}
   493  }
   494  
   495  func (r *repo) loopOnce() error {
   496  	if err := r.fetch(); err != nil {
   497  		r.logf("fetch failed: %v", err)
   498  		r.setErr(err)
   499  		return err
   500  	}
   501  	for _, dest := range r.dests {
   502  		if err := r.push(dest); err != nil {
   503  			r.logf("push failed: %v", err)
   504  			r.setErr(err)
   505  			return err
   506  		}
   507  	}
   508  	r.setErr(nil)
   509  	r.setStatus("waiting")
   510  	return nil
   511  }
   512  
   513  func (r *repo) logf(format string, args ...interface{}) {
   514  	log.Printf(r.name+": "+format, args...)
   515  }
   516  
   517  // fetch runs "git fetch" in the repository root.
   518  // It tries three times, just in case it failed because of a transient error.
   519  func (r *repo) fetch() error {
   520  	err := r.try(3, func(attempt int) error {
   521  		r.setStatus(fmt.Sprintf("running git fetch origin, attempt %d", attempt))
   522  		if _, stderr, err := r.runGitLogged("fetch", "--prune", "origin"); err != nil {
   523  			return fmt.Errorf("%v\n\n%s", err, stderr)
   524  		}
   525  		return nil
   526  	})
   527  	if err != nil {
   528  		r.setStatus("git fetch failed")
   529  	} else {
   530  		r.setStatus("ran git fetch")
   531  	}
   532  	return err
   533  }
   534  
   535  // push runs "git push -f --mirror dest" in the repository root.
   536  // It tries three times, just in case it failed because of a transient error.
   537  func (r *repo) push(dest remote) error {
   538  	err := r.try(3, func(attempt int) error {
   539  		r.setStatus(fmt.Sprintf("syncing to %v, attempt %d", dest, attempt))
   540  		args := []string{"push", "-f", "--mirror"}
   541  		if dest.pushOption != "" {
   542  			args = append(args, "--push-option", dest.pushOption)
   543  		}
   544  		args = append(args, dest.name)
   545  		if _, stderr, err := r.runGitLogged(args...); err != nil {
   546  			return fmt.Errorf("%v\n\n%s", err, stderr)
   547  		}
   548  		return nil
   549  	})
   550  	if err != nil {
   551  		r.setStatus("sync to " + dest.name + " failed")
   552  	} else {
   553  		r.setStatus("did sync to " + dest.name)
   554  	}
   555  	return err
   556  }
   557  
   558  func (r *repo) fetchRevIfNeeded(ctx context.Context, rev string) error {
   559  	if _, _, err := r.runGitQuiet("cat-file", "-e", rev); err == nil {
   560  		return nil
   561  	}
   562  	r.logf("attempting to fetch missing revision %s from origin", rev)
   563  	_, _, err := r.runGitLogged("fetch", "origin", rev)
   564  	return err
   565  }
   566  
   567  // GET /<name>.tar.gz
   568  // GET /debug/watcher/<name>
   569  func (r *repo) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   570  	if req.Method != "GET" && req.Method != "HEAD" {
   571  		w.WriteHeader(http.StatusBadRequest)
   572  		return
   573  	}
   574  	if strings.HasPrefix(req.URL.Path, "/debug/watcher/") {
   575  		r.serveStatus(w, req)
   576  		return
   577  	}
   578  	rev := req.FormValue("rev")
   579  	if rev == "" {
   580  		w.WriteHeader(http.StatusBadRequest)
   581  		return
   582  	}
   583  	ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
   584  	defer cancel()
   585  	if err := r.fetchRevIfNeeded(ctx, rev); err != nil {
   586  		// Try the archive anyway, it might work
   587  		r.logf("error fetching revision %s: %v", rev, err)
   588  	}
   589  	tgz, _, err := r.runGitQuiet("archive", "--format=tgz", rev)
   590  	if err != nil {
   591  		http.Error(w, err.Error(), http.StatusInternalServerError)
   592  		return
   593  	}
   594  	w.Header().Set("Content-Length", strconv.Itoa(len(tgz)))
   595  	w.Header().Set("Content-Type", "application/x-compressed")
   596  	w.Write(tgz)
   597  }
   598  
   599  func (r *repo) serveStatus(w http.ResponseWriter, req *http.Request) {
   600  	w.Header().Set("Content-Type", "text/html")
   601  	fmt.Fprintf(w, "<html><head><title>watcher: %s</title><body><h1>watcher status for repo: %q</h1>\n",
   602  		r.name, r.name)
   603  	fmt.Fprintf(w, "<pre>\n")
   604  	nowRound := time.Now().Round(time.Second)
   605  	r.status.foreachDesc(func(ent statusEntry) {
   606  		fmt.Fprintf(w, "%v   %-20s %v\n",
   607  			ent.t.In(time.UTC).Format(time.RFC3339),
   608  			nowRound.Sub(ent.t.Round(time.Second)).String()+" ago",
   609  			ent.status)
   610  	})
   611  	fmt.Fprintf(w, "\n</pre></body></html>")
   612  }
   613  
   614  func (r *repo) try(n int, fn func(attempt int) error) error {
   615  	var err error
   616  	for tries := 0; tries < n; tries++ {
   617  		time.Sleep(time.Duration(tries) * 5 * time.Second * time.Duration(r.mirror.timeoutScale)) // Linear back-off.
   618  		if err = fn(tries); err == nil {
   619  			break
   620  		}
   621  	}
   622  	return err
   623  }
   624  
   625  func (m *gitMirror) notifyChanged(name string) {
   626  	repo, ok := m.repos[name]
   627  	if !ok {
   628  		return
   629  	}
   630  	select {
   631  	case repo.changed <- true:
   632  	default:
   633  	}
   634  }
   635  
   636  // pollGerritAndTickleLoop polls Gerrit's JSON meta URL of all its URLs
   637  // and their current branch heads.  When this sees that one has
   638  // changed, it tickles the channel for that repo and wakes up its
   639  // poller, if its poller is in a sleep.
   640  func (m *gitMirror) pollGerritAndTickleLoop() {
   641  	last := map[string]string{} // repo -> last seen hash
   642  	for {
   643  		gerritRepos, err := m.gerritMetaMap()
   644  		if err != nil {
   645  			log.Printf("pollGerritAndTickle: gerritMetaMap failed, skipping: %v", err)
   646  			gerritRepos = nil
   647  		}
   648  		for repo, hash := range gerritRepos {
   649  			if hash != last[repo] {
   650  				last[repo] = hash
   651  				m.notifyChanged(repo)
   652  			}
   653  		}
   654  		time.Sleep(*flagPollInterval)
   655  	}
   656  }
   657  
   658  // subscribeToMaintnerAndTickleLoop subscribes to maintner.golang.org
   659  // and watches for any ref changes in realtime.
   660  func (m *gitMirror) subscribeToMaintnerAndTickleLoop() {
   661  	for {
   662  		if err := m.subscribeToMaintnerAndTickle(); err != nil {
   663  			log.Printf("maintner loop: %v; retrying in 30 seconds", err)
   664  			time.Sleep(30 * time.Second)
   665  		}
   666  	}
   667  }
   668  
   669  func (m *gitMirror) subscribeToMaintnerAndTickle() error {
   670  	ctx := context.Background()
   671  	retryTicker := time.NewTicker(10 * time.Second)
   672  	defer retryTicker.Stop() // we never return, though
   673  	for {
   674  		err := maintner.TailNetworkMutationSource(ctx, godata.Server, func(e maintner.MutationStreamEvent) error {
   675  			if e.Mutation != nil && e.Mutation.Gerrit != nil {
   676  				gm := e.Mutation.Gerrit
   677  				if strings.HasPrefix(gm.Project, "go.googlesource.com/") {
   678  					proj := strings.TrimPrefix(gm.Project, "go.googlesource.com/")
   679  					log.Printf("maintner refs for %s changed", gm.Project)
   680  					m.notifyChanged(proj)
   681  				}
   682  			}
   683  			return e.Err
   684  		})
   685  		log.Printf("maintner tail error: %v; sleeping+restarting", err)
   686  
   687  		// prevent retry looping faster than once every 10
   688  		// seconds; but usually retry immediately in the case
   689  		// where we've been running for a while already.
   690  		<-retryTicker.C
   691  	}
   692  }
   693  
   694  // gerritMetaMap returns the map from repo name (e.g. "go") to its
   695  // latest master hash.
   696  func (m *gitMirror) gerritMetaMap() (map[string]string, error) {
   697  	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
   698  	defer cancel()
   699  	projs, err := m.gerritClient.ListProjects(ctx)
   700  	if err != nil {
   701  		return nil, fmt.Errorf("gerritClient.ListProjects: %v", err)
   702  	}
   703  	result := map[string]string{}
   704  	for _, p := range projs {
   705  		b, err := m.gerritClient.GetBranch(ctx, p.Name, "master")
   706  		if errors.Is(err, gerrit.ErrResourceNotExist) {
   707  			continue
   708  		} else if err != nil {
   709  			return nil, fmt.Errorf(`gerritClient.GetBranch(ctx, %q, "master"): %v`, p.Name, err)
   710  		}
   711  		result[p.Name] = b.Revision
   712  	}
   713  	return result, nil
   714  }
   715  
   716  // GET /debug/goroutines
   717  func handleDebugGoroutines(w http.ResponseWriter, r *http.Request) {
   718  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   719  	buf := make([]byte, 1<<20)
   720  	w.Write(buf[:runtime.Stack(buf, true)])
   721  }
   722  
   723  // GET /debug/env
   724  func handleDebugEnv(w http.ResponseWriter, r *http.Request) {
   725  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   726  	for _, kv := range os.Environ() {
   727  		fmt.Fprintf(w, "%s\n", kv)
   728  	}
   729  }
   730  
   731  // runCmdContext allows OS-specific overrides of process execution behavior.
   732  // See runCmdContextLinux.
   733  var runCmdContext = runCmdContextDefault
   734  
   735  // runCmdContextDefault runs cmd controlled by ctx.
   736  func runCmdContextDefault(ctx context.Context, cmd *exec.Cmd) error {
   737  	if err := cmd.Start(); err != nil {
   738  		return err
   739  	}
   740  	resChan := make(chan error, 1)
   741  	go func() {
   742  		resChan <- cmd.Wait()
   743  	}()
   744  
   745  	select {
   746  	case err := <-resChan:
   747  		return err
   748  	case <-ctx.Done():
   749  	}
   750  	// Canceled. Interrupt and see if it ends voluntarily.
   751  	cmd.Process.Signal(os.Interrupt)
   752  	select {
   753  	case <-resChan:
   754  		return ctx.Err()
   755  	case <-time.After(time.Second):
   756  	}
   757  	// Didn't shut down in response to interrupt. Kill it hard.
   758  	cmd.Process.Kill()
   759  	<-resChan
   760  	return ctx.Err()
   761  }