daemon

package
v0.26.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package daemon implements the long-lived ppz daemon: IPC server, on-disk state, NATS connection, and HTTP client to ppz-server.

Index

Constants

This section is empty.

Variables

View Source
var ErrCredsRequired = errors.New("credentials required")
View Source
var ErrUnauthorized = errors.New("daemon.RefreshLoop: unauthorized")

ErrUnauthorized is what RefreshFn returns when the bearer was revoked / expired — distinct from transient network failures (which the loop retries). Triggers OnUnauthorized + stop.

Functions

func Call

func Call(sock, method string, params, result any) error

Call sends one request to the daemon over a fresh connection and decodes either result or error.

func CursorKey

func CursorKey(orgID, handle, channel string) string

CursorKey builds the per-channel key from the canonical subject parts.

func IsAlive

func IsAlive(pid int) bool

IsAlive returns true if the PID-bearing process exists.

func PIDFromHome

func PIDFromHome(home string) int

PIDFromHome reads $PPZ_HOME/daemon.pid. Returns 0 if absent.

Types

type Credentials

type Credentials struct {
	URL     string `json:"url"`
	APIKey  string `json:"api_key"`
	OrgID   string `json:"org_id,omitempty"`
	OrgName string `json:"org_name,omitempty"`

	// Auth V2 Phase 3 — short-lived NATS user credentials.
	// Re-fetched periodically by the daemon's refresh goroutine.
	NATSUserJWT  string `json:"nats_user_jwt,omitempty"`
	NATSUserSeed string `json:"nats_user_seed,omitempty"`
}

Credentials are persisted at $PPZ_HOME/credentials (mode 0600). OrgID + OrgName are stored alongside URL+APIKey so a SIGHUP / file-poller reload doesn't drop the resolved org info (the alternative would be re-calling /auth/exchange after every reload).

type Daemon

type Daemon struct {
	Home    string
	Sock    string
	State   *State
	HTTP    *http.Client
	NC      *nats.Conn // nil until Login
	NATSURL string
	Cursors *cursors

	// Phase 3.5 — JWT refresh loop. Started on Login (and restored
	// in ensureNATS for daemon restarts), holds the latest minted
	// (jwt, seed) for the current org, and re-runs /auth/exchange
	// at exp-30s. nats.Connect's UserJWT callback reads from
	// Refresh.Current() so reconnects pick up fresh credentials.
	// Stopped on Logout / replaced on Login.
	Refresh *RefreshLoop

	// Phase 0 (agent hardening) — short tail of NATS connection-state
	// transitions. Surfaced by `ppz status` (latest state) and
	// `ppz diag` (full ring). Initialised in New() so the handlers
	// registered on the very first nats.Connect have a non-nil ring
	// to append to.
	NATSEvents *NATSEventRing
}

Daemon is the singleton process per $PPZ_HOME.

func New

func New(home, sock string) *Daemon

func (*Daemon) Run

func (d *Daemon) Run(ctx context.Context) error

Run runs the daemon in the foreground. Returns when ctx is cancelled or the listener fails.

type NATSEvent added in v0.26.0

type NATSEvent struct {
	Type   string    `json:"type"`
	At     time.Time `json:"at"`
	Reason string    `json:"reason,omitempty"`
}

NATSEvent is one entry in the daemon's NATS connection-state log. Type is one of "connect", "disconnect", "reconnect", "closed" — matching the nats.go handler set we register. At is the moment the handler fired. Reason captures the error string for disconnect / closed events (empty for the others). Phase 0 is observe-only — these events drive `ppz status` and `ppz diag` output but do not influence reconnect behaviour.

type NATSEventRing added in v0.26.0

type NATSEventRing struct {
	// contains filtered or unexported fields
}

NATSEventRing is a fixed-capacity, append-only, drop-oldest ring of NATSEvent records. Used by the daemon to surface a recent tail of connection-state transitions through `ppz diag`.

Thread-safe: every accessor takes mu. The ring lives on Daemon and is initialised in New() — before any nats.Connect call — so the handlers we register can append without nil-checking.

func (*NATSEventRing) Append added in v0.26.0

func (r *NATSEventRing) Append(typ, reason string, at time.Time)

Append records one event. When the ring is full the oldest entry is dropped — diag's value is precisely catching transient events that happened "a few minutes ago", so we keep the tail and lose the head.

func (*NATSEventRing) Snapshot added in v0.26.0

func (r *NATSEventRing) Snapshot() []NATSEvent

Snapshot returns a copy of the current events in chronological order (oldest first). Safe to retain — the returned slice is independent of the ring's internal storage.

type RefreshFn

type RefreshFn func(ctx context.Context, orgID string) (jwt, seed string, expUnix int64, err error)

RefreshFn is the work the refresh loop calls when a JWT is about to expire. Implementations re-run POST /api/v1/auth/exchange and return the new (jwt, seed). orgID lets a multi-org daemon route to the right account.

type RefreshLoop

type RefreshLoop struct {
	OrgID          string
	Refresh        RefreshFn
	OnUnauthorized func(orgID string)
	// contains filtered or unexported fields
}

RefreshLoop monitors one (org, JWT) pair and refreshes it before expiry. Concurrency: Current() may be called from any goroutine; Start/Stop must be called from the same goroutine.

func (*RefreshLoop) Current

func (r *RefreshLoop) Current() (jwt, seed string)

Current returns the freshest (jwt, seed) — used by nats.UserJWT() callbacks on every NATS (re)connect.

func (*RefreshLoop) LastRefreshAt added in v0.17.0

func (r *RefreshLoop) LastRefreshAt() time.Time

LastRefreshAt returns when the loop last accepted fresh credentials. Start counts as the first refresh because its credentials came from /auth/exchange immediately before the loop was started.

func (*RefreshLoop) RefreshNowIfDue added in v0.17.9

func (r *RefreshLoop) RefreshNowIfDue(ctx context.Context, now time.Time) (bool, error)

RefreshNowIfDue refreshes synchronously when the cached credential is already inside its refresh window. It covers machines waking from sleep: the timer goroutine may not have run yet, but the next command must not continue with an expired JWT.

func (*RefreshLoop) Start

func (r *RefreshLoop) Start(ctx context.Context, jwt, seed string, expUnix int64) error

Start begins the refresh goroutine with an initial credential. expUnix is the JWT's `exp` claim in unix seconds.

func (*RefreshLoop) Stop

func (r *RefreshLoop) Stop()

Stop halts the refresh goroutine. Idempotent.

type State

type State struct {
	// contains filtered or unexported fields
}

State is the daemon's in-memory mirror of on-disk credentials + current handle. "current" is keyed by session id (tty / $PPZ_SESSION) so each terminal window has its own current source — same scoping as cursors, avoids the "new terminal silently inherits a stale current set hours ago in another window" footgun.

func NewState

func NewState(home string) *State

func (*State) ClearCurrent

func (s *State) ClearCurrent(session string) error

ClearCurrent drops this session's current. Used by `ppz disconnect`. Idempotent — clearing an already-clear session is a no-op.

func (*State) ClearCurrentForHandle added in v0.21.0

func (s *State) ClearCurrentForHandle(handle string) error

ClearCurrentForHandle drops every session whose current equals handle. Called after a source destroy so stale per-session pointers don't linger.

func (*State) Credentials

func (s *State) Credentials() (*Credentials, bool)

func (*State) Current

func (s *State) Current(session string) string

func (*State) ForgetPipe added in v0.21.0

func (s *State) ForgetPipe(handle string)

ForgetPipe removes a handle from the known-pipes cache. Called after a source is destroyed so the cache doesn't return stale hits.

func (*State) Home

func (s *State) Home() string

func (*State) KeyPrefix

func (s *State) KeyPrefix() string

func (*State) KnowsPipe

func (s *State) KnowsPipe(handle string) bool

func (*State) LoadFromDisk

func (s *State) LoadFromDisk() error

LoadFromDisk reads credentials and current from $PPZ_HOME. Called at startup and on SIGHUP. Missing files mean "not logged in" / "no current".

`current.json` is the session→handle map. The legacy plain-text `current` file (pre-per-session) is migrated into session "default" if both exist.

func (*State) LoginCheck

func (s *State) LoginCheck() string

LoginCheck returns the cached server-validation result. "" means "not observed yet" — callers (e.g. status) should probe.

func (*State) OrgID

func (s *State) OrgID() string

func (*State) OrgName

func (s *State) OrgName() string

func (*State) RememberPipe

func (s *State) RememberPipe(handle string)

func (*State) ResetPipes

func (s *State) ResetPipes(handles []string)

func (*State) SetCurrent

func (s *State) SetCurrent(session, handle string) error

func (*State) SetLogin

func (s *State) SetLogin(creds Credentials, orgID, orgName, keyPrefix string) error

func (*State) SetLoginCheck

func (s *State) SetLoginCheck(state string)

SetLoginCheck records the latest server-validation result. Called from callServer based on response status. Idempotent — same value twice is fine, but transitions (ok→invalid, invalid→ok) are kept.

func (*State) SetOrgID

func (s *State) SetOrgID(id string)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL