package platform import ( "bytes" "context" "errors" "fmt" "os/exec" "sync" "time" ) const ( quickProbeTimeout = 2 * time.Second standardProbeTimeout = 3 * time.Second slowProbeTimeout = 8 * time.Second probeWaitDelay = 500 * time.Millisecond defaultProbeMaxBytes = 1 << 20 // stdout and stderr combined ) var errProbeOutputLimit = errors.New("platform probe output limit exceeded") // probeSpec is immutable input for one subprocess probe. Each Run call creates // its own context, so a slow primary probe cannot consume a fallback's budget. type probeSpec struct { name string args []string timeout time.Duration maxOutput int } func newProbe(name string, timeout time.Duration, args ...string) probeSpec { return probeSpec{ name: name, args: append([]string(nil), args...), timeout: timeout, maxOutput: defaultProbeMaxBytes, } } type probeRunner interface { Run(probeSpec) ([]byte, error) } type systemProbeRunner struct{} func (systemProbeRunner) Run(spec probeSpec) ([]byte, error) { return runProbeCommand(context.Background(), spec) } type probeOutput struct { mu sync.Mutex stdout bytes.Buffer stderr bytes.Buffer remaining int exceeded bool cancel context.CancelFunc } type probeStream struct { output *probeOutput stdout bool } func (s probeStream) Write(p []byte) (int, error) { s.output.mu.Lock() defer s.output.mu.Unlock() allowed := len(p) if allowed > s.output.remaining { allowed = s.output.remaining } if allowed > 0 { if s.stdout { _, _ = s.output.stdout.Write(p[:allowed]) } else { _, _ = s.output.stderr.Write(p[:allowed]) } s.output.remaining -= allowed } if allowed < len(p) && !s.output.exceeded { s.output.exceeded = true s.output.cancel() } // Report the full input as consumed. Cancellation terminates the process; // returning a short write could leave it blocked on a full pipe first. return len(p), nil } func (o *probeOutput) snapshot() (stdout, stderr []byte, exceeded bool) { o.mu.Lock() defer o.mu.Unlock() return append([]byte(nil), o.stdout.Bytes()...), append([]byte(nil), o.stderr.Bytes()...), o.exceeded } func runProbeCommand(parent context.Context, spec probeSpec) ([]byte, error) { if spec.name == "" { return nil, errors.New("platform probe command is empty") } if spec.timeout <= 0 { return nil, fmt.Errorf("platform probe %q has invalid timeout %s", spec.name, spec.timeout) } if spec.maxOutput <= 0 { return nil, fmt.Errorf("platform probe %q has invalid output limit %d", spec.name, spec.maxOutput) } if parent == nil { parent = context.Background() } ctx, cancel := context.WithTimeout(parent, spec.timeout) defer cancel() output := &probeOutput{remaining: spec.maxOutput, cancel: cancel} cmd := exec.CommandContext(ctx, spec.name, spec.args...) cmd.Stdout = probeStream{output: output, stdout: true} cmd.Stderr = probeStream{output: output} cmd.WaitDelay = probeWaitDelay runErr := cmd.Run() stdout, stderr, exceeded := output.snapshot() if exceeded { return stdout, fmt.Errorf("%w: %s exceeded %d bytes", errProbeOutputLimit, spec.name, spec.maxOutput) } if ctx.Err() != nil { return stdout, fmt.Errorf("platform probe %s: %w", spec.name, ctx.Err()) } if runErr != nil { if len(stderr) > 0 { return stdout, fmt.Errorf("platform probe %s failed: %w: %s", spec.name, runErr, bytes.TrimSpace(stderr)) } return stdout, fmt.Errorf("platform probe %s failed: %w", spec.name, runErr) } return stdout, nil }