Read getent's exit code, not its wording (novox/hq issue 213)

A user that does not exist yet was matched as Go's 'exit status 2', while the host's runner says
'getent exited 2', so it read as a user database that did not answer: the controller's account was
never created and the handover to its process stopped there. The runner keeps the exit underneath
its words, and a caller asks the code.
This commit is contained in:
jochen
2026-10-04 02:11:43 +02:00
parent 2d5e76434b
commit 8280a82ef8
3 changed files with 86 additions and 4 deletions
+15 -2
View File
@@ -1458,6 +1458,15 @@ func remove(ctx context.Context, sys system.System, a store.Applied, run Runner,
// undo is never reported as done; not fatal for a former target, which was never dropped by anyone.
var errNoRemoval = errors.New("no way to remove")
// exited is a command that ran and exited non-zero: its words, and the exit itself.
type exited struct {
words string
exit *exec.ExitError
}
func (e *exited) Error() string { return e.words }
func (e *exited) Unwrap() error { return e.exit }
// ExecRunner runs a real command, with stdin closed and output captured.
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
@@ -1466,8 +1475,12 @@ func ExecRunner(ctx context.Context, name string, args ...string) (string, error
if err != nil {
var exit *exec.ExitError
if errors.As(err, &exit) {
return string(out), fmt.Errorf("%s exited %d: %s",
name, exit.ExitCode(), strings.TrimSpace(string(exit.Stderr)))
// The words as they always were, and the exit underneath them, so a caller asks the
// code (system.ExitCode) rather than matching text that this line is free to reword.
return string(out), &exited{
words: fmt.Sprintf("%s exited %d: %s", name, exit.ExitCode(), strings.TrimSpace(string(exit.Stderr))),
exit: exit,
}
}
return string(out), fmt.Errorf("%s: %w", name, err)
}