Skip to content
quarry Artifact [f92f7eaf2e]
Login

Artifact [f92f7eaf2e]

Artifact f92f7eaf2ef8f8ae34956a7637e7cd8e6595c0b3b67058b116b2ffc1709dab06:


// 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
}