Add deferred reporting finalization

This commit is contained in:
2026-10-26 02:44:41 +00:00
parent 5df88a6d94
commit 6ba9338c0a
3 changed files with 373 additions and 1 deletions

View File

@@ -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