buildah writes STEP lines on stdout and its error on stderr, so the last line of both hid every image build's cause. mesh-build-image's reason is now stderr's last lines (600 bytes) with the exit status, and the journal gets both tails; mesh-build-node keeps stderr's end and stdout's start apart; mesh-build-go's cause skips go build's '# package' line.
91 lines
2.8 KiB
Go
91 lines
2.8 KiB
Go
package main
|
|
|
|
// Why a command failed, as the command said it (issue 1703). buildah writes its progress (STEP n/m, the RUN's own
|
|
// output) on stdout and its error on stderr, so the last line of both together is a STEP line and hides the error:
|
|
// a failure's reason is stderr's last lines, never stdout's, and the whole tail of stderr is said in the journal.
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// CauseBytes bounds what a failure's reason carries of the command's output; TailBytes bounds what the journal gets.
|
|
const (
|
|
CauseBytes = 600
|
|
TailBytes = 4096
|
|
)
|
|
|
|
// cause says why a command failed: its exit status (or why it never ran to one) and the last lines it wrote on stderr,
|
|
// at most CauseBytes of them, joined with " | ". Only when stderr is empty does stdout's tail stand in, said as such.
|
|
func cause(r forgekey.Ran) string {
|
|
head := exitOf(r)
|
|
if said := tailLines(r.Stderr, CauseBytes, " | "); said != "" {
|
|
return head + ": " + said
|
|
}
|
|
if said := tailLines(r.Stdout, CauseBytes, " | "); said != "" {
|
|
return head + ", nothing on stderr; stdout ends: " + said
|
|
}
|
|
return head + ", and it wrote nothing"
|
|
}
|
|
|
|
// exitOf is how a command ended: why it never ran to an exit status, or the status.
|
|
func exitOf(r forgekey.Ran) string {
|
|
if r.Err != "" {
|
|
return r.Err
|
|
}
|
|
return fmt.Sprintf("exit status %d", r.Status)
|
|
}
|
|
|
|
// tailLines is the last non-empty lines of s, trimmed, joined with sep, at most max bytes: the lines are taken whole
|
|
// from the end while they fit, and when even the last one does not, its end is kept behind "…".
|
|
func tailLines(s string, max int, sep string) string {
|
|
var lines []string
|
|
for _, l := range strings.Split(s, "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
lines = append(lines, l)
|
|
}
|
|
}
|
|
var keep []string
|
|
n := 0
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
add := len(lines[i])
|
|
if len(keep) > 0 {
|
|
add += len(sep)
|
|
}
|
|
if n+add > max {
|
|
if len(keep) == 0 {
|
|
keep = append(keep, "…"+cutFront(lines[i], max-len("…")))
|
|
}
|
|
break
|
|
}
|
|
keep = append([]string{lines[i]}, keep...)
|
|
n += add
|
|
}
|
|
return strings.Join(keep, sep)
|
|
}
|
|
|
|
// cutFront is the last at most max bytes of s, starting on a whole character.
|
|
func cutFront(s string, max int) string {
|
|
if max <= 0 {
|
|
return ""
|
|
}
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
s = s[len(s)-max:]
|
|
for len(s) > 0 && !utf8.RuneStart(s[0]) {
|
|
s = s[1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// sayFailure writes the tail of a failed command's stderr (and of its stdout, where buildah's RUN output goes) to the
|
|
// journal, so what the reason had to leave out can still be read there.
|
|
func (m *Module) sayFailure(what string, r forgekey.Ran) {
|
|
m.say("%s failed (%s); stderr ends:\n%s\nstdout ends:\n%s", what, exitOf(r),
|
|
tailLines(r.Stderr, TailBytes, "\n"), tailLines(r.Stdout, TailBytes, "\n"))
|
|
}
|