busctl

package
v1.0.54 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package busctl glues the consume client to the bus daemon. It encapsulates the three operations a consumer needs at startup and shutdown:

discover: find the running bus or fork a fresh one (race-free, plan §12
          P3 "try dial → try fork lock → retry dial")
spawn:    exec `dws event _bus --client-id <id>` as a detached
          background process (stdio detach, setsid, ready pipe handshake)
stop:     gracefully terminate the bus daemon (SIGTERM + IPC fallback)

All three operations are short-lived helpers — they own no long-lived goroutines and return whole errors to their caller.

Index

Constants

View Source
const DefaultStatusRPCTimeout = 2 * time.Second

DefaultStatusRPCTimeout caps how long QueryStatus waits for the bus to reply. 2s is generous — the bus's status_resp is a synchronous in-memory snapshot, sub-millisecond in practice; the timeout exists only to bound pathological cases (bus stuck in shutdown).

View Source
const DefaultStopTimeout = 5 * time.Second

DefaultStopTimeout is the wall-clock budget Stop waits for the bus to exit after the signal is sent. 5s covers the bus's own graceful tear-down (broadcast Bye → consumer goroutines drain → cleanup) with margin.

View Source
const ReadyFDEnv = "DWS_EVENT_BUS_READY_FD"

ReadyFDEnv is the env var the spawned `event _bus` child inspects to find the ready-pipe write end. The parent passes the FD number; child opens it via os.NewFile(fd, "ready") and writes 'R' on success or 'E' on failure. 3 is the first FD slot beyond stdio in cmd.ExtraFiles.

Variables

View Source
var ErrNotRunning = errors.New("busctl: bus is not running")

ErrNotRunning indicates bus.lock either does not exist or its recorded PID is not alive. Stop returns this as a sentinel so the caller can distinguish "nothing to stop" from "failed to stop".

View Source
var ErrSpawnFailed = errors.New("busctl: bus child reported startup failure on ready pipe")

ErrSpawnFailed is returned when the child reports startup failure via the ready pipe ('E' byte). The child's exit error / log file holds the actual cause; this sentinel just lets the caller distinguish "ready pipe said no" from "ready pipe timed out / closed early".

View Source
var ErrSpawnTimeout = errors.New("busctl: bus child did not signal readiness within deadline")

ErrSpawnTimeout is returned when ReadyTimeout elapses without any signal.

View Source
var ReadyTimeout = 10 * time.Second

ReadyTimeout caps how long Spawn waits for the child to signal readiness. 10s is generous — bus startup is local-only work (file I/O + socket bind), so 1s would normally suffice; the extra headroom covers cold-start keychain prompts and slow CI machines.

Functions

func Discover

func Discover(cfg DiscoverConfig) (net.Conn, error)

Discover returns a connected net.Conn to the bus for cfg.ClientID. If the bus is not running, Discover forks a new one and waits for it to come up.

Race-free three-step (plan §12 P3):

  1. try dial IPC endpoint → success → done
  2. failed → call Spawn (fork _bus); Spawn blocks until ready pipe says 'R' (or returns ErrSpawnFailed if another process won the race and our bus startup hit ErrBusy on the lock)
  3. dial again — should succeed; retry with backoff up to DialDeadline in case Spawn succeeded but socket bind has tiny latency

On concurrent Discover by N processes: only one Spawn succeeds (the others get ErrBusy via the bus daemon's own lock acquisition). Losers fall through to the retry-dial loop in step 3 and connect to the bus the winner brought up.

func LockPath

func LockPath(workDir string) string

LockPath returns the canonical bus.lock path for the given working dir. Centralised so consume / status / stop all agree on the location.

func MetaPath

func MetaPath(workDir string) string

MetaPath returns the canonical bus.meta path.

func QueryStatus

func QueryStatus(endpoint string) (*transport.StatusResp, error)

QueryStatus dials the bus IPC, sends Hello with Role=status, sends a StatusReq, reads exactly one StatusResp, and closes. Returns the decoded response or an error if any step fails.

Used by `dws event status` and `dws event list` to fetch live per-consumer / per-event-type counters. The bus's handleStatusRPC path (see internal/event/bus/daemon.go) handles this connection without registering with the Hub — ad-hoc tooling does not count as a consumer in `status.active_consumers`.

func ReadyFDFromEnv

func ReadyFDFromEnv() *os.File

ReadyFDFromEnv returns the inherited ready pipe (or nil if not set). The `event _bus` command handler calls this at startup, passes the returned *os.File to bus.Run as Config.ReadyPipe, and the bus signals readiness through it.

func Spawn

func Spawn(cfg SpawnConfig) (pid int, err error)

Spawn forks a detached `dws event _bus --client-id <id>` child process and waits for it to signal readiness via the ready pipe. Returns the child's PID on success — the caller can then dial the bus IPC endpoint.

stdio detach (plan invariant #7):

  • cmd.Stdout / cmd.Stderr set to nil so the child's own writes don't pollute the parent's NDJSON stream
  • Setsid on Unix so the child survives parent SIGHUP / parent exit
  • CREATE_NEW_PROCESS_GROUP on Windows (set in spawn_windows.go)

Child startup (handled by the eventcmd._bus handler, P6):

  • Opens os.NewFile(<DWS_EVENT_BUS_READY_FD>, "ready")
  • On startup success → writes 'R' and closes
  • On startup failure → writes 'E' and closes (child exits)

Parent (this function):

  • Holds the read end open until either 1 byte is read or ReadyTimeout
  • Returns ErrSpawnFailed for 'E', ErrSpawnTimeout otherwise

func Stop

func Stop(cfg StopConfig) error

Stop signals the bus daemon for cfg.WorkDir to exit gracefully and waits for the process to actually die. Returns ErrNotRunning if no bus is running for that work dir.

Implementation note: on Unix we send SIGTERM. The bus daemon's Run loop watches its parent ctx for cancellation; the cobra `event _bus` command wires signal.NotifyContext so SIGTERM triggers ctx.Done() → graceful shutdown path. On Windows we use os.Process.Signal(os.Interrupt) which the Go runtime maps to TerminateProcess for processes outside our console group; for v1 that's acceptable (Windows graceful shutdown is future work — plan §16 v2).

Types

type BusEntry

type BusEntry struct {
	WorkDir      string              `json:"workdir"`
	Edition      string              `json:"edition"`
	SourceKind   dwsevent.SourceKind `json:"source_kind,omitempty"`
	ClientIDHash string              `json:"client_id_hash"`
	IdentityHash string              `json:"identity_hash,omitempty"`
	HolderPID    int                 `json:"holder_pid"`
	State        BusEntryState       `json:"state"`
	// Meta, if non-nil, lets list/status display the original ClientID
	// (reverse-mapped from the hash) and the bus start time.
	Meta *bus.Meta `json:"meta,omitempty"`
}

BusEntry is one bus working directory found on disk plus its detected lifecycle state. EnumerateBuses produces these; the cobra layer joins them with QueryStatus output to render the full status view.

func EnumerateBuses

func EnumerateBuses(configDir string, editionFilter string) ([]BusEntry, error)

EnumerateBuses scans <configDir>/events/<editionFilter>/*/ for bus working directories. An empty editionFilter scans every edition directory found under events/.

Returns a deterministic slice sorted by (edition, source_kind, identity_hash). Missing/inaccessible directories are skipped silently — list/status commands should still succeed when only some editions have ever run a bus.

func FindBusByClientID

func FindBusByClientID(configDir, editionName, clientIDHash string) *BusEntry

FindBusByClientID is the "current ClientID" lookup used by `event status` (no --all). Returns the entry for the given (edition, clientIDHash) pair or nil if no bus has ever started for it. The caller derives the hash using event.ClientIDHash.

func FindBusByIdentity

func FindBusByIdentity(configDir, editionName string, sourceKind dwsevent.SourceKind, identityHash string) *BusEntry

FindBusByIdentity looks up a bus in the source-kind-aware layout.

func (BusEntry) IPCEndpoint

func (e BusEntry) IPCEndpoint() string

IPCEndpoint returns the IPC endpoint for this entry. Delegates to dwsevent.IPCEndpoint so status/stop dial exactly where consume and the bus daemon bound (including the short-path fallback when WorkDir is too deep for sun_path).

type BusEntryState

type BusEntryState string

BusEntryState classifies a discovered bus directory's runtime state. Used by `dws event status/list` to render the table and by --fail-on-orphan to drive exit code.

const (
	// BusStateRunning: bus.lock holds an alive PID — the daemon is up.
	BusStateRunning BusEntryState = "running"
	// BusStateOrphan: bus.meta exists but bus.lock PID is dead. The user
	// should `dws event stop --client-id <id>` (which detects the dead
	// PID and unblocks fresh starts) or rm -rf the working directory.
	BusStateOrphan BusEntryState = "orphan"
	// BusStateNotRunning: directory exists (e.g. bus.meta retained for
	// historic reasons) but bus.lock is missing or empty. Clean state.
	BusStateNotRunning BusEntryState = "not_running"
)

type DiscoverConfig

type DiscoverConfig struct {
	WorkDir     string
	IPCEndpoint string
	ClientID    string
	// Spawn is the fork-bus callback. Default busctl.Spawn.
	Spawn SpawnFunc
	// SpawnExtraArgs is forwarded to Spawn (for tests).
	SpawnExtraArgs []string
	// DialBackoff: initial sleep between retry dials when another process
	// is spawning. Doubled each attempt up to DialMaxBackoff.
	DialBackoff time.Duration
	// DialMaxBackoff caps backoff.
	DialMaxBackoff time.Duration
	// DialDeadline caps total wall-clock time spent discovering.
	DialDeadline time.Duration
}

DiscoverConfig describes one discover attempt. WorkDir holds bus.lock and usually (on Unix) bus.sock — see dwsevent.IPCEndpoint for the short-path fallback when WorkDir is too deep; the caller must mkdir it with pkg/config.DirPerm beforehand.

type EntryStatus

type EntryStatus struct {
	Entry BusEntry              `json:"entry"`
	Live  *transport.StatusResp `json:"live,omitempty"`
}

EntryStatus combines static FS info (BusEntry) with the live RPC snapshot (StatusResp). For not_running / orphan entries Live is nil.

func QueryEntry

func QueryEntry(entry BusEntry) EntryStatus

QueryEntry fetches the live status for one BusEntry. Returns the entry wrapped with a nil Live when state != running (or when the dial fails). Errors from QueryStatus are folded into Live=nil so the caller's table rendering does not need to surface per-bus dial failures (they are already conveyed by State).

type SpawnConfig

type SpawnConfig struct {
	// ExecPath is the dws binary to exec. Default os.Executable().
	ExecPath string

	// ClientID is passed as `--client-id` to `dws event _bus`. Required.
	ClientID string

	// ExtraArgs are appended after `--client-id`. Empty for normal use; tests
	// pass `--extra-flag-for-test` etc.
	ExtraArgs []string

	// Env to pass to the child. Defaults to os.Environ(). The ReadyFDEnv
	// entry is appended automatically.
	Env []string
}

SpawnConfig describes one spawn attempt. ClientID is the only field inspected by the child; the rest govern process attributes the parent applies before exec.

type SpawnFunc

type SpawnFunc func(SpawnConfig) (pid int, err error)

SpawnFunc abstracts the fork-bus operation so tests can inject a fake instead of execing a real binary. Production callers pass busctl.Spawn.

type StopConfig

type StopConfig struct {
	// WorkDir holds bus.lock; Stop reads the PID from there.
	WorkDir string
	// Timeout is the total wall-clock budget for graceful exit. After this,
	// Stop returns an error; it does NOT escalate to SIGKILL — leave that
	// to the operator.
	Timeout time.Duration
}

StopConfig identifies the target bus and tunes timing.

Jump to

Keyboard shortcuts

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