diff --git a/internal/cli/commands_system.go b/internal/cli/commands_system.go index db9134b..7e72a2a 100644 --- a/internal/cli/commands_system.go +++ b/internal/cli/commands_system.go @@ -97,6 +97,9 @@ See docs/privileges.md for the full trust model. if err := d.runSystemctl(cmd.Context(), "restart", installmeta.DefaultService); err != nil { return err } + if err := d.waitForDaemonReady(cmd.Context(), paths.ResolveSystem().SocketPath); err != nil { + return err + } _, err := fmt.Fprintln(cmd.OutOrStdout(), "restarted") 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 { 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) return err } diff --git a/internal/cli/daemon_lifecycle.go b/internal/cli/daemon_lifecycle.go index ec9f011..4c9f8c1 100644 --- a/internal/cli/daemon_lifecycle.go +++ b/internal/cli/daemon_lifecycle.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "strings" + "time" "banger/internal/config" "banger/internal/installmeta" @@ -13,6 +14,37 @@ import ( "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 ( loadInstallMetadata = func() (installmeta.Metadata, error) { return installmeta.Load(installmeta.DefaultPath)