Documentation
¶
Index ¶
- Constants
- Variables
- func AcquireWorkspaceLock(root string) (func(), error)
- func EntryPath(cwd string) 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 ReapEntry(e Entry) error
- func ReapPID(pid int) error
- func Register(cwd, name string) error
- func RegistryDir() string
- func Remove(cwd string) error
- func ResolveCwd(p string) string
- func WorkspaceID(cwd string) string
- func Write(e Entry) error
- type Entry
- type ErrLockHeld
- type HubProcess
- type HubStatus
- type Info
- type OrphanHub
- type OrphanSource
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. The PID is no longer encoded in the filename (issue #250); the workspace registry is single-file-per-workspace and last-writer-wins. Per-host orphan visibility is now provided by `bones status --all` (issue #264) which scans the host process table — superseding the duplicate-detection role the PID-suffix originally played in #208.
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. Honors BONES_DIR (issue #291) when set.
func LockPidPath ¶ added in v0.9.0
LockPidPath returns the absolute path of the sibling pid file recording the lock holder. Honors BONES_DIR when set.
func Reap ¶ added in v0.7.0
Reap terminates the process for e and removes its registry entry. Thin wrapper over ReapEntry preserved for backwards compatibility with the existing test surface; new callers should select between ReapEntry (registry-tracked) and ReapPID (process-only) explicitly.
func ReapEntry ¶ added in v0.15.1
ReapEntry 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 ReapPID ¶ added in v0.15.1
ReapPID terminates pid (SIGTERM-then-SIGKILL like ReapEntry) but does not touch the registry. Used for process-source orphans surfaced by AllOrphanHubs that have no registry entry to remove. A no-op if pid is already dead.
func Register ¶ added in v0.13.0
Register persists a PID=0 "registered but idle" entry for cwd (#305). Used by `bones up` so the workspace is visible to `bones status --all` between `bones up` and the first verb that triggers a hub serve.
Idempotent: if an entry already exists with HubPID > 0 (a live or recently-live hub), Register is a no-op so the hub-written record is preserved. If the existing entry is a PID=0 register row, it's refreshed with the new name and timestamp.
`bones down` removes entries via Remove; a hub start overwrites the idle row with a PID-bearing entry via Write.
func RegistryDir ¶
func RegistryDir() string
RegistryDir returns the directory that holds workspace entry files.
func Remove ¶
Remove deletes the registry entry for the given workspace cwd — the canonical <id>.json plus any legacy <id>-<pid>.json files left over from pre-#250 layouts. Idempotent. Used by `bones down` and stale-entry pruners that operate at workspace granularity.
func ResolveCwd ¶ added in v0.15.1
ResolveCwd returns the symlink-resolved absolute form of p, falling back to filepath.Clean(p) when EvalSymlinks fails (path doesn't exist, permission denied, broken symlink chain). Used to normalize both registry-side cwds (as stored, e.g. "/tmp/foo") and process- side cwds (as lsof returns them, e.g. "/private/tmp/foo") to the same canonical form before comparison.
Without this normalization a workspace at /tmp/foo on macOS (where /tmp is a symlink to /private/tmp) shows in `bones status --all` twice (#353); the registry entry keyed by /tmp/foo (which preserves /tmp) misses the lookup keyed by the lsof-resolved /private/tmp path.
EvalSymlinks does NOT make a filesystem call when p has no symlink components, so the fallback path on Linux (where /tmp is a real directory) costs roughly the same as filepath.Clean.
func WorkspaceID ¶
WorkspaceID returns a deterministic 16-hex-char identifier for an absolute cwd. Used as the registry filename prefix: ~/.bones/workspaces/<id>.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>.json — one file per workspace (the supervisor model). Older bones binaries used the per-PID layout `<id>-<pid>.json` (pre-#250); the read path migrates legacy files into the canonical name silently on first read.
func List ¶
List returns all registry entries, skipping corrupt files.
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
deprecated
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.
Deprecated: prefer AllOrphanHubs, which also surfaces process-only orphans (live `bones hub start` PIDs with no registry entry at all). Retained for the existing test surface; new callers should not use it.
func Read ¶
Read loads the workspace's registry entry. Single-file layout (#250): the canonical path is <id>.json. Legacy per-PID files (`<id>-<pid>.json`) are migrated on read into the canonical name and then deleted — silent, best-effort. If multiple legacy entries exist the alive-PID one wins (or the most recent if all are dead). If the canonical file AND legacy files both exist (a half-migrated workspace) the canonical file is preferred and the legacy files are removed.
Self-prunes the workspace's entry on read (#229): if the chosen entry's HubPID is dead OR its Cwd no longer exists, the file is removed before returning ErrNotFound.
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 HubProcess ¶ added in v0.11.0
type HubProcess struct {
PID int
ETime string // raw ps "elapsed" column, e.g. "2-14:05:09" or "19:23"
Cwd string // absolute path, or "" when undiscoverable
Cmd string // full command line as ps reported it
}
HubProcess is a live `bones hub start` process discovered by scanning the host process table. Used by `bones status --all` to surface orphan hubs (#264): hubs whose PID is alive but whose workspace cwd is missing, isn't in the registry, or whose registry entry doesn't match the live PID.
Cwd is best-effort. On Linux it's read from /proc/<pid>/cwd; on macOS via `lsof -p <pid> -d cwd`. If neither path resolves the cwd (process exited, sandboxed, lsof unavailable), Cwd is left empty and the renderer surfaces it as "unknown" rather than dropping the row.
func LiveHubProcesses ¶ added in v0.11.0
func LiveHubProcesses() ([]HubProcess, error)
LiveHubProcesses scans the host for running `bones hub start` processes and returns them with parsed cwd, pid, etime, and the original command. Best-effort: a process whose cwd is unreadable still appears in the result with Cwd == "". A `ps` failure (e.g. binary missing, sandboxed) returns an error; callers may render only Section 1 and continue.
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.
type OrphanHub ¶ added in v0.15.1
type OrphanHub struct {
Source OrphanSource
Entry Entry
Process HubProcess
PID int
Cwd string
Reason string
}
OrphanHub is the unified shape returned by AllOrphanHubs. The Source discriminant tells callers which struct fields are meaningful:
- Source==SourceRegistry: Entry is populated; PID/Cwd are mirrored from Entry.HubPID/Entry.Cwd for renderer convenience.
- Source==SourceProcess: Process is populated; Entry is zero; PID/Cwd are mirrored from Process.PID/Process.Cwd. The renderer uses Process.ETime in lieu of Entry.StartedAt.
Reason carries a short human-readable string describing why the orphan was flagged; used by `bones status --all`'s tabular output.
func AllOrphanHubs ¶ added in v0.15.1
AllOrphanHubs returns the union of registry-side orphans (entries where IsOrphan is true) and process-only orphans (live `bones hub start` PIDs with no matching registry entry).
Discovery proceeds in two passes:
- List() + IsOrphan filter — produces SourceRegistry rows.
- LiveHubProcesses() minus the live PIDs from (1)+remaining List entries — produces SourceProcess rows for processes the registry doesn't account for.
Cwd comparison uses ResolveCwd so a workspace path like /tmp/foo compares equal to the lsof-resolved /private/tmp/foo on macOS (#353).
Best-effort: a LiveHubProcesses failure (e.g. no `ps` on PATH) is not fatal — only registry-source orphans are returned in that case, alongside the underlying error.
type OrphanSource ¶ added in v0.15.1
type OrphanSource int
OrphanSource discriminates how an OrphanHub was discovered. Process- only orphans (no registry entry at all) used to be invisible to `bones hub reap`, `bones doctor`, and `bones down` because those callers all used Orphans() which only filters List(). AllOrphanHubs unions both kinds; the Source field tells the caller which fields of OrphanHub are populated.
const ( // SourceRegistry: the orphan was discovered via the registry // (entry exists, IsOrphan returned true). Entry is fully populated. SourceRegistry OrphanSource = iota // SourceProcess: the orphan is a live `bones hub start` process // with no matching registry entry. Process is populated; Entry is // the zero value. SourceProcess )