Add deferred reporting finalization
This commit is contained in:
169
reporter.go
169
reporter.go
@@ -40,6 +40,7 @@ type reportingState struct {
|
||||
mode reportingMode
|
||||
progress bool
|
||||
progressSet bool
|
||||
deferred []deferredMessage
|
||||
}
|
||||
|
||||
type destinationState struct {
|
||||
@@ -51,6 +52,12 @@ type destinationState struct {
|
||||
writeError error
|
||||
}
|
||||
|
||||
type deferredMessage struct {
|
||||
category messageCategory
|
||||
message string
|
||||
details []Detail
|
||||
}
|
||||
|
||||
// Reporter coordinates presentation and delivery for one invocation.
|
||||
//
|
||||
// The invocation supplies the reporter. Consumers use Output for substantive
|
||||
@@ -59,6 +66,12 @@ type Reporter struct {
|
||||
state *reportingState
|
||||
}
|
||||
|
||||
// DeferredReporter queues messages for delivery during invocation finalization.
|
||||
// Copies share the same invocation reporting state.
|
||||
type DeferredReporter struct {
|
||||
state *reportingState
|
||||
}
|
||||
|
||||
type managedOutputWriter struct {
|
||||
state *reportingState
|
||||
destination reportingDestination
|
||||
@@ -149,6 +162,162 @@ func (r *Reporter) Progress(message string, details ...Detail) error {
|
||||
return r.submitMessage(categoryProgress, message, details)
|
||||
}
|
||||
|
||||
// AtEnd returns a view for messages delivered after the handler returns.
|
||||
func (r *Reporter) AtEnd() DeferredReporter {
|
||||
if r == nil {
|
||||
return DeferredReporter{}
|
||||
}
|
||||
return DeferredReporter{state: r.state}
|
||||
}
|
||||
|
||||
// Info queues an informational message for invocation finalization.
|
||||
func (r DeferredReporter) Info(message string, details ...Detail) error {
|
||||
return r.enqueue(categoryInfo, message, details)
|
||||
}
|
||||
|
||||
// Warn queues a warning message for invocation finalization.
|
||||
func (r DeferredReporter) Warn(message string, details ...Detail) error {
|
||||
return r.enqueue(categoryWarning, message, details)
|
||||
}
|
||||
|
||||
// Debug queues a debug message for invocation finalization.
|
||||
func (r DeferredReporter) Debug(message string, details ...Detail) error {
|
||||
return r.enqueue(categoryDebug, message, details)
|
||||
}
|
||||
|
||||
func (r DeferredReporter) enqueue(category messageCategory, message string, details []Detail) error {
|
||||
if r.state == nil {
|
||||
return ErrReporterClosed
|
||||
}
|
||||
state := r.state
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
if state.closed {
|
||||
return ErrReporterClosed
|
||||
}
|
||||
state.deferred = append(state.deferred, deferredMessage{
|
||||
category: category,
|
||||
message: message,
|
||||
details: append([]Detail(nil), details...),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) finalize(outcome Outcome, completion func(*Reporter)) Outcome {
|
||||
if r == nil || r.state == nil {
|
||||
return normalizeOutcome(outcome)
|
||||
}
|
||||
state := r.state
|
||||
state.mu.Lock()
|
||||
if state.closed {
|
||||
outcome.Presentation = state.presentationLocked()
|
||||
state.mu.Unlock()
|
||||
return normalizeOutcome(outcome)
|
||||
}
|
||||
deferred := append([]deferredMessage(nil), state.deferred...)
|
||||
state.deferred = nil
|
||||
state.mu.Unlock()
|
||||
|
||||
for _, message := range deferred {
|
||||
_ = r.submitMessage(message.category, message.message, message.details)
|
||||
}
|
||||
if completion != nil {
|
||||
completion(r)
|
||||
}
|
||||
|
||||
state.mu.Lock()
|
||||
if presentationFailedLocked(state) {
|
||||
outcome = applyPresentationFailure(outcome)
|
||||
}
|
||||
if outcome.Status != 0 {
|
||||
outcome = ensureFallbackExplanation(outcome)
|
||||
if state.diagnostics.writeError == nil {
|
||||
_ = renderTerminalLocked(state, outcome.Explanation)
|
||||
}
|
||||
}
|
||||
if presentationFailedLocked(state) {
|
||||
outcome = applyPresentationFailure(outcome)
|
||||
}
|
||||
state.closed = true
|
||||
state.deferred = nil
|
||||
outcome.Presentation = state.presentationLocked()
|
||||
state.mu.Unlock()
|
||||
return normalizeOutcome(outcome)
|
||||
}
|
||||
|
||||
func normalizeOutcome(outcome Outcome) Outcome {
|
||||
if outcome.Kind == "" && outcome.Status == 0 && outcome.Domain == "" {
|
||||
outcome.Kind = OutcomeSuccess
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
func presentationFailedLocked(state *reportingState) bool {
|
||||
return state.output.renderError != nil || state.output.writeError != nil ||
|
||||
state.diagnostics.renderError != nil || state.diagnostics.writeError != nil
|
||||
}
|
||||
|
||||
func applyPresentationFailure(outcome Outcome) Outcome {
|
||||
switch outcome.Kind {
|
||||
case OutcomeDomain:
|
||||
if outcome.Status == 0 {
|
||||
outcome.Status = 1
|
||||
}
|
||||
case OutcomeSuccess, OutcomeUsage, OutcomeOperational:
|
||||
outcome.Kind = OutcomeOperational
|
||||
outcome.Status = 1
|
||||
case "":
|
||||
outcome.Kind = OutcomeOperational
|
||||
outcome.Status = 1
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
func ensureFallbackExplanation(outcome Outcome) Outcome {
|
||||
if outcome.Explanation.Message != "" {
|
||||
return outcome
|
||||
}
|
||||
for _, detail := range outcome.Explanation.Details {
|
||||
if detail != nil {
|
||||
return outcome
|
||||
}
|
||||
}
|
||||
outcome.Explanation = Explanation{Message: "Command failed."}
|
||||
return outcome
|
||||
}
|
||||
|
||||
func renderTerminalLocked(state *reportingState, explanation Explanation) error {
|
||||
destination := state.destination(destinationDiagnostics)
|
||||
destination.submitted = true
|
||||
var renderErr, writeErr error
|
||||
if explanation.Message != "" {
|
||||
writer := &coordinatedWriter{state: state, destination: destinationDiagnostics}
|
||||
_, _ = writer.Write([]byte("ERROR: " + strings.TrimSuffix(explanation.Message, "\n") + "\n"))
|
||||
writeErr = writer.writeError
|
||||
}
|
||||
if writeErr == nil {
|
||||
for _, detail := range explanation.Details {
|
||||
if detail == nil {
|
||||
continue
|
||||
}
|
||||
writer := &coordinatedWriter{state: state, destination: destinationDiagnostics}
|
||||
detailErr := detail(writer)
|
||||
detailWriteErr := writer.writeError
|
||||
detailRenderErr := recordRendererErrorLocked(state, destinationDiagnostics, detailErr, detailWriteErr)
|
||||
if renderErr == nil {
|
||||
renderErr = detailRenderErr
|
||||
}
|
||||
if writeErr == nil {
|
||||
writeErr = detailWriteErr
|
||||
}
|
||||
}
|
||||
}
|
||||
if renderErr == nil && writeErr == nil {
|
||||
destination.delivered = true
|
||||
}
|
||||
return combineOperationErrors(renderErr, writeErr)
|
||||
}
|
||||
|
||||
func (r *Reporter) submitMessage(category messageCategory, message string, details []Detail) error {
|
||||
if r == nil || r.state == nil {
|
||||
return ErrReporterClosed
|
||||
|
||||
Reference in New Issue
Block a user