Documentation
¶
Index ¶
- Constants
- Variables
- func AcquireWorkspaceLock(root string) (func(), error)
- func EntryPath(cwd string, pid int) string
- func IsAlive(e Entry) bool
- func IsOrphan(e Entry) bool
- func LockPath(root string) string
- func LockPidPath(root string) string
- func Reap(e Entry) error
- func RegistryDir() string
- func Remove(cwd string) error
- func RemoveByPID(cwd string, pid int) error
- func WorkspaceID(cwd string) string
- func Write(e Entry) error
- type Entry
- type ErrLockHeld
- type HubStatus
- type Info
Constants ¶
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 ¶
var ErrNotFound = errors.New("registry: entry not found")
ErrNotFound is returned by Read when no entry exists for the given cwd.
var HealthTimeout = 500 * time.Millisecond
Functions ¶
func AcquireWorkspaceLock ¶ added in v0.9.0
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 ¶
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 ¶
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
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:
- e.Cwd does not exist on disk (ENOENT)
- e.Cwd exists but its workspace marker (.bones/agent.id) does not
- 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
LockPath returns the absolute path of the workspace lock file for the workspace at root.
func LockPidPath ¶ added in v0.9.0
LockPidPath returns the absolute path of the sibling pid file recording the lock holder.
func Reap ¶ added in v0.7.0
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 ¶
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
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 ¶
WorkspaceID returns a deterministic 16-hex-char identifier for an absolute cwd. Used as the registry filename prefix: ~/.bones/workspaces/<id>-<pid>.json.
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
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 ¶
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
Orphans returns all registry entries whose process is alive but whose workspace is gone. Read-only; the caller decides what to do.
func Read ¶
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
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.
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
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.