Documentation
¶
Overview ¶
Package service runs Grafo's local background indexing supervisor, owns the user-level registry of watched repository roots, generates the platform service definitions that start the supervisor, and reports diagnostics for all of it.
Three authorities are consumed rather than duplicated: the registry owns which roots are watched, indexer.DiscoverProject owns branch and index identity, and internal/indexer owns indexing semantics. Ownership of anything Grafo writes outside a repository is proven through the agentinstall receipt ledger and bounded by agentinstall.CheckUserConfigRoot.
Index ¶
- Constants
- Variables
- func CanonicalRoot(root string) (string, string, error)
- func DefinitionBinary(goos, contents string) (string, bool)
- func InstallableBinary(reader agentinstall.Reader, executable string) (string, error)
- func LogPath(stateDir string) string
- func SharedRoots(registry Registry, root string) []string
- func StateDir(reader agentinstall.Reader) (string, error)
- func StatusPath(stateDir string) string
- func SupervisorLockPath(stateDir string) string
- func TailLog(path string, lines int) ([]string, error)
- func TryDonorIndexLock(indexPath string) (indexseed.Unlock, bool, error)
- type Action
- type AgentCheck
- type BinaryCheck
- type Diagnosis
- type Discoverer
- type DoctorOptions
- type Event
- type Finding
- type Logger
- type Options
- type Platform
- type Registry
- type RegistryCheck
- type Root
- type RootCheck
- type RootStatus
- type Runner
- type SQLiteRunner
- type Settings
- type Snapshot
- type State
- type Store
- type Supervisor
- type SupervisorCheck
- type Unlock
Constants ¶
const ( RepairPruneRoot = "prune-missing-root" RepairRefreshAgents = "refresh-agent-registration" RepairServiceDefinition = "recreate-service-definition" RepairRestartService = "restart-stale-service" )
Repair names. Exactly these repairs exist; anything else is reported with a manual step instead.
const ( MaxLogBytes = 1 << 20 LogHistory = 1 )
Log rotation bounds. The supervisor runs unattended, so its log is capped and rotated rather than allowed to grow without limit.
const ( ChangeInstalled = "installed" ChangeUpdated = "updated" ChangeRemoved = "removed" ChangeUnchanged = "unchanged" ChangeSkipped = "skipped" )
Change values reported by service operations.
const DefaultInterval = 5 * time.Second
DefaultInterval is the reconciliation interval used when a root does not pin one of its own.
const DefinitionVersion = "1"
DefinitionVersion is the version marker embedded in every generated service definition. It is compared exactly, so a definition generated by a future Grafo is reported instead of being silently replaced.
const Label = "games.cafecito.grafo.index"
Label is the launchd label and the systemd unit stem of Grafo's supervisor.
const MinimumInterval = 500 * time.Millisecond
MinimumInterval bounds how often a root may be reconciled.
const StaleAfter = 10 * time.Minute
StaleAfter is how long a supervisor may go without updating its status before it is reported stale.
const SystemdUnit = "grafo-index.service"
SystemdUnit is the user unit file name.
Variables ¶
var ErrLockBusy = errors.New("locked by another grafo process")
ErrLockBusy reports that a competing grafo process still held the lock when the wait elapsed. It is ordinary contention rather than a broken lock, so callers can match on it and render guidance in their own vocabulary.
Functions ¶
func CanonicalRoot ¶
CanonicalRoot resolves a user-supplied repository path to an absolute, symlink-resolved directory together with its display name, so the same directory registers exactly once however it is spelled. Branch and index identity are not decided here: indexer.DiscoverProject remains the authority for those, and it resolves the git top level from this path at run time.
func DefinitionBinary ¶
DefinitionBinary reports the executable an installed definition starts, parsed from the platform's documented command field.
func InstallableBinary ¶
func InstallableBinary(reader agentinstall.Reader, executable string) (string, error)
InstallableBinary resolves the executable a service definition may point at and refuses one that will not still be there when the init system starts it.
A definition names an absolute path and is read minutes, days, or reboots later, so an executable that lives in a directory the toolchain deletes on exit produces a permanently broken service. Three locations are refused, and the first two are decided structurally by containment in a directory the operating system itself reports rather than by matching names:
- the temporary directory ($TMPDIR, or $GOTMPDIR which `go run` prefers), which also covers `go test` binaries;
- the Go build cache ($GOCACHE, or the per-user cache directory's go-build subtree), where `go run` and `go build` without -o place their output;
- a path with a "go-build<digits>" element, which is the exact directory name the Go toolchain creates for a one-shot build. This last rule is the only name heuristic, it applies when the environment signals above are absent, and it is deliberately the narrowest of the three: refusing a durable location such as /opt/go-builder/bin/grafo would be as much a defect as accepting an ephemeral one, so the shape is matched rather than a prefix.
Refusing is the fail-closed choice: a diagnostic naming the cause and the remedy is strictly better than writing a unit that is guaranteed to break.
func SharedRoots ¶
SharedRoots reports the registered roots, other than root, that contain it or are contained by it. A shared root makes pruning ambiguous, so repair refuses.
func StateDir ¶
func StateDir(reader agentinstall.Reader) (string, error)
StateDir returns the Grafo configuration directory that holds the registry, the supervisor status file and the service logs.
func StatusPath ¶
StatusPath returns the supervisor status file inside the state directory.
func SupervisorLockPath ¶
SupervisorLockPath returns the lock that admits exactly one supervisor.
func TailLog ¶
TailLog returns the last lines of the supervisor log, oldest first. A missing log is reported as an empty tail rather than an error, because the service may simply not have run yet.
func TryDonorIndexLock ¶ added in v0.2.0
TryDonorIndexLock adapts the branch-index lock to the narrow capability index adoption needs. Adoption must never wait on a donor another process is refreshing, so contention is reported rather than blocked on, and the seeding package stays independent of this one.
Types ¶
type Action ¶
type Action struct {
Platform string `json:"platform"`
Kind string `json:"kind"` // "definition", "activate", "deactivate", "restart"
Target string `json:"target"`
Change string `json:"change"`
DryRun bool `json:"dry_run,omitempty"`
Detail string `json:"detail,omitempty"`
}
Action is one service-management step that was taken, or in a dry run one that would be taken.
func Install ¶
func Install(ctx context.Context, env agentinstall.Environment, binary, stateDir string, dryRun bool) ([]Action, error)
Install writes the generated definition and activates the service. It is idempotent: an already-correct definition is reported unchanged and the activation commands are replayed, which every supported init system accepts.
func Uninstall ¶
func Uninstall(ctx context.Context, env agentinstall.Environment, dryRun bool) ([]Action, error)
Uninstall deactivates the service and removes the definition, but only when a receipt proves Grafo wrote exactly the bytes at exactly this path. Ownership is decided before anything happens, so a foreign unit at Grafo's path is never stopped, disabled, or removed: stopping a service the user manages is itself a mutation, and it must not happen on a file Grafo cannot prove it wrote. Uninstalling twice is not an error.
type AgentCheck ¶
type AgentCheck struct {
Client string `json:"client"`
Display string `json:"display"`
Installed bool `json:"installed"`
Registered bool `json:"registered"`
Path string `json:"path,omitempty"`
Artifacts []struct {
Kind string `json:"kind"`
Target string `json:"target"`
Present bool `json:"present"`
Matches bool `json:"matches"`
} `json:"artifacts,omitempty"`
Detail string `json:"detail,omitempty"`
}
AgentCheck reports one supported MCP client's Grafo registration and the guidance artifacts Grafo recorded installing for it.
type BinaryCheck ¶
type BinaryCheck struct {
Path string `json:"path"`
Version string `json:"version"`
// Installed reports that the path exists and is a file.
Installed bool `json:"installed"`
// Serviceable reports that the path is durable enough for a service
// definition to name. A `go run` build is installed but not serviceable.
Serviceable bool `json:"serviceable"`
Detail string `json:"detail,omitempty"`
}
BinaryCheck reports the Grafo binary a service definition can point at.
type Diagnosis ¶
type Diagnosis struct {
Format string `json:"format"`
Binary BinaryCheck `json:"binary"`
Registry RegistryCheck `json:"registry"`
Roots []RootCheck `json:"roots"`
Service State `json:"service"`
Supervisor SupervisorCheck `json:"supervisor"`
Agents []AgentCheck `json:"agents"`
Findings []Finding `json:"findings"`
// Repairs lists what --repair actually did, in order.
Repairs []Action `json:"repairs,omitempty"`
Healthy bool `json:"healthy"`
}
Diagnosis is the whole doctor report.
func Diagnose ¶
func Diagnose(ctx context.Context, env agentinstall.Environment, options DoctorOptions) (Diagnosis, error)
Diagnose inspects the binary, the registry, every registered root, the platform service, the supervisor status and the agent registrations. Without DoctorOptions.Repair it mutates nothing at all.
type Discoverer ¶
Discoverer resolves branch and index identity for a root. Production uses indexer.DiscoverProject, which is the single authority for that identity.
type DoctorOptions ¶
type DoctorOptions struct {
// Binary is the absolute path of the running Grafo binary.
Binary string
// StateDir overrides the Grafo state directory.
StateDir string
// Repair applies the enumerated repairs. Doctor is read-only without it.
Repair bool
// Discover resolves branch identity; defaults to indexer.DiscoverProject.
Discover Discoverer
// DetectAgents reports supported MCP clients read-only; defaults to
// agentinstall.Detect over the whole registry. Doctor never extends that
// client list.
DetectAgents func(ctx context.Context, env agentinstall.Environment) ([]agentinstall.Status, error)
// RefreshAgents refreshes only the Grafo artifacts a client already has;
// defaults to agentinstall.Install with Options.Refresh.
RefreshAgents func(ctx context.Context, env agentinstall.Environment, binary string) ([]agentinstall.Action, error)
}
DoctorOptions configures a diagnosis.
type Event ¶
Event is a hint that a root may have changed. Hints are advisory: the indexer remains the authority on what actually changed. An overflow hint means the platform watcher dropped events, so the root needs a full reconciliation.
type Finding ¶
type Finding struct {
Area string `json:"area"`
Level string `json:"level"` // "error" or "warning"
Target string `json:"target,omitempty"`
Detail string `json:"detail"`
// Repair names the enumerated repair that fixes this finding, or "" when only
// a manual step can.
Repair string `json:"repair,omitempty"`
// Manual is the actionable step for a condition Grafo must not repair itself.
Manual string `json:"manual,omitempty"`
// Repaired reports that --repair fixed this finding in this run.
Repaired bool `json:"repaired,omitempty"`
}
Finding is one diagnosed condition.
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger writes the supervisor's bounded, rotating log.
Only paths, counts, timings and error text are recorded: there is no API that accepts file content, so source text and configuration values cannot reach the log even by mistake. Field values are sanitized to a single line, so an error string carrying embedded newlines cannot forge log records either.
func OpenLogger ¶
OpenLogger opens (creating if needed) the supervisor log at path.
type Options ¶
type Options struct {
// Env is the outside-world seam shared with the installer.
Env agentinstall.Environment
// Store is the watched-root registry.
Store *Store
// Concurrency bounds how many roots are indexed at once.
Concurrency int
// Discover resolves branch identity; defaults to indexer.DiscoverProject.
Discover Discoverer
// Runner performs indexing runs; defaults to the SQLite-backed indexer.
Runner Runner
// Hints is an optional platform-watcher feed. Periodic reconciliation runs
// regardless, so a missing or lossy watcher only affects latency.
Hints <-chan Event
// Logger receives bounded structured records.
Logger *Logger
// StateDir is the directory holding the status file and lock.
StateDir string
// Binary is the absolute supervisor binary path reported in status.
Binary string
}
Options configures a supervisor.
type Platform ¶
type Platform interface {
// Name reports the init system ("launchd", "systemd", or "unsupported").
Name() string
// Supported reports whether service installation works on this host.
Supported() bool
// DefinitionPath resolves the user-scoped definition file.
DefinitionPath(reader agentinstall.Reader) (string, error)
// Definition renders the exact content Grafo generates for this host, or
// refuses when a path cannot be represented safely in the definition syntax.
Definition(binary, stateDir string) (string, error)
// Restart restarts a running or stale service.
Restart(ctx context.Context, env agentinstall.Environment) error
// contains filtered or unexported methods
}
Platform is the operating-system integration for the background supervisor. It owns the generated definition content; doctor and the CLI consume it.
func PlatformFor ¶
PlatformFor returns the service adapter for the target operating system.
type RegistryCheck ¶
type RegistryCheck struct {
Path string `json:"path"`
Readable bool `json:"readable"`
Roots int `json:"roots"`
Detail string `json:"detail,omitempty"`
}
RegistryCheck reports the watched-root registry itself.
type Root ¶
type Root struct {
Root string `json:"root"`
Name string `json:"name"`
Interval string `json:"interval,omitempty"`
Paused bool `json:"paused,omitempty"`
AddedAt string `json:"added_at,omitempty"`
}
Root is one registered repository root.
type RootCheck ¶
type RootCheck struct {
Root string `json:"root"`
Name string `json:"name,omitempty"`
Paused bool `json:"paused,omitempty"`
Exists bool `json:"exists"`
Readable bool `json:"readable"`
GitManaged bool `json:"git_managed"`
Branch string `json:"branch,omitempty"`
IndexPath string `json:"index_path,omitempty"`
IndexPresent bool `json:"index_present"`
IndexBytes int64 `json:"index_bytes,omitempty"`
Status *RootStatus `json:"status,omitempty"`
Detail string `json:"detail,omitempty"`
}
RootCheck reports one registered root.
type RootStatus ¶
type RootStatus struct {
Root string `json:"root"`
Name string `json:"name,omitempty"`
Branch string `json:"branch,omitempty"`
IndexPath string `json:"index_path,omitempty"`
Paused bool `json:"paused,omitempty"`
Missing bool `json:"missing,omitempty"`
Runs int `json:"runs"`
Failures int `json:"failures"`
Updated int `json:"updated"`
Removed int `json:"removed"`
LastRunAt string `json:"last_run_at,omitempty"`
LastSuccessAt string `json:"last_success_at,omitempty"`
LastErrorAt string `json:"last_error_at,omitempty"`
LastError string `json:"last_error,omitempty"`
}
RootStatus is the supervisor's per-root state, persisted so that `grafo service status` and `grafo doctor` can report it from another process.
type Runner ¶
type Runner interface {
Index(ctx context.Context, project indexer.Project, options indexer.Options) (indexer.Report, error)
}
Runner performs one indexing run of one discovered project. The supervisor orchestrates roots and never implements a second indexer.
type SQLiteRunner ¶
type SQLiteRunner struct{}
SQLiteRunner is the production Runner: it opens the branch index the project resolved, under the per-index lock, and runs the shared indexer service.
type Settings ¶
type Settings struct {
// Interval is the reconciliation interval for this root. Zero means
// DefaultInterval.
Interval time.Duration
// Paused registers the root without indexing it.
Paused bool
}
Settings are the per-root options the user may pin when registering a root.
type Snapshot ¶
type Snapshot struct {
Format string `json:"format"`
PID int `json:"pid"`
Binary string `json:"binary,omitempty"`
StartedAt string `json:"started_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Roots []RootStatus `json:"roots"`
}
Snapshot is the whole supervisor state at one instant.
func ReadSnapshot ¶
ReadSnapshot reads the persisted supervisor status. A missing file yields an empty snapshot, because the service may simply never have run.
type State ¶
type State struct {
Platform string `json:"platform"`
Supported bool `json:"supported"`
// Definition is the generated unit or plist path.
Definition string `json:"definition,omitempty"`
Installed bool `json:"definition_installed"`
// Owned reports that a receipt proves Grafo wrote exactly the bytes on disk.
Owned bool `json:"definition_owned"`
// Conflict reports a definition at Grafo's path that Grafo cannot prove it
// wrote. It is reported and never replaced without an explicit repair.
Conflict bool `json:"definition_conflict,omitempty"`
Loaded bool `json:"loaded"`
Running bool `json:"running"`
Detail string `json:"detail,omitempty"`
}
State is what the platform reports about Grafo's installed service.
func Describe ¶
func Describe(ctx context.Context, env agentinstall.Environment) (State, error)
Describe reports the platform state without mutating anything.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes the registry. Reads never mutate anything; writes take an exclusive cross-process lock, replace the file atomically, and preserve mode 0600 where the platform supports it.
func NewStore ¶
func NewStore(env agentinstall.Environment) *Store
NewStore returns the registry store backed by env.
func (*Store) Add ¶
Add registers a root. The path is canonicalized first, so the same repository reached through a symlink or a relative path registers exactly once.
func (*Store) Load ¶
Load reads the registry. A missing file yields an empty registry; a malformed file or an unknown format is an error that reports how to recover, and the file is left byte-identical.
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor keeps every registered root incrementally indexed.
Concurrency model: exactly one supervisor process per user, admitted by an exclusive kernel-backed lock on the state directory; inside it, at most one goroutine per root and at most Concurrency roots indexing at once; and every indexing run - foreground or background - holds the per-index lock for the branch index it writes. A killed supervisor therefore needs no cleanup: the kernel drops its locks, SQLite rolls back the partial transaction, and the next run rediscovers and reconciles from the committed index.
func NewSupervisor ¶
func NewSupervisor(options Options) *Supervisor
NewSupervisor returns a supervisor with defaults applied.
func (*Supervisor) Hint ¶
func (s *Supervisor) Hint(event Event)
Hint records a change hint for one root, debouncing it and, for an overflow, scheduling a bounded full reconciliation of that root.
func (*Supervisor) Once ¶
func (s *Supervisor) Once(ctx context.Context) (Snapshot, error)
Once performs one reconciliation pass over every registered root that is due, and returns the resulting snapshot. A malformed or locked registry stops every mutation: nothing is indexed and nothing is pruned.
func (*Supervisor) Run ¶
func (s *Supervisor) Run(ctx context.Context) error
Run supervises every registered root until ctx is cancelled. It fails closed when another supervisor already holds the lock, so two daemons can never schedule the same root.
func (*Supervisor) Snapshot ¶
func (s *Supervisor) Snapshot() Snapshot
Snapshot returns the current supervisor state in deterministic root order.
type SupervisorCheck ¶
type SupervisorCheck struct {
StatusPath string `json:"status_path"`
Present bool `json:"status_present"`
PID int `json:"pid,omitempty"`
Binary string `json:"binary,omitempty"`
StartedAt string `json:"started_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Stale bool `json:"stale,omitempty"`
Detail string `json:"detail,omitempty"`
}
SupervisorCheck reports the running supervisor, as far as its status file and the init system can tell.
type Unlock ¶
type Unlock func() error
Unlock releases a lock acquired by Lock.
func IndexLock ¶
IndexLock serializes writers of one branch index. Both the background supervisor and the foreground `grafo index`/`grafo watch` commands take it, so a daemon run and a foreground run can never write the same index concurrently. The lock lives beside the index inside .grafo, which is never committed.
func Lock ¶
Lock takes an exclusive advisory cross-process lock on path, creating the file if it is missing, and waits up to wait for a competing holder to release it.
Locks are advisory but kernel-backed on Unix, which is what makes a killed supervisor safe: the operating system drops the lock when the process dies, so no stale marker has to be reasoned about and no manual cleanup is needed.
func TryIndexLock ¶
TryIndexLock attempts to serialize a branch-index mutation without waiting. A false acquired result is ordinary contention, not an operation error. The zero-byte lock anchor intentionally remains after unlock so waiters can never race a replacement inode.