service

package
v1.0.0-rc.3 Latest Latest
Warning

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

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

Documentation

Overview

Package service installs a user-scoped supervisor unit that runs the gateway in the foreground and records that unit in the project lockfile before writing it. Rendering is pure; supervisor control is behind the Supervisor interface so tests never need a host session.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnitNotLoaded = errors.New("unit not loaded")

ErrUnitNotLoaded means the user manager does not have the unit. Destroy treats that as unsupervised: there is no Restart=on-failure to trip.

Functions

func HasEntries

func HasEntries(ctx context.Context, home string) (bool, error)

HasEntries reports whether any service entry exists.

func Hash

func Hash(text string) string

Hash is the lockfile installed hash for unit text.

func Label

func Label(name string) string

Label is the launchd label and the stable unit identity.

func Lookup

func Lookup(ctx context.Context, home, name string) (*project.Entry, error)

Lookup returns the service entry for the stack name, or nil.

func ParseLinger

func ParseLinger(output string) string

ParseLinger reads `Linger=yes` or `Linger=no` from loginctl. Anything else is unknown.

func Render

func Render(spec Spec) (string, error)

Render returns the supervisor unit text for spec. The same spec always produces the same bytes.

func ResolveBinary

func ResolveBinary(override string) (string, error)

ResolveBinary chooses the path the unit will execute.

An absolute executable --binary wins. Otherwise, if LookPath("gridctl") and os.Executable resolve to the same file, the LookPath result is written (the stable Homebrew symlink). If they differ, os.Executable is written so a development binary is not replaced by a release on PATH. The unit never relies on PATH to find gridctl.

func SanitizeName

func SanitizeName(name string) string

SanitizeName maps a stack name onto the supervisor unit alphabet [A-Za-z0-9._-]. Other runes become "-", runs of "-" collapse, and leading and trailing "-" and "." are trimmed.

func UnitFileName

func UnitFileName(name string) string

UnitFileName is the systemd unit filename (not a path).

func UnitPath

func UnitPath(home string, homeOverridden bool, platform Platform, name string) string

UnitPath returns the supervisor unit path for name.

Linux uses <home>/.config/systemd/user/gridctl-<name>.service, except that XDG_CONFIG_HOME is honored when it is set and either the home is not overridden or the cleaned value is inside home. An XDG_CONFIG_HOME outside an overridden home is ignored so `gridctl --home /tmp/demo service install` cannot write into the real ~/.config. macOS always uses <home>/Library/LaunchAgents/dev.gridctl.<name>.plist.

Types

type AttentionError

type AttentionError struct {
	Msg string
}

AttentionError is an exit-1 condition: the user has to choose or finish a step. The unit and lock entry are left as the message describes.

func (*AttentionError) Error

func (e *AttentionError) Error() string

type BuildInput

type BuildInput struct {
	// Platform overrides the host. Empty uses the host platform.
	Platform Platform
	// StackName is the raw stack name. StackFile is absolute.
	StackName string
	StackFile string
	Port      int
	BasePort  int
	Watch     bool
	// RuntimeFlag is the global --runtime value, forwarded only when set.
	RuntimeFlag string
	Home        string
	// HomeOverridden reports state.HomeOverridden so the unit path stays
	// under an overridden home.
	HomeOverridden bool
	// Environ is the process environment to filter. Nil means os.Environ
	// is not consulted here; the caller passes the slice.
	Environ []string
	// PassphraseFile is an absolute path already verified by the caller.
	// It is written as GRIDCTL_VAULT_PASSPHRASE_FILE and is never read here.
	PassphraseFile string
	// Binary, when set, is used as-is (the caller already validated
	// --binary). Empty resolves the gridctl binary.
	Binary string
	Pack   string
}

BuildInput is the resolved install request BuildSpec turns into a Spec.

type InfraError

type InfraError struct {
	Msg string
}

InfraError is an exit-2 condition: the supervisor binary is missing or its output cannot be read.

func (*InfraError) Error

func (e *InfraError) Error() string

type InstallOptions

type InstallOptions struct {
	Spec           Spec
	DryRun         bool
	Force          bool
	NoEnable       bool
	Replace        bool
	HomeOverridden bool
	Notes          []string
}

InstallOptions configures one install.

type InstallResult

type InstallResult struct {
	Name        string            `json:"name"`
	Platform    string            `json:"platform"`
	UnitPath    string            `json:"unit_path"`
	Label       string            `json:"label"`
	ExecStart   []string          `json:"exec_start"`
	Environment map[string]string `json:"environment"`
	Enabled     bool              `json:"enabled"`
	Running     bool              `json:"running"`
	PID         int               `json:"pid"`
	Linger      string            `json:"linger,omitempty"`
	Notes       []string          `json:"notes,omitempty"`
	DryRun      bool              `json:"dry_run"`
	Text        string            `json:"text,omitempty"`
	WouldEnable bool              `json:"would_enable,omitempty"`
}

InstallResult is the machine-readable install document.

type KV

type KV struct {
	Key   string
	Value string
}

KV is one environment assignment, in unit order.

func FilterEnvironment

func FilterEnvironment(environ []string, home string) (kept []KV, skipped []string, err error)

FilterEnvironment keeps a positive allowlist from environ, in this order: PATH, DOCKER_HOST, DOCKER_CONTEXT, CONTAINER_HOST, GRIDCTL_RUNTIME, GRIDCTL_NO_SKILL_UPDATE_CHECK, any GRIDCTL_EXPERIMENTAL_*, and SSH_AUTH_SOCK only when its cleaned value is under $XDG_RUNTIME_DIR, /run/user/, or <home>/.ssh/. GRIDCTL_HOME is never copied; the caller writes it from the resolved home.

skipped lists a rejected SSH_AUTH_SOCK value (the path only). Every other variable is dropped silently. A kept key that is also in vault.InternalCredentialKeys is an error (defense in depth; the allowlist cannot produce one).

type LogOptions

type LogOptions struct {
	Lines  int
	Follow bool
	Stdout io.Writer
	Stderr io.Writer
}

LogOptions configures a log follow.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns service lock entries and the unit files they point at.

func NewManager

func NewManager(home string, sup Supervisor) *Manager

NewManager builds a manager for home. sup may be nil only in tests that never call the supervisor; production callers pass NewSupervisor.

func (*Manager) Install

func (m *Manager) Install(ctx context.Context, opts InstallOptions) (InstallResult, error)

Install records the unit in the lockfile and writes it. Dry-run checks conflict and drift against the read-only lock and returns before Set, Save, or the file write.

func (*Manager) Linger

func (m *Manager) Linger(ctx context.Context) (string, error)

Linger reports the user-manager linger state.

func (*Manager) Preview

func (m *Manager) Preview(ctx context.Context, force bool) ([]ResetRow, error)

Preview lists the units reset would touch. force matches Remove, so a dry-run with --force does not report kept-drift for a unit Remove would delete. It does not call the supervisor.

func (*Manager) Remove

func (m *Manager) Remove(ctx context.Context, force bool) ([]ResetRow, error)

Remove disables and stops each unit, removes the file when the hash matches (or force), and drops the record. Per-item failures are rows.

func (*Manager) Status

func (m *Manager) Status(ctx context.Context, name string) ([]Row, error)

Status returns one row per service entry, or the named stack.

func (*Manager) Stop

func (m *Manager) Stop(ctx context.Context, name string) (StopReport, error)

Stop stops a loaded unit without disabling it and without KillDaemon. A missing entry reports Present false. A unit that is not loaded is not a stop failure: the supervisor does not own the process.

func (*Manager) Uninstall

func (m *Manager) Uninstall(ctx context.Context, name string, opts UninstallOptions) (UninstallResult, error)

Uninstall disables and stops the unit, removes the file when the hash matches (or Force), and drops the lock entry. A drifted unit without Force is kept and reported. A stop failure leaves the record so the command can be re-run.

type Platform

type Platform string

Platform is the supervisor the unit targets.

const (
	PlatformSystemd Platform = "systemd"
	PlatformLaunchd Platform = "launchd"
)

func HostPlatform

func HostPlatform() (Platform, error)

HostPlatform reports the supervisor for this operating system.

type ResetRow

type ResetRow struct {
	Name   string
	Client string
	Path   string
	Action string
	Detail string
	Error  string
}

ResetRow is the reset adapter's view of one unit. resetops converts it.

type Row

type Row struct {
	Name        string `json:"name"`
	Platform    string `json:"platform"`
	State       string `json:"state"`
	Enabled     string `json:"enabled"`
	PID         int    `json:"pid,omitempty"`
	Port        int    `json:"port,omitempty"`
	Stack       string `json:"stack"`
	Unit        string `json:"unit"`
	Linger      string `json:"linger,omitempty"`
	Remediation string `json:"remediation,omitempty"`
	Label       string `json:"label,omitempty"`
	Pack        string `json:"pack,omitempty"`
	Detail      string `json:"detail,omitempty"`
}

Row is one status line.

type RuntimeState

type RuntimeState struct {
	// State is running, failed, inactive, not-loaded, or unknown.
	State   string
	Enabled bool
	PID     int
	Detail  string
}

RuntimeState is the parsed supervisor view of one unit.

func ParseLaunchctlPrint

func ParseLaunchctlPrint(output string) RuntimeState

ParseLaunchctlPrint parses `launchctl print` loosely. Only `state = ` and `pid = ` are read. Anything else is unknown rather than an error, because the text format is not stable across macOS versions.

func ParseSystemctlShow

func ParseSystemctlShow(output string) RuntimeState

ParseSystemctlShow parses `systemctl show -p ...` key=value lines. running requires ActiveState=active and SubState=running. A unit the manager does not know is not-loaded.

type Spec

type Spec struct {
	Platform               Platform
	Name                   string // sanitized service name
	StackName              string // raw stack name
	Label                  string
	Binary                 string
	Argv                   []string
	Env                    []KV
	Home                   string
	LogPath                string
	UnitPath               string
	PodmanSocketDependency bool
	StackFile              string
	Pack                   string
	Port                   int
	BasePort               int
}

Spec is the complete input to Render. It carries no I/O.

func BuildSpec

func BuildSpec(ctx context.Context, in BuildInput) (Spec, []string, error)

BuildSpec resolves binary, argv, filtered environment, home, unit path, and label. Runtime detection is advisory and never returned as an error.

type StopReport

type StopReport struct {
	// Present is true when a service lock entry exists.
	Present bool
	Path    string
	// OwnedPID is the supervisor's main PID when that process is the
	// recorded daemon, or the unit was active. Zero means the supervisor
	// does not own the recorded process. Destroy must not KillDaemon an
	// owned PID: Restart=on-failure would start it again.
	OwnedPID int
	// StoppedUnit is true when a loaded unit was stopped. Destroy prints
	// the re-enable message only then.
	StoppedUnit bool
	// NotLoaded means the user manager does not have the unit, or no
	// supervisor binary is installed. Destroy continues as unsupervised.
	NotLoaded bool
}

StopReport is what destroy needs after asking the supervisor to stop.

type Supervisor

type Supervisor interface {
	Enable(ctx context.Context, spec Spec) error
	Disable(ctx context.Context, spec Spec) error
	// Stop stops the unit without disabling it. destroy uses this so
	// Restart=on-failure does not see a killed process.
	Stop(ctx context.Context, spec Spec) error
	Query(ctx context.Context, spec Spec) (RuntimeState, error)
	Logs(ctx context.Context, spec Spec, opts LogOptions) error
	// Linger reports "yes", "no", or "unknown". Non-Linux supervisors
	// return "unknown" and a nil error.
	Linger(ctx context.Context) (string, error)
	// DaemonReload refreshes the user manager after a unit file is
	// removed. Non-Linux supervisors return nil.
	DaemonReload(ctx context.Context) error
	// Restart reloads unit text and restarts a unit the supervisor
	// already owns. It is not KillDaemon: a five-second SIGKILL would
	// trip Restart=on-failure.
	Restart(ctx context.Context, spec Spec) error
}

Supervisor controls one user-scoped service manager. Implementations shell out to systemctl, loginctl, journalctl, or launchctl. Tests inject a fake.

func NewSupervisor

func NewSupervisor() Supervisor

NewSupervisor returns the host supervisor. A missing binary is reported when a method runs, not here, so status and reset can still read the lockfile on a machine without systemctl.

type UninstallOptions

type UninstallOptions struct {
	Force  bool
	DryRun bool
}

UninstallOptions configures uninstall.

type UninstallResult

type UninstallResult struct {
	Name     string `json:"name"`
	Action   string `json:"action"`
	UnitPath string `json:"unit_path,omitempty"`
	Detail   string `json:"detail,omitempty"`
	DryRun   bool   `json:"dry_run"`
}

UninstallResult is one uninstall outcome.

Jump to

Keyboard shortcuts

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