blob: ac8d27382faa32a87845eb23f1a08de670297e4a [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package botanist
import (
"bytes"
"context"
"io"
"math"
"time"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/lib/streams"
)
// LockedWriter is a wrapper around a writer that locks around each write so
// that multiple writes won't interleave with each other.
type LockedWriter struct {
locks chan *writeLock
writer io.Writer
}
type writeLock struct {
start chan struct{}
end chan struct{}
}
// NewLockedWriter returns a LockedWriter that associates a new lock with the
// provided writer.
func NewLockedWriter(ctx context.Context, writer io.Writer) *LockedWriter {
w := &LockedWriter{
locks: make(chan *writeLock),
writer: writer,
}
go func() {
for lock := range w.locks {
// Signal write to start.
lock.start <- struct{}{}
// Wait for write to finish.
<-lock.end
}
}()
return w
}
func (w *LockedWriter) Write(data []byte) (int, error) {
start := make(chan struct{})
end := make(chan struct{})
// Queue write.
w.locks <- &writeLock{start, end}
// Wait for turn to start write.
<-start
// Defer sending struct on chan to signal end of write.
defer func() { end <- struct{}{} }()
return w.writer.Write(data)
}
func (w *LockedWriter) Close() {
close(w.locks)
}
// LineWriter is a wrapper around a writer that writes line by line so
// that multiple writers to the same underlying writer won't interleave
// their writes midline.
type LineWriter struct {
writer io.Writer
line []byte
prefix string
}
// NewLineWriter returns a new LineWriter.
func NewLineWriter(writer io.Writer, prefix string) *LineWriter {
return &LineWriter{
writer: writer,
prefix: prefix,
}
}
// Write stores bytes until it gets a newline and then writes to the underlying
// writer line by line. If the underlying Write() returns an err, this writer
// will return the number of bytes of the current data that were written.
// Otherwise, it returns the full length of the data to notify callers that it
// has received the whole data.
func (w *LineWriter) Write(data []byte) (int, error) {
lines := bytes.SplitAfter(data, []byte("\n"))
written := 0
for _, line := range lines {
if bytes.HasSuffix(line, []byte("\n")) {
toWrite := []byte{}
if w.prefix != "" {
toWrite = append(toWrite, []byte(w.prefix+": ")...)
}
toWrite = append(toWrite, w.line...)
n, err := w.writer.Write(append(toWrite, line...))
written += int(math.Max(0, float64(n-len(toWrite))))
if err != nil {
return written, err
}
w.line = []byte{}
} else {
w.line = append(w.line, line...)
}
}
return len(data), nil
}
// TimestampWriter is a wrapper around a writer that prepends its writes
// with the current host timestamp. This will allow all botanist logs
// (kernel/serial/syslog/test) to be reliably lined up when reading them.
type TimestampWriter struct {
writer io.Writer
format string
}
func NewTimestampWriter(writer io.Writer) *TimestampWriter {
return &TimestampWriter{writer, "15:04:05.000000 "}
}
func (w *TimestampWriter) Write(data []byte) (int, error) {
if n, err := w.writer.Write([]byte(time.Now().Format(w.format))); err != nil {
return n, err
}
return w.writer.Write(data)
}
// NewStiodWriters returns a new LineWriter for the stdout and stderr associated
// with the provided context. It also returns a function to flush out any
// remaining data not written by Write because it didn't end with a newline.
func NewStdioWriters(ctx context.Context, id string) (io.Writer, io.Writer, func()) {
stdoutWriter := NewLineWriter(streams.Stdout(ctx), id)
stderrWriter := NewLineWriter(streams.Stderr(ctx), id)
flush := func() {
// Flush out the rest of the data stored by the writers.
if len(stdoutWriter.line) > 0 {
if _, err := stdoutWriter.Write([]byte("\n")); err != nil {
logger.Debugf(ctx, "failed to flush out data to stdout %q: %s", string(stdoutWriter.line), err)
}
}
if len(stderrWriter.line) > 0 {
if _, err := stderrWriter.Write([]byte("\n")); err != nil {
logger.Debugf(ctx, "failed to flush out data to stderr %q: %s", string(stderrWriter.line), err)
}
}
}
return stdoutWriter, stderrWriter, flush
}