cli: wait for the daemon socket to answer ping after install/restart

systemd's Type=simple reports a unit "active" the moment its
ExecStart binary is exec()'d, which for bangerd happens well before
the daemon has read its config and bound /run/banger/bangerd.sock.
'banger system install' and 'banger system restart' both returned
inside that window, so the very next 'banger ...' command would hit
ensureDaemon, miss on a single ping, and exit with "service not
reachable; run sudo banger system restart" — the same restart that
had just succeeded. Smoke tripped over this on every run.

Add waitForDaemonReady: poll daemonPing for up to 15s after the
restart returns. Both the system install and restart paths now
block until the daemon is genuinely accepting RPCs, so the next
CLI invocation can talk to it without retrying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-26 21:22:31 -03:00
parent 679cf87cfd
commit 74e5a7cedb
No known key found for this signature in database
GPG key ID: 33112E6833C34679
2 changed files with 38 additions and 0 deletions

View file

@ -97,6 +97,9 @@ See docs/privileges.md for the full trust model.
if err := d.runSystemctl(cmd.Context(), "restart", installmeta.DefaultService); err != nil { if err := d.runSystemctl(cmd.Context(), "restart", installmeta.DefaultService); err != nil {
return err return err
} }
if err := d.waitForDaemonReady(cmd.Context(), paths.ResolveSystem().SocketPath); err != nil {
return err
}
_, err := fmt.Fprintln(cmd.OutOrStdout(), "restarted") _, err := fmt.Fprintln(cmd.OutOrStdout(), "restarted")
return err return err
}, },
@ -184,6 +187,9 @@ func (d *deps) runSystemInstall(ctx context.Context, out io.Writer, ownerFlag st
if err := d.runSystemctl(ctx, "restart", installmeta.DefaultService); err != nil { if err := d.runSystemctl(ctx, "restart", installmeta.DefaultService); err != nil {
return err return err
} }
if err := d.waitForDaemonReady(ctx, installmeta.DefaultSocketPath); err != nil {
return err
}
_, err = fmt.Fprintf(out, "installed\nowner: %s\nsocket: %s\nhelper_socket: %s\nservice: %s\nhelper_service: %s\n", meta.OwnerUser, installmeta.DefaultSocketPath, installmeta.DefaultRootHelperSocketPath, installmeta.DefaultService, installmeta.DefaultRootHelperService) _, err = fmt.Fprintf(out, "installed\nowner: %s\nsocket: %s\nhelper_socket: %s\nservice: %s\nhelper_service: %s\n", meta.OwnerUser, installmeta.DefaultSocketPath, installmeta.DefaultRootHelperSocketPath, installmeta.DefaultService, installmeta.DefaultRootHelperService)
return err return err
} }

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time"
"banger/internal/config" "banger/internal/config"
"banger/internal/installmeta" "banger/internal/installmeta"
@ -13,6 +14,37 @@ import (
"banger/internal/paths" "banger/internal/paths"
) )
const (
daemonReadyTimeout = 15 * time.Second
daemonReadyPollInterval = 100 * time.Millisecond
)
// waitForDaemonReady blocks until the daemon at socketPath answers
// ping, the context is cancelled, or daemonReadyTimeout elapses.
// Used by `system install` and `system restart` so they don't return
// before the daemon has actually finished binding its socket — the
// systemd Type=simple unit reports "active" the moment the binary
// is exec()'d, well before bangerd has read its config and listened
// on the unix socket.
func (d *deps) waitForDaemonReady(ctx context.Context, socketPath string) error {
deadline := time.Now().Add(daemonReadyTimeout)
pingCtx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
for {
if _, err := d.daemonPing(pingCtx, socketPath); err == nil {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("daemon did not become ready at %s within %s", socketPath, daemonReadyTimeout)
}
select {
case <-pingCtx.Done():
return fmt.Errorf("daemon did not become ready at %s: %w", socketPath, pingCtx.Err())
case <-time.After(daemonReadyPollInterval):
}
}
}
var ( var (
loadInstallMetadata = func() (installmeta.Metadata, error) { loadInstallMetadata = func() (installmeta.Metadata, error) {
return installmeta.Load(installmeta.DefaultPath) return installmeta.Load(installmeta.DefaultPath)