daemonservice

package
v0.70.2 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package daemonservice owns the platform-neutral contracts for Graith's app-associated per-user daemon. Darwin-specific registration is isolated in build-tagged files; other platforms retain direct spawning.

Index

Constants

View Source
const (
	DefaultSlot          = "default"
	ControllerExecutable = "graith-service-controller"
	DaemonExecutable     = "gr"
	AppBundleName        = "Graith.app"
)

Variables

View Source
var (
	// ManagedBuild is set to true only for a release/development channel that
	// also packages a matching signed Graith.app. Source and go-install builds
	// deliberately retain direct spawning.
	ManagedBuild = "false"
	// ExpectedTeamID and ExpectedRequirementBase64 are stamped into stable builds.
	// Development-signed builds may leave them empty only when DevelopmentBuild
	// is explicitly true. The requirement is base64-encoded so codesign's
	// whitespace and quoting cannot be reinterpreted by go build's -ldflags.
	ExpectedTeamID            = ""
	ExpectedRequirementBase64 = ""
	DevelopmentBuild          = "false"
)
View Source
var (
	ErrReceiptMissing = errors.New("daemon service receipt and backup are missing")
	ErrSlotCapacity   = errors.New("all daemon service profile slots are leased")
)
View Source
var BuiltInEnvironmentNames = []string{
	"PATH", "SHELL", "TMPDIR", "LANG", "LC_*",
	"XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR",
}
View Source
var ErrPendingInProgress = errors.New("daemon service transaction is still in progress")
View Source
var ErrStartupRequestExists = errors.New("daemon service startup request already exists")

Functions

func BeginStart

func BeginStart(receipt *Receipt, intent StartIntent) error

func BundleCandidates

func BundleCandidates(executable string) []string

BundleCandidates returns only package-relative locations. It never searches /Applications or trusts an environment override because a matching app must come from the same installed release as the CLI.

func CacheRoot

func CacheRoot(uid int) (string, error)

func CancelStart

func CancelStart(receipt *Receipt, label, nonce string) error

func CanonicalPathContains

func CanonicalPathContains(parent, child string) (bool, error)

CanonicalPathContains reports whether parent contains child after applying the same fail-closed canonicalization as CanonicalPathsOverlap.

func CanonicalPathsOverlap

func CanonicalPathsOverlap(first, second string) (bool, error)

CanonicalPathsOverlap reports whether either absolute path contains the other after resolving every existing symlink component. Missing leaf components are retained beneath their nearest verified ancestor; broken symlinks and paths that cannot be inspected fail closed.

func CommitProfile

func CommitProfile(receipt *Receipt, lease Lease) error

func CompleteStart

func CompleteStart(receipt *Receipt, label, nonce string) error

func GenerateAssets

func GenerateAssets(launchAgentsDir, swiftOutput string) error

GenerateAssets writes every static LaunchAgent plist plus the Swift controller's generated lookup table from the embedded source manifest.

func GenerationID

func GenerationID(version, commit string) (string, error)

func InstallRequestEnvironment

func InstallRequestEnvironment(request StartupRequest) error

func IsManagedBuild

func IsManagedBuild() bool

func MarkCurrentProcessStopped

func MarkCurrentProcessStopped(profile string) error

MarkCurrentProcessStopped is a no-op outside a managed packaged build. A managed foreground daemon invokes it on every ordinary exit; same-PID exec never runs defers, so preserved upgrades keep their running marker intact.

func OSUserHome

func OSUserHome(uid int) (string, error)

func ProfileForDefinition

func ProfileForDefinition(definition Definition, requestedProfile string) error

func ProjectEnvironment

func ProjectEnvironment(environ []string, inherit []string, uid int) (map[string]string, error)

func QuarantineSlot

func QuarantineSlot(receipt *Receipt, slot, reason string) error

func ReceiptRoot

func ReceiptRoot(uid int) (string, error)

func ReleaseProfile

func ReleaseProfile(receipt *Receipt, profile string, confirmedDownAndUnregistered bool) error

func RemoveStartupRequest

func RemoveStartupRequest(root string, uid int, definition Definition) error

func ResolveUpgradeCandidateContext

func ResolveUpgradeCandidateContext(ctx context.Context, currentPath, version, commit string, uid int) (string, bool, error)

ResolveUpgradeCandidateContext returns the immutable cached service payload that a managed daemon may exec. Fallback installations keep using the caller's current executable.

func ServiceControlRoot

func ServiceControlRoot(int) (string, error)

func ServiceControlRootContext

func ServiceControlRootContext(context.Context, int) (string, error)

func ValidateManagedInstallationContext

func ValidateManagedInstallationContext(ctx context.Context, executable, version, commit string) error

ValidateManagedInstallationContext verifies the exact package-associated app without caching it or creating service state. Diagnostic commands use this read-only gate before reporting a managed installation as healthy.

func VerifyDarwinDistributionContext

func VerifyDarwinDistributionContext(context.Context, string) error

func WithStartLock

func WithStartLock(root string, uid int, definition Definition, fn func() error) error

func WriteStartupRequest

func WriteStartupRequest(root string, request StartupRequest) error

Types

type Bootstrap

type Bootstrap struct {
	Definition Definition
	Request    StartupRequest
	Generation Generation
	// contains filtered or unexported fields
}

func BootstrapFreshService

func BootstrapFreshService(label, slot string, now time.Time) (_ Bootstrap, returnErr error)

BootstrapFreshService is the only fresh managed-service entry point. It derives both roots from the OS account, validates the immutable cached app and receipt/lease agreement, consumes the request once, installs its minimal environment, and clears the durable nonce.

func (Bootstrap) Abort

func (bootstrap Bootstrap) Abort() error

Abort clears only the running marker installed by this fresh bootstrap. The request was already consumed, so later config/path/bootstrap failure must not leave the failed PID recorded as a live generation.

func (Bootstrap) ValidateResolvedConfig

func (bootstrap Bootstrap) ValidateResolvedConfig(configFile string, paths config.Paths) error

type BundleExpectations

type BundleExpectations struct {
	Version             string
	Commit              string
	StandalonePath      string
	TeamID              string
	Requirement         string
	AllowDevelopmentSig bool
	VerifySignature     func(string) (SignatureInfo, error)
	VerifyDistribution  func(string) error
}

type DarwinController

type DarwinController struct{}

func (DarwinController) JobState

func (DarwinController) Kickstart

func (DarwinController) Register

func (DarwinController) RegisterFresh

func (DarwinController) Status

func (DarwinController) Unregister

type Definition

type Definition struct {
	Slot      string
	Label     string
	PlistName string
}

func DefinitionForSlot

func DefinitionForSlot(slot string) (Definition, error)

func Definitions

func Definitions() []Definition

func PrepareManagedUpgrade

func PrepareManagedUpgrade(profile, candidatePath string) (Definition, func() error, bool, error)

PrepareManagedUpgrade validates candidatePath as an exact embedded payload already recorded by a trusted new CLI. It updates only running-generation state; launchd registration remains on the old app until the daemon is down.

func PrepareRetainedManagedUpgrade

func PrepareRetainedManagedUpgrade(
	label, slot, profile, currentCandidatePath, nextCandidatePath string,
) (Definition, func() error, error)

PrepareRetainedManagedUpgrade validates a previously adopted managed daemon whose running image is an immutable retained copy, then stages the exact next cached generation. currentCandidatePath is the manifest-bound original payload for the running generation; the daemon layer remains responsible for binding the retained running image to that manifest before calling here.

Unlike PrepareManagedUpgrade, this function is always fail-closed: it is used only after the caller has explicitly selected managed adoption, so missing or mismatched receipt state is an error rather than an unmanaged fallback.

func ValidateMarker

func ValidateMarker(label, slot string) (Definition, error)

func ValidateRetainedAdoptedService

func ValidateRetainedAdoptedService(label, slot, profile, candidatePath string) (Definition, error)

ValidateRetainedAdoptedService validates a managed daemon after an exec upgrade whose running image may be an immutable retained copy. Unlike RunningManagedProcess, this does not compare os.Executable with the cached app payload: the daemon layer has already bound and verified the running image against its upgrade manifest before calling this function. Instead it anchors the manifest's original candidate path to the exact validated generation recorded as running for this service and PID.

This is deliberately adoption-only and fail-closed. A missing receipt or service registration is an error, never an unmanaged result.

type ExpectedStartup

type ExpectedStartup struct {
	Profile    string
	Generation string
	Nonce      string
}

type Generation

type Generation struct {
	ID          string `json:"id"`
	AppPath     string `json:"app_path"`
	Version     string `json:"version"`
	BundleBuild string `json:"bundle_build"`
	Commit      string `json:"commit"`
	PayloadHash string `json:"payload_hash"`
	TeamID      string `json:"team_id,omitempty"`
	Requirement string `json:"requirement,omitempty"`
}

type JobState

type JobState struct {
	Present                bool
	Running                bool
	PID                    int
	Program                string
	ProgramIdentifier      string
	ParentBundleIdentifier string
	ParentBundleVersion    string
}

type Lease

type Lease struct {
	Profile              string       `json:"profile"`
	Slot                 string       `json:"slot"`
	UID                  int          `json:"uid"`
	RegisteredGeneration string       `json:"registered_generation,omitempty"`
	RunningGeneration    string       `json:"running_generation,omitempty"`
	RunningPID           int          `json:"running_pid,omitempty"`
	Paths                config.Paths `json:"paths,omitempty"`
}

func ReserveProfile

func ReserveProfile(receipt *Receipt, profile string, uid int, generation string) (Lease, bool, error)

type ManagedProcess

type ManagedProcess struct {
	Definition Definition
	Profile    string
	Generation Generation
	PID        int
	Store      ReceiptStore
}

func RunningManagedProcess

func RunningManagedProcess(profile string) (ManagedProcess, bool, error)

type Manager

type Manager struct {
	UID         int
	Bundle      ValidatedBundle
	Store       ReceiptStore
	ControlRoot string
	Controller  ServiceController
	Now         func() time.Time
	TeamID      string
	Requirement string
	Development bool
	Verifier    func(string) (SignatureInfo, error)
	SkipCacheGC bool // test-only seam for synthetic managers without real bundles
	// contains filtered or unexported fields
}

func (*Manager) CurrentDefinition

func (manager *Manager) CurrentDefinition(profile string) (Definition, bool, error)

func (*Manager) GarbageCollectCache

func (manager *Manager) GarbageCollectCache() ([]string, error)

GarbageCollectCache removes only validated immutable generations that are unreferenced by the current receipt, its backup, or this installed CLI. The receipt lock stays held across the decision and removal so another profile cannot acquire a generation between the reference check and deletion.

func (*Manager) Launch

func (manager *Manager) Launch(ctx context.Context, cfg *config.Config, paths config.Paths, configFile string, lifetime time.Duration, environ []string) error

func (*Manager) MarkStopped

func (manager *Manager) MarkStopped(profile string, pid int) error

func (*Manager) Receipt

func (manager *Manager) Receipt() (Receipt, error)

func (*Manager) Remove

func (manager *Manager) Remove(ctx context.Context, profile string, all bool, stop StopDaemonFunc) error

func (*Manager) Repair

func (manager *Manager) Repair(ctx context.Context) ([]string, error)

func (*Manager) Reports

func (manager *Manager) Reports(ctx context.Context, profile string, all bool) ([]ServiceReport, error)

func (*Manager) ReserveForCleanRestart

func (manager *Manager) ReserveForCleanRestart(ctx context.Context, profile string) error

ReserveForCleanRestart makes a supported profile service-addressable before a caller stops a working daemon. Existing leases are intentionally left on their registered generation while the process is live; Launch rotates the dormant registration after the exact old process has exited. A new profile is allocated and registered here so capacity or approval failure cannot turn an intentional restart into avoidable downtime.

type Manifest

type Manifest struct {
	Schema           int    `json:"schema"`
	BundleIdentifier string `json:"bundle_identifier"`
	DefaultLabel     string `json:"default_label"`
	NamedLabelPrefix string `json:"named_label_prefix"`
	ProfileSlots     int    `json:"profile_slots"`
}

func ServiceManifest

func ServiceManifest() Manifest

type Mode

type Mode string
const (
	ModeManaged           Mode = "managed"
	ModeLinuxFallback     Mode = "linux-direct-spawn"
	ModeOlderMacFallback  Mode = "older-macos-direct-spawn"
	ModeUnbundledFallback Mode = "unbundled-direct-spawn"
)

func DetectMode

func DetectMode() (Mode, string, error)

type PendingOperation

type PendingOperation struct {
	Kind              string    `json:"kind"`
	Profile           string    `json:"profile,omitempty"`
	Slot              string    `json:"slot"`
	UID               int       `json:"uid"`
	Generation        string    `json:"generation"`
	Token             string    `json:"token,omitempty"`
	CreatedAt         time.Time `json:"created_at,omitempty"`
	OwnerPID          int       `json:"owner_pid,omitempty"`
	OwnerPIDStartTime int64     `json:"owner_pid_start_time,omitempty"`
}

type Receipt

type Receipt struct {
	Schema      int                    `json:"schema"`
	Transaction uint64                 `json:"transaction"`
	Default     *Registration          `json:"default,omitempty"`
	Leases      map[string]Lease       `json:"leases"`
	Quarantined map[string]string      `json:"quarantined,omitempty"`
	Generations map[string]Generation  `json:"generations,omitempty"`
	Pending     *PendingOperation      `json:"pending,omitempty"`
	Starts      map[string]StartIntent `json:"starts,omitempty"`
	Checksum    string                 `json:"checksum"`
}

func NewReceipt

func NewReceipt() Receipt

func ReadReceipt

func ReadReceipt(uid int) (Receipt, error)

type ReceiptStore

type ReceiptStore struct {
	Root string
	UID  int
}

ReceiptStore serializes global profile-slot mutations across every profile. Its root is outside profile data_dir so named daemons cannot allocate the same static service slot independently.

func (ReceiptStore) InitializeAfterTotalLoss

func (s ReceiptStore) InitializeAfterTotalLoss(receipt Receipt) error

InitializeAfterTotalLoss replaces only receipts already proven unusable by the caller. Original bytes are retained under explicit .corrupt names for diagnosis; they are never silently overwritten with an empty mapping.

func (ReceiptStore) Load

func (s ReceiptStore) Load() (Receipt, error)

func (ReceiptStore) Update

func (s ReceiptStore) Update(initialize bool, mutate func(*Receipt) error) (Receipt, error)

Update performs one crash-safe receipt transaction. initialize must only be true after the caller proved none of the 65 exact jobs is registered/live.

type Registration

type Registration struct {
	Slot                 string       `json:"slot"`
	Label                string       `json:"label"`
	RegisteredGeneration string       `json:"registered_generation,omitempty"`
	RunningGeneration    string       `json:"running_generation,omitempty"`
	RunningPID           int          `json:"running_pid,omitempty"`
	Paths                config.Paths `json:"paths,omitempty"`
}

type Resolution

type Resolution struct {
	Mode    Mode
	Reason  string
	Manager *Manager
}

func Resolve

func Resolve(executable, version, commit string, uid int) (Resolution, error)

func ResolveContext

func ResolveContext(ctx context.Context, executable, version, commit string, uid int) (Resolution, error)

func ResolveForRepair

func ResolveForRepair(executable, version, commit string, uid int) (Resolution, error)

func ResolveWith

func ResolveWith(options ResolveOptions) (Resolution, error)

type ResolveOptions

type ResolveOptions struct {
	GOOS             string
	MacOSMajor       int
	Managed          bool
	Executable       string
	Version          string
	Commit           string
	UID              int
	Controller       ServiceController
	Expectations     BundleExpectations
	ControlRoot      string
	ReceiptRoot      string
	SkipReceiptCheck bool
	// contains filtered or unexported fields
}

type ServiceController

type ServiceController interface {
	Status(ctx context.Context, controllerPath string, definition Definition) (ServiceStatus, error)
	Register(ctx context.Context, controllerPath string, definition Definition) (ServiceStatus, error)
	// RegisterFresh registers only when the controller can prove the job was
	// absent immediately before this call. It must return an error rather than
	// normalize an already-registered race to StatusEnabled.
	RegisterFresh(ctx context.Context, controllerPath string, definition Definition) (ServiceStatus, error)
	Unregister(ctx context.Context, controllerPath string, definition Definition) (ServiceStatus, error)
	Kickstart(ctx context.Context, uid int, definition Definition) error
	JobState(ctx context.Context, uid int, definition Definition) (JobState, error)
}

type ServiceReport

type ServiceReport struct {
	Profile              string        `json:"profile,omitempty"`
	Slot                 string        `json:"slot"`
	Label                string        `json:"label"`
	LeaseState           string        `json:"lease_state"`
	RegisteredGeneration string        `json:"registered_generation,omitempty"`
	RunningGeneration    string        `json:"running_generation,omitempty"`
	Status               ServiceStatus `json:"status"`
	JobPresent           bool          `json:"job_present"`
	JobRunning           bool          `json:"job_running"`
	PID                  int           `json:"pid,omitempty"`
	RecordedPID          int           `json:"recorded_pid,omitempty"`
	QuarantineReason     string        `json:"quarantine_reason,omitempty"`
	Paths                config.Paths  `json:"-"`
	Job                  JobState      `json:"-"`
}

type ServiceStatus

type ServiceStatus string
const (
	StatusNotRegistered    ServiceStatus = "not-registered"
	StatusEnabled          ServiceStatus = "enabled"
	StatusRequiresApproval ServiceStatus = "requires-approval"
	StatusNotFound         ServiceStatus = "not-found"
)

type SignatureInfo

type SignatureInfo struct {
	Identifier  string
	TeamID      string
	Requirement string
}

func VerifyDarwinSignature

func VerifyDarwinSignature(string) (SignatureInfo, error)

func VerifyDarwinSignatureContext

func VerifyDarwinSignatureContext(context.Context, string) (SignatureInfo, error)

type StartIntent

type StartIntent struct {
	Label      string    `json:"label"`
	Slot       string    `json:"slot"`
	Profile    string    `json:"profile,omitempty"`
	Generation string    `json:"generation"`
	Nonce      string    `json:"nonce"`
	CreatedAt  time.Time `json:"created_at,omitempty"`
	ExpiresAt  time.Time `json:"expires_at,omitempty"`
}

type StartupRequest

type StartupRequest struct {
	Schema      int               `json:"schema"`
	Profile     string            `json:"profile,omitempty"`
	Slot        string            `json:"slot"`
	Label       string            `json:"label"`
	ConfigFile  string            `json:"config_file,omitempty"`
	Paths       config.Paths      `json:"paths"`
	Generation  string            `json:"generation"`
	Environment map[string]string `json:"environment"`
	Nonce       string            `json:"nonce"`
	UID         int               `json:"uid"`
	CreatedAt   time.Time         `json:"created_at"`
	ExpiresAt   time.Time         `json:"expires_at"`
	Checksum    string            `json:"checksum"`
}

func ConsumeStartupRequest

func ConsumeStartupRequest(root string, uid int, definition Definition, expected ExpectedStartup, now time.Time) (StartupRequest, error)

func NewStartupRequest

func NewStartupRequest(definition Definition, profile, configFile string, paths config.Paths, generation string, environment map[string]string, uid int, now time.Time, lifetime time.Duration) (StartupRequest, error)

type StopDaemonFunc

type StopDaemonFunc func(ServiceReport) error

type ValidatedBundle

type ValidatedBundle struct {
	AppPath    string
	Generation Generation
}

func CacheBundle

func CacheBundle(source ValidatedBundle, expectations BundleExpectations, uid int) (ValidatedBundle, error)

func CacheBundleContext

func CacheBundleContext(ctx context.Context, source ValidatedBundle, expectations BundleExpectations, uid int) (ValidatedBundle, error)

func DiscoverBundle

func DiscoverBundle(executable string, expectations BundleExpectations) (ValidatedBundle, bool, error)

DiscoverBundle distinguishes an absent package payload (explicit unbundled fallback) from a present-but-invalid app, which callers must fail closed.

func ValidateBundle

func ValidateBundle(appPath string, expectations BundleExpectations) (ValidatedBundle, error)

Directories

Path Synopsis
cmd
generate command

Jump to

Keyboard shortcuts

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