Help us shape the future of Go! Take the 2026 Go Developer Survey

service

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const (
	ChangeInstalled = "installed"
	ChangeUpdated   = "updated"
	ChangeRemoved   = "removed"
	ChangeUnchanged = "unchanged"
	ChangeSkipped   = "skipped"
)

Change values reported by service operations.

View Source
const DefaultInterval = 5 * time.Second

DefaultInterval is the reconciliation interval used when a root does not pin one of its own.

View Source
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.

View Source
const Label = "games.cafecito.grafo.index"

Label is the launchd label and the systemd unit stem of Grafo's supervisor.

View Source
const MinimumInterval = 500 * time.Millisecond

MinimumInterval bounds how often a root may be reconciled.

View Source
const StaleAfter = 10 * time.Minute

StaleAfter is how long a supervisor may go without updating its status before it is reported stale.

View Source
const SystemdUnit = "grafo-index.service"

SystemdUnit is the user unit file name.

Variables

View Source
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

func CanonicalRoot(root string) (string, string, error)

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

func DefinitionBinary(goos, contents string) (string, bool)

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:

  1. the temporary directory ($TMPDIR, or $GOTMPDIR which `go run` prefers), which also covers `go test` binaries;
  2. 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;
  3. 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 LogPath

func LogPath(stateDir string) string

LogPath returns the supervisor log file inside the Grafo state directory.

func SharedRoots

func SharedRoots(registry Registry, root string) []string

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

func StatusPath(stateDir string) string

StatusPath returns the supervisor status file inside the state directory.

func SupervisorLockPath

func SupervisorLockPath(stateDir string) string

SupervisorLockPath returns the lock that admits exactly one supervisor.

func TailLog

func TailLog(path string, lines int) ([]string, error)

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

func TryDonorIndexLock(indexPath string) (indexseed.Unlock, bool, error)

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.

func (Diagnosis) Fprint

func (d Diagnosis) Fprint(out interface{ Write([]byte) (int, error) })

Fprint writes a human-readable diagnosis.

type Discoverer

type Discoverer func(ctx context.Context, root string) (indexer.Project, error)

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

type Event struct {
	Root     string
	Overflow bool
}

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

func OpenLogger(path string) (*Logger, error)

OpenLogger opens (creating if needed) the supervisor log at path.

func (*Logger) Close

func (l *Logger) Close() error

Close closes the underlying file.

func (*Logger) Path

func (l *Logger) Path() string

Path reports the log file location.

func (*Logger) Record

func (l *Logger) Record(level, message string, fields map[string]string)

Record appends one structured record. Fields are emitted in sorted key order so the log is deterministic.

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

func PlatformFor(goos string) Platform

PlatformFor returns the service adapter for the target operating system.

type Registry

type Registry struct {
	Format string `json:"format"`
	Roots  []Root `json:"roots"`
}

Registry is the user-level document listing every watched root.

func (Registry) Lookup

func (r Registry) Lookup(root string) (Root, bool)

Lookup returns the registered entry for a canonical root path.

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.

func (Root) Every

func (r Root) Every() time.Duration

Every returns the reconciliation interval of a root, falling back to the default when the stored value is absent or out of range.

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"`
	SharedWith   []string    `json:"shared_with,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"`
	Unavailable   bool   `json:"unavailable,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.

func (SQLiteRunner) Index

func (SQLiteRunner) Index(ctx context.Context, project indexer.Project, options indexer.Options) (indexer.Report, error)

Index runs one indexing pass. The per-index lock makes a background run and a foreground `grafo index` mutually exclusive on the same database.

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

func ReadSnapshot(path string) (Snapshot, error)

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

func (s *Store) Add(root string, settings Settings) (Registry, bool, error)

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

func (s *Store) Load() (Registry, error)

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.

func (*Store) Path

func (s *Store) Path() (string, error)

Path returns the registry file location.

func (*Store) Remove

func (s *Store) Remove(root string) (Registry, bool, error)

Remove unregisters a root. Removing an unregistered root is not an error, so the operation is safely replayable.

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

func IndexLock(indexPath string, wait time.Duration) (Unlock, error)

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

func Lock(path string, wait time.Duration) (Unlock, error)

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

func TryIndexLock(indexPath string) (unlock Unlock, acquired bool, err error)

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.

Jump to

Keyboard shortcuts

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