Add atomic reporting and message categories
This commit is contained in:
194
reporter.go
194
reporter.go
@@ -1,7 +1,9 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -21,6 +23,15 @@ const (
|
||||
destinationDiagnostics
|
||||
)
|
||||
|
||||
type messageCategory uint8
|
||||
|
||||
const (
|
||||
categoryInfo messageCategory = iota
|
||||
categoryWarning
|
||||
categoryDebug
|
||||
categoryProgress
|
||||
)
|
||||
|
||||
type reportingState struct {
|
||||
mu sync.Mutex
|
||||
output destinationState
|
||||
@@ -53,6 +64,20 @@ type managedOutputWriter struct {
|
||||
destination reportingDestination
|
||||
}
|
||||
|
||||
type coordinatedWriter struct {
|
||||
state *reportingState
|
||||
destination reportingDestination
|
||||
writeError error
|
||||
}
|
||||
|
||||
func (w *coordinatedWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.state.writeLocked(w.destination, p)
|
||||
if err != nil && w.writeError == nil {
|
||||
w.writeError = err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func newReporter(streams Streams) *Reporter {
|
||||
streams = normalizeStreams(streams)
|
||||
return &Reporter{state: &reportingState{
|
||||
@@ -77,6 +102,175 @@ func (w managedOutputWriter) Write(p []byte) (int, error) {
|
||||
return w.state.write(w.destination, p)
|
||||
}
|
||||
|
||||
// RenderOutput coordinates a complete substantive report rendering operation.
|
||||
func (r *Reporter) RenderOutput(write func(io.Writer) error) error {
|
||||
if r == nil || r.state == nil {
|
||||
return ErrReporterClosed
|
||||
}
|
||||
state := r.state
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
if state.closed {
|
||||
return ErrReporterClosed
|
||||
}
|
||||
if write == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
destination := state.destination(destinationOutput)
|
||||
destination.submitted = true
|
||||
writer := &coordinatedWriter{state: state, destination: destinationOutput}
|
||||
renderErr := write(writer)
|
||||
if state.mode == modeQuiet {
|
||||
destination.suppressed = true
|
||||
} else if renderErr == nil && writer.writeError == nil {
|
||||
destination.delivered = true
|
||||
}
|
||||
return finishRenderLocked(state, destinationOutput, renderErr, writer.writeError)
|
||||
}
|
||||
|
||||
// Info submits an informational message.
|
||||
func (r *Reporter) Info(message string, details ...Detail) error {
|
||||
return r.submitMessage(categoryInfo, message, details)
|
||||
}
|
||||
|
||||
// Warn submits a warning message.
|
||||
func (r *Reporter) Warn(message string, details ...Detail) error {
|
||||
return r.submitMessage(categoryWarning, message, details)
|
||||
}
|
||||
|
||||
// Debug submits a debug message.
|
||||
func (r *Reporter) Debug(message string, details ...Detail) error {
|
||||
return r.submitMessage(categoryDebug, message, details)
|
||||
}
|
||||
|
||||
// Progress submits a progress message.
|
||||
func (r *Reporter) Progress(message string, details ...Detail) error {
|
||||
return r.submitMessage(categoryProgress, message, details)
|
||||
}
|
||||
|
||||
func (r *Reporter) submitMessage(category messageCategory, message string, details []Detail) error {
|
||||
if r == nil || r.state == nil {
|
||||
return ErrReporterClosed
|
||||
}
|
||||
state := r.state
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
if state.closed {
|
||||
return ErrReporterClosed
|
||||
}
|
||||
|
||||
details = append([]Detail(nil), details...)
|
||||
hasDetails := false
|
||||
for _, detail := range details {
|
||||
if detail != nil {
|
||||
hasDetails = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if message == "" && !hasDetails {
|
||||
return nil
|
||||
}
|
||||
|
||||
visible := state.visible(category)
|
||||
destination := state.destination(destinationDiagnostics)
|
||||
destination.submitted = true
|
||||
if !visible {
|
||||
destination.suppressed = true
|
||||
if !hasDetails {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var renderErr, writeErr error
|
||||
if visible && message != "" {
|
||||
writer := &coordinatedWriter{state: state, destination: destinationDiagnostics}
|
||||
_, writeErr = writer.Write([]byte(messageLine(category, message)))
|
||||
writeErr = writer.writeError
|
||||
}
|
||||
if visible && writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
for _, detail := range details {
|
||||
if detail == nil {
|
||||
continue
|
||||
}
|
||||
var detailErr, detailWriteErr error
|
||||
if visible {
|
||||
writer := &coordinatedWriter{state: state, destination: destinationDiagnostics}
|
||||
detailErr = detail(writer)
|
||||
detailWriteErr = writer.writeError
|
||||
} else {
|
||||
detailErr = detail(io.Discard)
|
||||
}
|
||||
detailRenderErr := recordRendererErrorLocked(state, destinationDiagnostics, detailErr, detailWriteErr)
|
||||
if renderErr == nil {
|
||||
renderErr = detailRenderErr
|
||||
}
|
||||
if writeErr == nil {
|
||||
writeErr = detailWriteErr
|
||||
}
|
||||
}
|
||||
if visible && renderErr == nil && writeErr == nil {
|
||||
destination.delivered = true
|
||||
}
|
||||
return combineOperationErrors(renderErr, writeErr)
|
||||
}
|
||||
|
||||
func (state *reportingState) visible(category messageCategory) bool {
|
||||
switch category {
|
||||
case categoryInfo, categoryWarning:
|
||||
return state.mode == modeVerbose || state.mode == modeDebug
|
||||
case categoryDebug:
|
||||
return state.mode == modeDebug
|
||||
case categoryProgress:
|
||||
if state.mode == modeQuiet {
|
||||
return false
|
||||
}
|
||||
if state.progressSet {
|
||||
return state.progress
|
||||
}
|
||||
return state.mode == modeDebug
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func messageLine(category messageCategory, message string) string {
|
||||
prefix := ""
|
||||
switch category {
|
||||
case categoryWarning:
|
||||
prefix = "WARN: "
|
||||
case categoryDebug:
|
||||
prefix = "DEBUG: "
|
||||
}
|
||||
return prefix + strings.TrimSuffix(message, "\n") + "\n"
|
||||
}
|
||||
|
||||
func finishRenderLocked(state *reportingState, destination reportingDestination, renderErr, writeErr error) error {
|
||||
renderErr = recordRendererErrorLocked(state, destination, renderErr, writeErr)
|
||||
return combineOperationErrors(renderErr, writeErr)
|
||||
}
|
||||
|
||||
func recordRendererErrorLocked(state *reportingState, destination reportingDestination, renderErr, writeErr error) error {
|
||||
if renderErr == nil || (writeErr != nil && errors.Is(renderErr, writeErr)) {
|
||||
return nil
|
||||
}
|
||||
state.recordRenderErrorLocked(destination, renderErr)
|
||||
return renderErr
|
||||
}
|
||||
|
||||
func combineOperationErrors(renderErr, writeErr error) error {
|
||||
switch {
|
||||
case renderErr == nil:
|
||||
return writeErr
|
||||
case writeErr == nil || errors.Is(renderErr, writeErr):
|
||||
return renderErr
|
||||
default:
|
||||
return errors.Join(renderErr, writeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (state *reportingState) write(destination reportingDestination, p []byte) (int, error) {
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user