// childlog.go gives the store child a voice.
//
// `zit serve` was started with no Stdout or Stderr, so everything it
// said went to /dev/null. That is invisible until the day the child is
// the only one who knows what went wrong — and then it is the whole
// problem. A push was refused for fifteen hours with the child's
// explanation, `malformed file line`, reaching the pusher as an
// unparseable error card and reaching the forge's log not at all. The
// operator could see THAT a push failed and never WHY, and the one
// component holding the answer was writing it to a closed pipe.
//
// So the child's diagnostics land in the forge's log, named by the
// repository they came from. This is cheap and it is not clever: the
// point is only that the answer exists somewhere a person can read it.
package xfer
import (
"bytes"
"io"
"log"
"sync"
)
// maxChildLine caps a single buffered line, so a child that writes a
// megabyte without a newline cannot grow this without bound. The tail
// past the cap is dropped, not held.
const maxChildLine = 8 << 10
// childLog is an io.Writer that turns a child's stderr into log lines,
// one per newline, each named with the repository.
//
// Writes arrive from the child's pipe on its own goroutine while the
// backend may be dropped from another, so it holds a mutex: the cost is
// nothing on a stream that is silent in normal operation, and the
// alternative is a race on the partial-line buffer.
type childLog struct {
repo string
mu sync.Mutex
buf bytes.Buffer
// truncating drops the remainder of a line already past the cap,
// so one oversized line cannot smear into the next.
truncating bool
}
func newChildLog(repo string) io.Writer { return &childLog{repo: repo} }
func (c *childLog) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
n := len(p)
for len(p) > 0 {
i := bytes.IndexByte(p, '\n')
if i < 0 {
c.append(p)
break
}
c.append(p[:i])
c.emit()
p = p[i+1:]
}
return n, nil
}
func (c *childLog) append(b []byte) {
if c.truncating {
return
}
if room := maxChildLine - c.buf.Len(); len(b) > room {
c.buf.Write(b[:room])
c.truncating = true
return
}
c.buf.Write(b)
}
// emit writes one line and resets. A blank line is dropped rather than
// logged as an empty entry: children pad their output and a log full of
// bare timestamps is worse than no log.
func (c *childLog) emit() {
line := bytes.TrimRight(c.buf.Bytes(), "\r")
if len(bytes.TrimSpace(line)) > 0 {
log.Printf("zit serve %s: %s", c.repo, line)
}
c.buf.Reset()
c.truncating = false
}