| 1 | package shellrun |
| 2 | |
| 3 | import "io" |
| 4 | |
| 5 | // The foreground output caps are one contract shared by every shell execution |
| 6 | // path. They live here so a second runner cannot ship a different memory bound |
| 7 | // than the one #6473/#6528 established. |
| 8 | |
| 9 | // BoundedOutput collects model-visible combined output under the shared cap: |
| 10 | // complete text up to 10 MiB, then a fixed head plus a rolling 64 KiB tail |
| 11 | // separated by an explicit truncation notice. Write never short-writes. |
| 12 | type BoundedOutput struct { |
| 13 | buf *boundedBuffer |
| 14 | } |
| 15 | |
| 16 | // NewBoundedOutput returns a collector using the shared foreground caps. |
| 17 | func NewBoundedOutput() *BoundedOutput { |
| 18 | c := &outputCollector{} |
| 19 | c.combined = &boundedBuffer{ |
| 20 | mu: &c.mu, |
| 21 | limit: combinedOutputMaxBytes, |
| 22 | tailLimit: combinedOutputTailBytes, |
| 23 | marker: combinedOutputTruncated, |
| 24 | } |
| 25 | return &BoundedOutput{buf: c.combined} |
| 26 | } |
| 27 | |
| 28 | // Write appends p, evicting middle output once the cap is crossed. |
| 29 | func (o *BoundedOutput) Write(p []byte) (int, error) { return o.buf.Write(p) } |
| 30 | |
| 31 | // WriteString appends s. |
| 32 | func (o *BoundedOutput) WriteString(s string) { |
| 33 | if s == "" { |
| 34 | return |
| 35 | } |
| 36 | _, _ = o.buf.Write([]byte(s)) |
| 37 | } |
| 38 | |
| 39 | // String returns the collected output, including the truncation notice when the |
| 40 | // cap was crossed. |
| 41 | func (o *BoundedOutput) String() string { return o.buf.String() } |
| 42 | |
| 43 | // Truncated reports whether output was evicted. |
| 44 | func (o *BoundedOutput) Truncated() bool { |
| 45 | o.buf.mu.Lock() |
| 46 | defer o.buf.mu.Unlock() |
| 47 | return o.buf.truncated |
| 48 | } |
| 49 | |
| 50 | // ProgressWriter buffers partial UTF-8 characters until its owner ends the stream. |
| 51 | type ProgressWriter interface { |
| 52 | io.Writer |
| 53 | Flush() |
| 54 | } |
| 55 | |
| 56 | // NewProgressWriter returns the shared live-progress sink. Live output crosses |
| 57 | // async UI queues and append-only reducers before the bounded final result |
| 58 | // replaces it, so it is capped far below the final output cap; a never-ending |
| 59 | // command must not exhaust memory on the transient path. |
| 60 | func NewProgressWriter(emit func(string)) ProgressWriter { |
| 61 | return newProgressWriter(emit, progressOutputMaxBytes, progressOutputTruncated) |
| 62 | } |
| 63 |