package installer import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "sync" "time" ) const ( detectionProbeTimeout = 3 * time.Second detectionProbeMaxBytes = 1 << 20 detectionProbeWait = 500 * time.Millisecond ) var errDetectionProbeOutputLimit = errors.New("detection probe output limit exceeded") type boundedDetectionOutput struct { mu sync.Mutex buf bytes.Buffer limit int exceeded bool cancel context.CancelFunc } func (o *boundedDetectionOutput) Write(p []byte) (int, error) { o.mu.Lock() defer o.mu.Unlock() remaining := o.limit - o.buf.Len() if remaining > 0 { writeLen := len(p) if writeLen > remaining { writeLen = remaining } _, _ = o.buf.Write(p[:writeLen]) } if len(p) > remaining { if !o.exceeded { o.exceeded = true o.cancel() } } // Consume the whole write so the child cannot remain blocked on a pipe; // the caller rejects the capped result after the process exits. return len(p), nil } func (o *boundedDetectionOutput) snapshot() ([]byte, bool) { o.mu.Lock() defer o.mu.Unlock() return append([]byte(nil), o.buf.Bytes()...), o.exceeded } // runDetectionProbe executes a read-only system/package query with a fixed // time budget, capped combined output, and a stable C locale. Probe failures // are deliberately distinguishable from authoritative absence by callers. func runDetectionProbe(name string, args ...string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), detectionProbeTimeout) defer cancel() output := &boundedDetectionOutput{limit: detectionProbeMaxBytes, cancel: cancel} cmd := exec.CommandContext(ctx, name, args...) cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C") cmd.Stdout = output cmd.Stderr = output cmd.WaitDelay = detectionProbeWait err := cmd.Run() data, exceeded := output.snapshot() if exceeded { return data, fmt.Errorf("%w: %s exceeded %d bytes", errDetectionProbeOutputLimit, name, detectionProbeMaxBytes) } if ctx.Err() != nil { return data, fmt.Errorf("detection probe %s: %w", name, ctx.Err()) } if err != nil { return data, fmt.Errorf("detection probe %s: %w", name, err) } return data, nil }