service

package
v0.9.4 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package service installs and removes the flue login service: a launchd agent on darwin, a systemd user unit on linux. It is self-contained — cmd/flue consumes it and nothing else does — and every interaction with a real service manager goes through the Runner seam so tests never touch one.

Index

Constants

View Source
const LaunchdLabel = "sh.flue.daemon"

LaunchdLabel is the launchd service label and the plist's basename.

Variables

View Source
var ErrNoUserManager = errors.New("service: systemd user services are not available here (no user manager — common on WSL)")

ErrNoUserManager reports that systemctl --user has nobody to talk to — the common WSL shape. The CLI turns this into a one-line pointer at `flue serve`; it is never a stack trace.

View Source
var ErrUnsupported = errors.New("service: no login-service support on this platform")

ErrUnsupported reports a platform with no login-service support. flue ships darwin and linux; WSL is linux with, usually, no user manager — which is ErrNoUserManager at Enable time, not this.

Functions

func LaunchdPlist

func LaunchdPlist(exe string) []byte

LaunchdPlist renders the launchd agent plist that runs `exe serve` at login. exe is the path os.Executable reports, symlinks left intact — for a brew cask install that is the stable /opt/homebrew/bin/flue symlink, not the version-pinned Caskroom target that `brew upgrade` deletes.

AbandonProcessGroup is what lets sessions outlive the daemon: without it, launchd cleans up the job's process group when the job ends, and the per-session holder processes — the whole point of which is to survive the daemon — would be collateral. They setsid away regardless, so the key is belt on top of braces, but the intent belongs in the unit.

func SystemdUnit

func SystemdUnit(exe string) []byte

SystemdUnit renders the systemd user unit that runs `exe serve` at login. The path is double-quoted for systemd's ExecStart lexer, and % is doubled because ExecStart expands specifiers.

KillMode=process is load-bearing for session durability: the default control-group mode kills every process in the unit's cgroup on stop or restart, holders included, which would put sessions right back to dying with the daemon. process scopes the stop signal to the daemon alone. Logout and reboot still tear the user slice down with SIGTERM, which is the signal a holder answers by writing its revival snapshot.

Types

type ExecRunner

type ExecRunner struct{}

ExecRunner is the production Runner.

func (ExecRunner) Run

func (ExecRunner) Run(name string, args ...string) ([]byte, error)

type Launchd

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

Launchd manages the login service through launchctl on darwin.

func NewLaunchd

func NewLaunchd(exe, home string, uid int, r Runner) *Launchd

func (*Launchd) Disable

func (l *Launchd) Disable() error

Disable boots the agent out and removes the plist. Both halves tolerate absence: a bootout of an unloaded label and a remove of a missing file are what "already disabled" looks like, and that is a success.

func (*Launchd) Enable

func (l *Launchd) Enable() error

Enable writes the plist and bootstraps it — the modern spelling, not `launchctl load`. When the label is already bootstrapped, bootstrap refuses; whether that is convergence depends on whether the plist just changed. launchd reads a plist only at bootstrap, so a loaded job keeps its old definition no matter what Enable writes to disk: when the render differs from what was there (a new binary path after an upgrade), Enable must bootout the stale job and bootstrap the fresh plist — restarting the daemon on purpose, because the old job would exec a path that may no longer exist. When the plist is byte-identical, the loaded job already matches, so Enable only verifies the label is loaded (print) and kickstarts it in case it is dead. kickstart without -k never restarts a running service, which is what keeps a no-op re-run of flue enable from killing live sessions.

func (*Launchd) RefreshUnit added in v0.9.0

func (l *Launchd) RefreshUnit() error

RefreshUnit converges the plist on disk and nothing else; see UnitRefresher. Restart's bootstrap reads the file fresh, so a refresh followed by Restart is how a plist change reaches an existing install.

func (*Launchd) Restart added in v0.2.0

func (l *Launchd) Restart() error

Restart stops the loaded job and bootstraps the plist on disk — the same bootout+bootstrap sequence Enable uses when the plist has drifted, chosen over `launchctl kickstart -k` on purpose: bootout tears the job down the graceful way (SIGTERM, then launchd's exit timeout), which is the signal cmdServe saves session snapshots on, while kickstart -k kills. A restart after an upgrade exists to carry live sessions onto the new build, so the graceful spelling is the only right one.

The bootout error is tolerated for the reason Enable tolerates it: an unloaded label is not a failure to stop it, and the bootstrap — which reads the plist fresh, so it also converges any drift — is the call whose error matters. awaitBootout between the two is what makes the sequence work at all: bootout only starts the teardown, and bootstrapping the same label while it drains is refused with EIO.

func (*Launchd) Status

func (l *Launchd) Status() (Status, error)

type Manager

type Manager interface {
	Enable() error
	Disable() error
	Restart() error
	Status() (Status, error)
}

Manager installs, removes, restarts, and inspects the flue login service.

  • Enable converges: it rewrites the unit if it drifted, loads it if it is not loaded, and starts it if it is dead — without restarting a healthy daemon, whose sessions must survive a re-run of flue enable.
  • Disable is idempotent: disabling what is not enabled is nil.
  • Restart bounces the daemon on purpose — the one thing Enable refuses to do — for the caller that has just replaced the binary and needs the running process to be the new one (flue update). Both implementations stop the daemon with SIGTERM, which is the graceful path cmdServe snapshots sessions on, so live sessions ride across the restart.

func ForPlatform

func ForPlatform(goos, exe, home string, uid int, r Runner) (Manager, error)

ForPlatform picks the implementation for goos. It takes goos as a parameter rather than reading runtime.GOOS so both arms are testable on any host.

type Runner

type Runner interface {
	Run(name string, args ...string) ([]byte, error)
}

Runner executes one service-manager command and returns its combined output. It exists so the command flows are testable against a fake and so CI never touches a real launchd or systemd.

type Status

type Status struct {
	Installed bool // the unit file is on disk
	Running   bool // the service manager reports it alive
}

Status reports the login service's two independent facts.

type Systemd

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

Systemd manages the login service through systemctl --user on linux.

func NewSystemd

func NewSystemd(exe, home string, r Runner) *Systemd

func (*Systemd) Disable

func (s *Systemd) Disable() error

Disable stops and disables the unit, removes the file, and reloads. Every systemctl failure is tolerated: on a machine with no user manager the file removal is the whole operation, and "already disabled" is a success.

Lingering is deliberately left as Enable set it. It is a per-user fact, not a per-service one — the user may linger for reasons that have nothing to do with flue — and disable-linger here would stop their other user services at logout. Removing the unit already means flue no longer runs.

func (*Systemd) Enable

func (s *Systemd) Enable() error

Enable writes the unit, reloads, and runs the spec's exact command: `systemctl --user enable --now flue`. Rewriting the unit unconditionally is what converges drift; enable --now starts the service only when it is not already active, so a healthy daemon is never restarted.

func (*Systemd) RefreshUnit added in v0.9.0

func (s *Systemd) RefreshUnit() error

RefreshUnit converges the unit file and reloads the manager's view of it, without touching the running service; see UnitRefresher. The reload is part of the file's delivery on systemd — a rewritten unit systemd has not re-read changes nothing — and reload never restarts anything.

func (*Systemd) Restart added in v0.2.0

func (s *Systemd) Restart() error

Restart is systemd's own word for it: systemctl --user restart flue. The unit is stopped with SIGTERM — the graceful path cmdServe saves session snapshots on — and started from the unit file on disk, so a binary swapped since the last start is the one that execs. restart also starts a unit that happens to be dead, which is the convergence an update wants: the point is that the next running daemon is the new build.

func (*Systemd) Status

func (s *Systemd) Status() (Status, error)

func (*Systemd) Warnings

func (s *Systemd) Warnings() []string

Warnings reports the advisories from the most recent Enable.

type UnitRefresher added in v0.9.0

type UnitRefresher interface {
	RefreshUnit() error
}

UnitRefresher is optionally implemented by a Manager that can converge the unit file on disk — and only the file — without touching the running service. It exists for restart paths: Restart bootstraps whatever unit is on disk, so refreshing first is how a restart also delivers a unit change (KillMode, AbandonProcessGroup) to installs from before the change. The running daemon is deliberately left alone; the caller's Restart is what makes the new unit take effect.

type Warner

type Warner interface {
	Warnings() []string
}

Warner is optionally implemented by a Manager whose Enable can succeed while still owing the user a fact. Warnings reports advisories from the most recent Enable — true and unfortunate but not failures, like loginctl enable-linger being refused in a container, where the service works while logged in and dies at the last logout. Launchd never warns, so it does not implement this; the CLI upgrades with a type assertion.

Jump to

Keyboard shortcuts

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