Capture and report the supervised Stalwart process's output
A dry run against a real 0.15.5 instance failed with:
recovery mode did not come up: http://127.0.0.1:8081/ did not become
reachable within 1m0s: connect: connection refused
Stalwart had explained itself immediately - "Failed to bind to [::]:8080:
Address already in use" - into a pipe nothing was reading. Diagnosing a
one-line problem took several rounds because the tool threw away the only
evidence. Anything that reports a supervised process failing has to be able
to say why.
Process now captures the child's combined stdout and stderr into a bounded
buffer (64 KiB, keeping the most recent output, with truncation marked
rather than silent - a dead server's reason is at the end of its log), and
exposes it via Output(). recovery.Run appends it to both the startup-timeout
and settings-apply failures, and validate.BootCheck to its boot failure.
Fixes a second bug found while testing the first: Stop returned early when
Signal reported the process had already exited, so cmd.Wait was never
called. Wait is what reaps the child AND waits for the goroutines copying
its output - so the output was discarded in exactly the case where it
matters most, the server dying on its own. os.ErrProcessDone is now treated
as "already gone, still reap it".
The test reproduces the original failure shape: hold the port, start the
helper, let the health check time out, and assert the child's own bind
error survived. Confirmed against the smoke VM too - the same run now ends
with Stalwart's "Address already in use (os error 98)" printed inside the
tool's error.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/LINUXexpert-org/stalwart-migrator/internal/checkpoint"
|
||||
@@ -87,13 +88,13 @@ func Run(ctx context.Context, store *checkpoint.Store, rs *checkpoint.RunState,
|
||||
startupTimeout = 60 * time.Second
|
||||
}
|
||||
if healthErr := WaitForHealthy(ctx, opts.HTTPClient, opts.ListenURL, startupTimeout); healthErr != nil {
|
||||
return checkpoint.StepOutcome{}, fmt.Errorf("recovery mode did not come up: %w", healthErr)
|
||||
return checkpoint.StepOutcome{}, fmt.Errorf("recovery mode did not come up: %w%s", healthErr, outputSuffix(proc))
|
||||
}
|
||||
|
||||
if applyErr := ApplyAll(ctx, ApplyOptions{
|
||||
CLIBinaryPath: opts.CLIBinaryPath, URL: opts.ListenURL, User: opts.AdminUser, Password: password,
|
||||
}, opts.ApplyFiles); applyErr != nil {
|
||||
return checkpoint.StepOutcome{}, fmt.Errorf("settings apply failed: %w", applyErr)
|
||||
return checkpoint.StepOutcome{}, fmt.Errorf("settings apply failed: %w%s", applyErr, outputSuffix(proc))
|
||||
}
|
||||
|
||||
return checkpoint.StepOutcome{
|
||||
@@ -108,3 +109,16 @@ func Run(ctx context.Context, store *checkpoint.Store, rs *checkpoint.RunState,
|
||||
report.Results = append(report.Results, CheckResult{Name: "recovery-cycle", Status: StatusOK, Detail: outcome.Detail})
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// outputSuffix renders a supervised process's captured output for
|
||||
// appending to an error, or nothing if it produced none. The server's own
|
||||
// words are usually the whole diagnosis - a bind conflict, a rejected
|
||||
// config value - and without them the caller is left guessing at a
|
||||
// timeout.
|
||||
func outputSuffix(proc *Process) string {
|
||||
out := strings.TrimSpace(proc.Output())
|
||||
if out == "" {
|
||||
return " (the process produced no output)"
|
||||
}
|
||||
return fmt.Sprintf("\n--- output from the supervised Stalwart process ---\n%s\n--- end of output ---", out)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user