161 lines
4.2 KiB
Go
161 lines
4.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Límites de salida enviada al modelo (§85). La salida completa se guarda
|
|
// en .agent/tmp/ cuando los supera, para poder consultarla por partes.
|
|
const (
|
|
inlineLimit = 16 << 10 // hasta aquí la salida va entera al modelo
|
|
headKeep = 8 << 10 // si se supera: primeros 8 KiB...
|
|
tailKeep = 4 << 10 // ...y últimos 4 KiB
|
|
maxSpillSize = 256 << 20 // tope del archivo de desbordamiento
|
|
)
|
|
|
|
// spillWriter captura un flujo de salida. Mantiene en memoria hasta
|
|
// inlineLimit bytes; si se supera, vuelca todo a un archivo temporal y
|
|
// conserva en memoria solo el inicio y una ventana circular del final.
|
|
//
|
|
// Así un "journalctl" o un "find /" enorme no agota la memoria ni el
|
|
// contexto del modelo, y aun así se conserva el resultado completo.
|
|
type spillWriter struct {
|
|
dir string // directorio para el archivo de desbordamiento
|
|
prefix string
|
|
|
|
head bytes.Buffer
|
|
tail []byte // ventana circular de tailKeep bytes
|
|
tailN int // bytes escritos en tail (hasta tailKeep)
|
|
tailAt int
|
|
total int64
|
|
|
|
file *os.File
|
|
fileErr error
|
|
fileBytes int64
|
|
}
|
|
|
|
func newSpillWriter(dir, prefix string) *spillWriter {
|
|
return &spillWriter{dir: dir, prefix: prefix, tail: make([]byte, tailKeep)}
|
|
}
|
|
|
|
func (w *spillWriter) Write(p []byte) (int, error) {
|
|
n := len(p)
|
|
w.total += int64(n)
|
|
|
|
// Ventana circular del final.
|
|
for _, b := range p {
|
|
w.tail[w.tailAt] = b
|
|
w.tailAt = (w.tailAt + 1) % tailKeep
|
|
if w.tailN < tailKeep {
|
|
w.tailN++
|
|
}
|
|
}
|
|
|
|
if w.file == nil && w.fileErr == nil && w.head.Len()+len(p) > inlineLimit {
|
|
w.openSpill()
|
|
}
|
|
if w.file != nil {
|
|
if w.fileBytes < maxSpillSize {
|
|
chunk := p
|
|
if rem := maxSpillSize - w.fileBytes; int64(len(chunk)) > rem {
|
|
chunk = chunk[:rem]
|
|
}
|
|
m, _ := w.file.Write(chunk)
|
|
w.fileBytes += int64(m)
|
|
}
|
|
if w.head.Len() < headKeep {
|
|
room := headKeep - w.head.Len()
|
|
w.head.Write(p[:min(room, len(p))])
|
|
}
|
|
return n, nil
|
|
}
|
|
if w.head.Len() < inlineLimit {
|
|
room := inlineLimit - w.head.Len()
|
|
w.head.Write(p[:min(room, len(p))])
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// openSpill crea el archivo y vuelca lo acumulado hasta ahora.
|
|
func (w *spillWriter) openSpill() {
|
|
if err := os.MkdirAll(w.dir, 0o700); err != nil {
|
|
w.fileErr = err
|
|
return
|
|
}
|
|
name := fmt.Sprintf("%s-%s-*.out", time.Now().Format("20060102-150405"), w.prefix)
|
|
f, err := os.CreateTemp(w.dir, name)
|
|
if err != nil {
|
|
w.fileErr = err
|
|
return
|
|
}
|
|
w.file = f
|
|
m, _ := f.Write(w.head.Bytes())
|
|
w.fileBytes = int64(m)
|
|
// A partir de aquí head solo guarda los primeros headKeep bytes.
|
|
if w.head.Len() > headKeep {
|
|
b := append([]byte(nil), w.head.Bytes()[:headKeep]...)
|
|
w.head.Reset()
|
|
w.head.Write(b)
|
|
}
|
|
}
|
|
|
|
// Close cierra el archivo de desbordamiento, si existe.
|
|
func (w *spillWriter) Close() {
|
|
if w.file != nil {
|
|
w.file.Close()
|
|
}
|
|
}
|
|
|
|
// Text devuelve la representación para el modelo: completa si cabe, o
|
|
// inicio + aviso + final, indicando dónde está el resultado completo.
|
|
func (w *spillWriter) Text() string {
|
|
if w.total <= inlineLimit {
|
|
return w.head.String()
|
|
}
|
|
omitted := w.total - int64(w.head.Len()) - int64(w.tailN)
|
|
where := "not saved"
|
|
switch {
|
|
case w.file != nil:
|
|
where = "full output saved to " + w.file.Name()
|
|
if w.fileBytes < w.total {
|
|
where += fmt.Sprintf(" (first %d bytes only)", w.fileBytes)
|
|
}
|
|
case w.fileErr != nil:
|
|
where = "could not save full output: " + w.fileErr.Error()
|
|
}
|
|
return fmt.Sprintf("%s\n\n[... %d bytes omitted of %d total; %s; use read_file with offset/limit or grep to inspect ...]\n\n%s",
|
|
w.head.String(), max(omitted, 0), w.total, where, w.tailString())
|
|
}
|
|
|
|
// SpillPath devuelve la ruta del archivo completo, si se creó.
|
|
func (w *spillWriter) SpillPath() string {
|
|
if w.file == nil {
|
|
return ""
|
|
}
|
|
return w.file.Name()
|
|
}
|
|
|
|
func (w *spillWriter) tailString() string {
|
|
if w.tailN < tailKeep {
|
|
return string(w.tail[:w.tailN])
|
|
}
|
|
return string(w.tail[w.tailAt:]) + string(w.tail[:w.tailAt])
|
|
}
|
|
|
|
// truncateText recorta textos ya en memoria (p.ej. read_file).
|
|
func truncateText(s string, limit int) (string, bool) {
|
|
if len(s) <= limit {
|
|
return s, false
|
|
}
|
|
return s[:limit], true
|
|
}
|
|
|
|
// tmpDir es el directorio de salidas extensas dentro del proyecto.
|
|
func tmpDir(root string) string {
|
|
return filepath.Join(root, ".agent", "tmp")
|
|
}
|