Files
2026-07-20 17:04:16 +00:00

124 lines
2.9 KiB
Go

//go:build linux
package workspace
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/nezhahq/nezha/integration/agentcompat/internal/evidence"
)
const workspaceTruncationMarker = "[TRUNCATED]\n"
type LogFile struct {
file *os.File
maxBytes int
written int
pending []byte
dropLine bool
closed bool
closeOnce sync.Once
closeErr error
mu sync.Mutex
}
func newLogFile(path string, maxBytes int) (*LogFile, error) {
root, err := os.OpenRoot(filepath.Dir(path))
if err != nil {
return nil, fmt.Errorf("open workspace log directory: %w", err)
}
defer root.Close()
file, err := root.OpenFile(filepath.Base(path), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return nil, fmt.Errorf("create workspace log: %w", err)
}
return &LogFile{file: file, maxBytes: maxBytes}, nil
}
func (logFile *LogFile) Name() string { return logFile.file.Name() }
func (logFile *LogFile) WriteString(value string) (int, error) {
return logFile.Write([]byte(value))
}
func (logFile *LogFile) Write(data []byte) (int, error) {
logFile.mu.Lock()
defer logFile.mu.Unlock()
if logFile.closed {
return 0, errors.New("write closed workspace log")
}
inputLength := len(data)
for len(data) > 0 {
newline := bytes.IndexByte(data, '\n')
if newline < 0 {
logFile.appendFragment(data)
break
}
logFile.appendFragment(data[:newline+1])
if err := logFile.flushLine(); err != nil {
return 0, err
}
data = data[newline+1:]
}
return inputLength, nil
}
func (logFile *LogFile) appendFragment(fragment []byte) {
if logFile.dropLine {
return
}
if len(logFile.pending)+len(fragment) > logFile.maxBytes {
logFile.pending = nil
logFile.dropLine = true
return
}
logFile.pending = append(logFile.pending, fragment...)
}
func (logFile *LogFile) flushLine() error {
if logFile.dropLine {
logFile.dropLine = false
return logFile.writeBounded(workspaceTruncationMarker)
}
redacted := evidence.Redact(string(logFile.pending))
logFile.pending = nil
if len(redacted) > logFile.maxBytes-logFile.written {
return logFile.writeBounded(workspaceTruncationMarker)
}
return logFile.writeBounded(redacted)
}
func (logFile *LogFile) writeBounded(value string) error {
remaining := logFile.maxBytes - logFile.written
if remaining <= 0 || value == "" {
return nil
}
if len(value) > remaining {
value = value[:remaining]
}
written, err := logFile.file.WriteString(value)
logFile.written += written
if err != nil {
return fmt.Errorf("write workspace log: %w", err)
}
return nil
}
func (logFile *LogFile) Close() error {
logFile.closeOnce.Do(func() {
logFile.mu.Lock()
defer logFile.mu.Unlock()
if len(logFile.pending) > 0 || logFile.dropLine {
logFile.closeErr = logFile.flushLine()
}
logFile.closed = true
logFile.closeErr = errors.Join(logFile.closeErr, logFile.file.Close())
})
return logFile.closeErr
}