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 }