registry

package
v0.9.2 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const (
	LockFileName    = "hub.lock"
	LockPidFileName = "hub.lock.pid"
)

LockFileName is the basename of the workspace-scoped lock file `bones hub start` acquires before any port bind, URL-file write, or hub bootstrap. Lives at <root>/.bones/hub.lock alongside the URL files and pid files (per the issue brief: "workspace-scoped lock file path lives under the workspace's bones-state directory"). The sibling hub.lock.pid records the holder for human-readable error messages.

Variables

View Source
var ErrNotFound = errors.New("registry: entry not found")

ErrNotFound is returned by Read when no entry exists for the given cwd.

View Source
var HealthTimeout = 500 * time.Millisecond

Functions

func AcquireWorkspaceLock added in v0.9.0

func AcquireWorkspaceLock(root string) (func(), error)

AcquireWorkspaceLock takes an exclusive non-blocking advisory lock on <root>/.bones/hub.lock. Used by `bones hub start` to refuse a second concurrent start against the same workspace before any side effects (port bind, URL-file write, fork-exec).

Returns a release func that drops the lock + removes the sibling pid file. If the lock is held by a live process, returns *ErrLockHeld. If the sibling pid file names a dead process, the stale pid is overwritten and the lock is reclaimed (the kill -9 recovery path from the issue's acceptance criteria).

Cross-platform: on Unix this is syscall.Flock(LOCK_EX|LOCK_NB); on Windows it falls back to a best-effort pid-file probe (see lock_windows.go) since flock is not available there.

func EntryPath

func EntryPath(cwd string, pid int) string

EntryPath returns the absolute path of the JSON file for a given workspace cwd and hub pid. The pid is encoded into the filename so concurrent hub starts against the same workspace produce distinct files (#208 acceptance criterion (c)).

func IsAlive

func IsAlive(e Entry) bool

IsAlive returns true if BOTH (a) the recorded HubPID is alive on this host AND (b) GET <HubURL> succeeds at the TCP/HTTP level within HealthTimeout. Both checks are required because a recycled PID can pass (a) but fail (b).

The HTTP probe doesn't require any specific endpoint — any HTTP response (including 4xx) means the port is bound and serving, which is what we actually want to know. The Fossil HTTP server bones uses doesn't expose a /health endpoint and we deliberately don't add a sidecar HTTP server just for the probe.

func IsOrphan added in v0.7.0

func IsOrphan(e Entry) bool

IsOrphan reports whether e represents a process that is alive on this host but whose workspace is no longer reachable. Three signals qualify a workspace as gone:

  1. e.Cwd does not exist on disk (ENOENT)
  2. e.Cwd exists but its workspace marker (.bones/agent.id) does not
  3. e.Cwd resolves into the user's Trash (~/.Trash on macOS, the XDG-Trash equivalent on Linux)

The PID-alive check is the same one IsAlive uses; an entry whose PID is dead is not an orphan (it's a stale entry that the read- time prune will delete; see prune.go).

Since #229's read-time self-prune, signal (1) and the dead-PID case are removed at the registry layer before IsOrphan is reached. IsOrphan still tests them defensively in case a new caller routes around List/Orphans, but in practice this function returns true only for the marker-missing or trashed-cwd signals.

func LockPath added in v0.9.0

func LockPath(root string) string

LockPath returns the absolute path of the workspace lock file for the workspace at root.

func LockPidPath added in v0.9.0

func LockPidPath(root string) string

LockPidPath returns the absolute path of the sibling pid file recording the lock holder.

func Reap added in v0.7.0

func Reap(e Entry) error

Reap terminates the process for e and removes its registry entry. SIGTERM first; if the PID is still alive after reapGrace, SIGKILL. Returns nil on success (process gone, entry removed) or an error describing which step failed. The entry is removed even after SIGKILL — leaving it would create a permanent registry record for a process that, by definition, isn't going to come back.

func RegistryDir

func RegistryDir() string

RegistryDir returns the directory that holds workspace entry files.

func Remove

func Remove(cwd string) error

Remove deletes ALL registry entries for the given workspace cwd — every per-pid file plus the legacy unsuffixed path. Idempotent. Used by `bones down` and stale-entry pruners that operate at workspace granularity. Callers that want to drop a single entry by pid should use RemoveByPID instead.

func RemoveByPID added in v0.9.0

func RemoveByPID(cwd string, pid int) error

RemoveByPID deletes the single registry entry for (cwd, pid). Idempotent: missing files are not an error. Used by Reap and by foreground hub.Start's defer cleanup so a workspace with two live entries can lose one without forgetting the other.

func WorkspaceID

func WorkspaceID(cwd string) string

WorkspaceID returns a deterministic 16-hex-char identifier for an absolute cwd. Used as the registry filename prefix: ~/.bones/workspaces/<id>-<pid>.json.

func Write

func Write(e Entry) error

Write persists e to its file atomically (tmp+rename). Creates the registry directory if missing. Filename is keyed by (cwd, HubPID) — see EntryPath.

Types

type Entry

type Entry struct {
	Cwd       string    `json:"cwd"`
	Name      string    `json:"name"`
	HubURL    string    `json:"hub_url"`
	NATSURL   string    `json:"nats_url"`
	HubPID    int       `json:"hub_pid"`
	StartedAt time.Time `json:"started_at"`
}

Entry is one workspace's registry record. Each `bones hub start` writes its own entry at ~/.bones/workspaces/<WorkspaceID>-<HubPID>.json — pid in the filename so two concurrent starts against the same workspace produce two entries (the duplicate-hub case from #208) rather than the second silently overwriting the first.

func Duplicates added in v0.9.0

func Duplicates(cwd string) ([]Entry, error)

Duplicates returns every registry entry whose canonical Cwd matches the given workspace AND whose HubPID is alive on this host. When the length is 0 or 1, the workspace has no concurrent hubs (steady state). When the length is >= 2, the issue described in #208 is present: two `bones hub start` invocations are competing for the same workspace's URL files and fossil state.

Sibling primitive of Orphans(): both walk the per-pid registry, both filter on PID liveness, both stay read-only. Doctor and status consume Duplicates and emit WARN-class output; reaping is left to the operator (the brief defers automatic kill of duplicates to a dedicated verb, mirroring ADR 0043's read-only-doctor doctrine).

PID-alive is the only liveness signal — no HTTP probe — because the duplicate scenario typically includes one hub that lost the port race and is no longer serving HTTP. Such a hub is still holding fossil inodes and overwriting URL files; the operator needs to know about it even if HealthTimeout would mark it down.

func List

func List() ([]Entry, error)

List returns all registry entries, skipping corrupt files. Includes every per-pid entry so two concurrent hubs against one workspace surface as two list rows (caller decides whether to dedupe).

Self-prunes stale entries on read (#229): any entry whose HubPID is not alive on this host OR whose Cwd no longer exists is deleted from disk before the surviving set is returned. ADR 0043 promises the registry "prunes on read"; pre-#229 only the in-memory filter honored that — the on-disk files accumulated indefinitely.

func Orphans added in v0.7.0

func Orphans() ([]Entry, error)

Orphans returns all registry entries whose process is alive but whose workspace is gone. Read-only; the caller decides what to do.

func Read

func Read(cwd string) (Entry, error)

Read loads the workspace's most-recent registry entry. If multiple entries exist (the duplicate-hub case), the alive one with the latest StartedAt wins; if none are alive, the latest StartedAt is returned. Use Duplicates(cwd) when the caller wants every entry.

Self-prunes the cwd-scoped entries on read (#229): any matched file whose HubPID is dead OR whose Cwd no longer exists is removed before the survivors compete for "best." Returns ErrNotFound when every entry was crud (or none existed to start).

type ErrLockHeld added in v0.9.0

type ErrLockHeld struct {
	PID  int
	Path string
}

ErrLockHeld is returned by AcquireWorkspaceLock when another live process holds the workspace lock. The PID is the holder (parsed from hub.lock.pid); callers surface it in CLI output so the operator can act.

func (*ErrLockHeld) Error added in v0.9.0

func (e *ErrLockHeld) Error() string

type HubStatus added in v0.8.0

type HubStatus string

HubStatus is a coarse liveness label produced by ListInfo. Values:

HubRunning  — registry recorded a hub PID, the PID is alive on this host,
              and a quick TCP/HTTP probe of HubURL returned a response.
HubStopped  — registry has an entry, but the hub is not reachable
              (PID dead OR HTTP probe failed).
HubUnknown  — the entry has no enough info to probe (e.g. missing
              HubURL/HubPID), or a probe was deliberately skipped.
const (
	HubRunning HubStatus = "running"
	HubStopped HubStatus = "stopped"
	HubUnknown HubStatus = "unknown"
)

type Info added in v0.8.0

type Info struct {
	Entry

	ID          string    `json:"id"`
	AgentID     string    `json:"agent_id"`
	HubStatus   HubStatus `json:"hub_status"`
	LastTouched time.Time `json:"last_touched"`
}

Info is one workspace registry record enriched with the on-disk filename (ID), file mtime (LastTouched), the workspace's agent.id marker (when present), and a coarse hub liveness label.

ID is the hex string used as the registry filename: ~/.bones/workspaces/<ID>.json. It equals WorkspaceID(Cwd).

func ListInfo added in v0.8.0

func ListInfo() ([]Info, error)

ListInfo enumerates every registry entry, attaching ID + mtime + agent.id + hub-status. Corrupt or unreadable files are skipped (matching List). The hub-status probe runs IsAlive on each entry; callers that want to skip the probe (e.g. for fast paths) should use List + their own enrichment.

Self-prunes stale entries on read (#229) — same predicate as List(): a dead HubPID or a missing Cwd both qualify the entry as crud and the file is removed before this function returns.

Results are sorted by Name, then Cwd, for stable output across calls.

Jump to

Keyboard shortcuts

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