171 lines
4.2 KiB
Go
171 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
)
|
|
|
|
// cpuWorkers returns the parallelism level for install/check work.
|
|
// Defaults to runtime.NumCPU(); overridable via BOOTSTRAP_PARALLELISM
|
|
// (e.g. for tests / constrained hosts) and clamped to >=1.
|
|
func cpuWorkers() int {
|
|
if v := os.Getenv("BOOTSTRAP_PARALLELISM"); v != "" {
|
|
var n int
|
|
_, _ = fmt.Sscanf(v, "%d", &n)
|
|
if n >= 1 {
|
|
return n
|
|
}
|
|
}
|
|
n := runtime.NumCPU()
|
|
if n < 1 {
|
|
return 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
// httpWorkersCap is the polite ceiling for HTTP-bound concurrency when no
|
|
// GitHub token has been provided (GitHub anon rate-limits at 60/hour).
|
|
// Authenticated requests get 5000/hour so we lift the cap when a token is
|
|
// available — see githubTokenSet.
|
|
const httpWorkersCap = 8
|
|
|
|
var githubTokenSet bool
|
|
|
|
// httpWorkers caps cpuWorkers() to httpWorkersCap unless a GitHub token has
|
|
// been supplied (in which case we use the full processor count).
|
|
func httpWorkers() int {
|
|
n := cpuWorkers()
|
|
if githubTokenSet {
|
|
return n
|
|
}
|
|
if n > httpWorkersCap {
|
|
return httpWorkersCap
|
|
}
|
|
return n
|
|
}
|
|
|
|
// parallelDo runs fn(i, items[i]) over items with at most maxWorkers
|
|
// goroutines in flight. Returns once every task has finished. Order of
|
|
// completion is not guaranteed; fn is responsible for its own synchronization
|
|
// when writing shared state.
|
|
func parallelDo[T any](items []T, maxWorkers int, fn func(i int, item T)) {
|
|
if len(items) == 0 {
|
|
return
|
|
}
|
|
if maxWorkers < 1 {
|
|
maxWorkers = 1
|
|
}
|
|
if maxWorkers > len(items) {
|
|
maxWorkers = len(items)
|
|
}
|
|
sem := make(chan struct{}, maxWorkers)
|
|
var wg sync.WaitGroup
|
|
for i, item := range items {
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func(i int, item T) {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
fn(i, item)
|
|
}(i, item)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// taskOutput is the per-call sink for status text and subprocess output.
|
|
//
|
|
// Two modes:
|
|
//
|
|
// Sequential (label==""): Printf goes straight to os.Stdout, and Writer()
|
|
// returns nil so runCmd falls back to its default streamed-to-stdout mode.
|
|
// Behavior matches the pre-parallelism code exactly.
|
|
//
|
|
// Captured (label!=""): Printf and runCmd output both land in an internal
|
|
// buffer; Flush() prints the whole block at once with a " [label] " prefix
|
|
// on every line. Used by parallel install workers so concurrent output
|
|
// doesn't interleave.
|
|
type taskOutput struct {
|
|
label string
|
|
buf bytes.Buffer
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func newSerialOutput() *taskOutput { return &taskOutput{} }
|
|
func newCapturedOutput(label string) *taskOutput {
|
|
return &taskOutput{label: label}
|
|
}
|
|
|
|
// Printf writes to the task's destination.
|
|
func (t *taskOutput) Printf(format string, args ...any) {
|
|
if t.label == "" {
|
|
fmt.Printf(format, args...)
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
fmt.Fprintf(&t.buf, format, args...)
|
|
}
|
|
|
|
// Println writes a line to the task's destination.
|
|
func (t *taskOutput) Println(args ...any) {
|
|
if t.label == "" {
|
|
fmt.Println(args...)
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
fmt.Fprintln(&t.buf, args...)
|
|
}
|
|
|
|
// Writer returns the io.Writer that runCmd/runShell should target via
|
|
// CmdOpts.Out. Returns nil in sequential mode (preserves streamed stdout).
|
|
func (t *taskOutput) Writer() io.Writer {
|
|
if t.label == "" {
|
|
return nil
|
|
}
|
|
return &lockingWriter{mu: &t.mu, w: &t.buf}
|
|
}
|
|
|
|
// Flush emits the captured buffer to w with the task label prefixed onto
|
|
// every line. Idempotent and a no-op in sequential mode.
|
|
func (t *taskOutput) Flush(w io.Writer) {
|
|
if t.label == "" {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.buf.Len() == 0 {
|
|
return
|
|
}
|
|
if w == nil {
|
|
w = os.Stdout
|
|
}
|
|
prefix := fmt.Sprintf(" [%s] ", t.label)
|
|
lines := bytes.Split(t.buf.Bytes(), []byte{'\n'})
|
|
for i, line := range lines {
|
|
if i == len(lines)-1 && len(line) == 0 {
|
|
break
|
|
}
|
|
fmt.Fprintf(w, "%s%s\n", prefix, line)
|
|
}
|
|
t.buf.Reset()
|
|
}
|
|
|
|
// lockingWriter is a thin io.Writer that holds the taskOutput mutex while
|
|
// writing, so runCmd / runShell can stream into the buffer concurrently with
|
|
// status Printf calls on the same task without corrupting the buffer.
|
|
type lockingWriter struct {
|
|
mu *sync.Mutex
|
|
w io.Writer
|
|
}
|
|
|
|
func (l *lockingWriter) Write(p []byte) (int, error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.w.Write(p)
|
|
}
|