package cli import ( "context" "errors" "fmt" "os" "strings" "time" "banger/internal/config" "banger/internal/installmeta" "banger/internal/model" "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) } currentUID = os.Getuid ) // ensureDaemon validates that the current CLI user matches the // installed banger owner, then pings the system socket. Every CLI // command that needs to talk to the daemon routes through here. func (d *deps) ensureDaemon(ctx context.Context) (paths.Layout, model.DaemonConfig, error) { meta, metaErr := loadInstallMetadata() if metaErr == nil && currentUID() != meta.OwnerUID { return paths.Layout{}, model.DaemonConfig{}, fmt.Errorf("banger is installed for %s; switch to that user or reinstall with `sudo banger system install --owner %s`", meta.OwnerUser, userHint()) } if metaErr != nil && !errors.Is(metaErr, os.ErrNotExist) { return paths.Layout{}, model.DaemonConfig{}, fmt.Errorf("load %s: %w", installmeta.DefaultPath, metaErr) } userLayout, err := paths.Resolve() if err != nil { return paths.Layout{}, model.DaemonConfig{}, err } cfg, err := config.Load(userLayout) if err != nil { return paths.Layout{}, model.DaemonConfig{}, err } layout := paths.ResolveSystem() if _, err := d.daemonPing(ctx, layout.SocketPath); err == nil { return layout, cfg, nil } if metaErr == nil { return paths.Layout{}, model.DaemonConfig{}, fmt.Errorf("banger service not reachable at %s; run `sudo banger system restart`", layout.SocketPath) } return paths.Layout{}, model.DaemonConfig{}, fmt.Errorf("banger service not running at %s; run `sudo banger system install`", layout.SocketPath) } func userHint() string { if sudoUser := strings.TrimSpace(os.Getenv("SUDO_USER")); sudoUser != "" { return sudoUser } if user := strings.TrimSpace(os.Getenv("USER")); user != "" { return user } return "" }