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
+31 -2
View File
@@ -19,6 +19,9 @@ import (
"context"
"errors"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/novox/mesh-host/internal/declaration"
@@ -95,8 +98,10 @@ func LookUpUser(ctx context.Context, run Runner, name string) (Login, bool, erro
out, err := run(ctx, "getent", "passwd", name)
if err != nil {
// getent's own convention: 2 means the key was not found, which is the only failure that
// means "no such user".
if strings.Contains(err.Error(), "exit status 2") {
// means "no such user". Asked of the exit code: matched as text, it looked for Go's wording
// ("exit status 2") while the host's runner says "getent exited 2", so a user that did not
// exist yet read as a database that did not answer, and no account was ever created.
if code, ok := ExitCode(err); ok && code == 2 {
return Login{}, false, nil
}
return Login{}, false, fmt.Errorf(
@@ -221,3 +226,27 @@ func For(name string) (System, error) {
func All() []System {
return []System{arch{}, alpine{}, android{}}
}
// ExitCode is the code a command exited with, when err says one: from the exit itself where the
// runner kept it, else from the words either runner shape uses ("exit status N", "<cmd> exited N").
func ExitCode(err error) (int, bool) {
if err == nil {
return 0, false
}
var exit *exec.ExitError
if errors.As(err, &exit) {
return exit.ExitCode(), true
}
if m := exitWords.FindStringSubmatch(err.Error()); len(m) == 3 {
for _, g := range m[1:] {
if g != "" {
if n, convErr := strconv.Atoi(g); convErr == nil {
return n, true
}
}
}
}
return 0, false
}
var exitWords = regexp.MustCompile(`exit status (\d+)|exited (\d+)`)