Documentation
¶
Overview ¶
Package service manages the git-treeline router as a system service. Supports macOS LaunchAgents and Linux systemd user units.
When GTL_HOME is set, labels and paths are suffixed with ".dev" to avoid colliding with the production install.
Index ¶
- Constants
- Variables
- func Bounce(routerPort int, wait time.Duration) error
- func CheckManagedHostsWrite(expected ManagedHostsSnapshot) error
- func CleanHosts() error
- func GeneratePfAnchor(routerPort int) string
- func GeneratePlist(gtlPath string) (string, error)
- func GenerateUnit(gtlPath string) (string, error)
- func Install(gtlPath string, port int) (string, error)
- func InstallPfReloadDaemon() error
- func InstallPortForward(routerPort int) error
- func InstalledBinaryPath() string
- func IsInstalled() bool
- func IsLinuxPortForwardPersistenceInstalled() bool
- func IsPfReloadDaemonCurrent() bool
- func IsPfReloadDaemonInstalled() bool
- func IsPortForwardConfigured() bool
- func IsRunning() bool
- func LaunchLabel() string
- func ManagedHosts() ([]string, error)
- func MissingHosts(expected []string) []string
- func NeedsHostsSync(expected []string) bool
- func PfReloadDaemonPath() string
- func PlistPath() string
- func Reload(routerPort int, wait time.Duration) error
- func ReloadPortForward() error
- func RouterLogFiles() (stdout, stderr string)
- func RouterVersionFile() string
- func RunningPID() int
- func RunningRouterVersion() string
- func StableExecutablePath() (string, error)
- func SyncHosts(hostnames []string) error
- func SyncManagedHostsNoninteractive(expected ManagedHostsSnapshot, hosts []string) (resultErr error)
- func SystemdUnit() string
- func Uninstall() error
- func UninstallPfReloadDaemon() error
- func UninstallPortForward() error
- func UnitPath() string
- func WaitRouterResponding(routerPort int, wait time.Duration) error
- func WriteRouterVersion(version string)
- type HealthCheck
- type ManagedHostsSnapshot
- type PortForwardStatus
Constants ¶
const DefaultReadyTimeout = 30 * time.Second
DefaultReadyTimeout is the default deadline for verifying that a restarted router is ready: a fresh version file (a new process started) AND the health endpoint answering (it is actually serving). Generous because TLS setup and route hydration can take several seconds on large registries — it's a deadline, not a sleep, so the happy path returns as soon as the router answers.
const HookReadyTimeout = 10 * time.Second
HookReadyTimeout is the readiness deadline for non-interactive tooling paths (e.g. the Homebrew post_install hook running `gtl serve restart --if-installed`), where blocking a package manager for 30 seconds on a genuinely broken start is worse than failing fast.
const PfReloadDaemonLabel = "dev.treeline.pfreload"
PfReloadDaemonLabel is the launchd label for our boot-time pf reloader. Lives in /Library/LaunchDaemons (system-level, runs as root). The daemon exists so pf rules survive a reboot AND macOS updates: Apple loads pf.conf at boot but does NOT enable pf, and OS updates periodically revert /etc/pf.conf to the stock file, silently deleting our rdr-anchor/load lines. The daemon runs pfEnsureScript, which re-inserts those lines when missing, then loads the ruleset and enables pf. It fires at boot (RunAtLoad) and whenever /etc/pf.conf changes (WatchPaths) — so an update that rewrites the file triggers its own repair.
The daemon must NOT reference our gtl binary: a root daemon executing a user-writable Homebrew binary would be a privilege escalation. Instead it runs /bin/sh on a root-owned script installed by `gtl serve install`.
Variables ¶
var ErrHostsBusy = errors.New("hosts writer busy")
var ErrHostsChanged = errors.New("hosts snapshot changed")
var ErrHostsPrivilegeRequired = errors.New("hosts privilege required")
var ErrHostsUncertain = errors.New("hosts write outcome uncertain")
Functions ¶
func Bounce ¶ added in v0.40.0
Bounce restarts the running router service so it picks up changes that don't require a new service definition (e.g. the `gtl` binary on disk has been upgraded by Homebrew, but the launchd-managed process is still the old build). On macOS this is `launchctl kickstart -k`; on Linux it is `systemctl --user restart`.
Bounce verifies the restart by waiting for the router to write a fresh `router.version` file and then answer health checks on `routerPort`, within `wait`.
Use Reload (not Bounce) when the service definition itself changed.
func CheckManagedHostsWrite ¶ added in v0.59.0
func CheckManagedHostsWrite(expected ManagedHostsSnapshot) error
CheckManagedHostsWrite checks access to the same file without changing it. The actual writer still revalidates bytes and identity under its own lock.
func CleanHosts ¶
func CleanHosts() error
CleanHosts removes all git-treeline entries from /etc/hosts.
func GeneratePfAnchor ¶
GeneratePfAnchor returns the pf anchor content for testing.
func GeneratePlist ¶
GeneratePlist returns the plist XML content as a string.
func GenerateUnit ¶
GenerateUnit returns the systemd unit content as a string.
func Install ¶
Install writes a service definition and activates it. Returns the path to the written file.
func InstallPfReloadDaemon ¶ added in v0.40.1
func InstallPfReloadDaemon() error
InstallPfReloadDaemon writes the LaunchDaemon plist to /Library/LaunchDaemons and bootstraps it so it runs at every boot. Requires sudo. Idempotent — safe to call when already installed.
macOS-only. On other platforms returns nil (no-op) since iptables on Linux distros generally persist their own rules via netfilter-persistent or iptables-save and don't need a separate boot service.
Note: gtl serve install bundles this work into the same sudo session as the pf rules install (see installDarwinPortForward) so users only ever hit a single password prompt and the two stay atomic. This function is retained for callers that need to (re-)install the daemon on its own — e.g. doctor-style repair flows.
func InstallPortForward ¶
InstallPortForward sets up an OS-level redirect from port 443 to the router port so users can access worktrees at https://{branch}.localhost without typing a port number. Requires sudo.
func InstalledBinaryPath ¶
func InstalledBinaryPath() string
InstalledBinaryPath reads the LaunchAgent plist (or systemd unit) and returns the binary path embedded in the service definition. Returns "" if the service file doesn't exist or can't be parsed.
func IsInstalled ¶ added in v0.40.1
func IsInstalled() bool
IsInstalled reports whether the service definition exists on disk for the current user, regardless of whether it's currently running. Used by `gtl serve restart --if-installed` so tooling integrations (Homebrew post_install) can safely be a no-op on machines where the user never installed the router.
func IsLinuxPortForwardPersistenceInstalled ¶ added in v0.48.0
func IsLinuxPortForwardPersistenceInstalled() bool
IsLinuxPortForwardPersistenceInstalled reports whether the boot-time redirect unit is installed AND enabled (so systemd will actually run it at boot). Read-only; `systemctl is-enabled` does not require root.
func IsPfReloadDaemonCurrent ¶ added in v0.54.2
func IsPfReloadDaemonCurrent() bool
IsPfReloadDaemonCurrent reports whether the installed daemon matches what this version of gtl would install — both the plist and the repair script, byte for byte. False for pre-script installs (whose plist ran `pfctl -ef` and could not survive a macOS update reverting pf.conf), signalling that `gtl serve install` should upgrade the daemon. Read-only; both files are world-readable.
func IsPfReloadDaemonInstalled ¶ added in v0.40.1
func IsPfReloadDaemonInstalled() bool
IsPfReloadDaemonInstalled reports whether our LaunchDaemon plist exists on disk. Read-only; no sudo needed.
func IsPortForwardConfigured ¶
func IsPortForwardConfigured() bool
IsPortForwardConfigured checks whether the port 443 redirect is in place on disk (pf.conf or iptables rules saved). Note: this can return true when the rules are not actually loaded into the kernel — see PortForwardActive for an authoritative check.
func LaunchLabel ¶
func LaunchLabel() string
func ManagedHosts ¶
ManagedHosts returns the hostnames currently in the managed block.
func MissingHosts ¶
MissingHosts returns hostnames from expected that are not yet present in the managed /etc/hosts block. If the hosts file cannot be read, all expected hostnames are returned (conservative: assume sync is needed).
func NeedsHostsSync ¶
NeedsHostsSync reports whether the hosts file needs updating for the given set of expected hostnames. Enabled on macOS (Safari cannot resolve *.localhost) and Linux (a custom TLD requires /etc/hosts on every platform); callers gate the Linux case on a non-localhost domain. Other platforms always report false.
func PfReloadDaemonPath ¶ added in v0.40.1
func PfReloadDaemonPath() string
PfReloadDaemonPath returns the on-disk plist path. macOS-only; on other platforms returns "" since we don't ship this daemon there.
func Reload ¶ added in v0.40.0
Reload tears down the existing service registration and re-registers it from the on-disk service definition. Use this after writing a new plist or systemd unit so launchd/systemd actually pick up the changes.
On macOS that's `launchctl bootout` followed by `launchctl bootstrap`. On Linux that's `systemctl --user daemon-reload` followed by `restart`.
func ReloadPortForward ¶ added in v0.40.0
func ReloadPortForward() error
ReloadPortForward re-applies the port-forwarding rules from disk into the running kernel ruleset. Useful after a reboot/network change wiped them without changing pf.conf or iptables-save state.
macOS requires sudo. Linux uses the same mechanism as install (currently re-running iptables rules); we delegate to the same install path because it is idempotent.
func RouterLogFiles ¶ added in v0.48.0
func RouterLogFiles() (stdout, stderr string)
RouterLogFiles returns the macOS stdout/stderr log paths for the router (StandardOutPath / StandardErrorPath in the launchd plist). Exported so `gtl serve logs` can tail them. Meaningful only on macOS — Linux logging goes through journalctl keyed on SystemdUnit().
func RouterVersionFile ¶
func RouterVersionFile() string
RouterVersionFile returns the path to the file where the running router records its version on startup.
func RunningPID ¶ added in v0.40.0
func RunningPID() int
RunningPID returns the PID of the running service process, or 0 if it cannot be determined. On macOS, parses `launchctl print` output for the authoritative PID launchd has registered for our label. On Linux, reads the MainPID from `systemctl --user show`.
func RunningRouterVersion ¶
func RunningRouterVersion() string
RunningRouterVersion reads the version recorded by the running router. Returns "" if the file doesn't exist or can't be read.
func StableExecutablePath ¶
StableExecutablePath returns a path suitable for embedding in a service definition (launchd plist, systemd unit). On Homebrew installs, os.Executable() resolves symlinks and returns a versioned Cellar path (e.g. /opt/homebrew/Cellar/git-treeline/0.38.0/bin/gtl) that breaks after `brew upgrade`. This function detects that case and returns the stable symlink path instead (e.g. /opt/homebrew/bin/gtl).
func SyncHosts ¶
SyncHosts updates /etc/hosts with entries for the given hostnames. On macOS this fixes Safari's failure to resolve *.localhost. For custom TLDs (non-.localhost), this is required on all platforms. Requires sudo.
func SyncManagedHostsNoninteractive ¶ added in v0.59.0
func SyncManagedHostsNoninteractive(expected ManagedHostsSnapshot, hosts []string) (resultErr error)
SyncManagedHostsNoninteractive never invokes sudo or replaces the inode. It only writes an existing, opted-in file already writable by this process. Flock coordinates strict writers; legacy and external editors do not share this guarantee. After any attempted write, failures are conservatively unknown.
func SystemdUnit ¶
func SystemdUnit() string
func Uninstall ¶
func Uninstall() error
Uninstall stops the service and removes the definition file.
func UninstallPfReloadDaemon ¶ added in v0.40.1
func UninstallPfReloadDaemon() error
UninstallPfReloadDaemon removes the LaunchDaemon. Symmetric with install — requires sudo. Idempotent.
func UninstallPortForward ¶
func UninstallPortForward() error
UninstallPortForward removes the OS-level port 443 redirect.
func WaitRouterResponding ¶ added in v0.48.0
WaitRouterResponding polls the router's health endpoint until it answers, or until `wait` elapses. Used after a restart: launchd/systemd reporting the service as running (and even the version file being rewritten) only proves the process started, not that it is serving requests. A router that only answers through the :443 forward counts as responding — the direct port being dropped is an OS packet-filter fault a bounce can't fix, and failing the bounce over it traps the user in a restart loop.
func WriteRouterVersion ¶
func WriteRouterVersion(version string)
WriteRouterVersion writes the current version to the version file. Called by the router on startup.
Types ¶
type HealthCheck ¶
type HealthCheck struct {
Name string `json:"name"`
Status string `json:"status"` // "ok", "warn", "error"
Detail string `json:"detail"`
Fix string `json:"fix,omitempty"`
}
HealthCheck represents a single doctor check result.
func CheckHealth ¶
func CheckHealth(routerPort int, cliVersion string) []HealthCheck
CheckHealth runs all serve-related health checks and returns the results.
type ManagedHostsSnapshot ¶ added in v0.59.0
type ManagedHostsSnapshot struct {
// contains filtered or unexported fields
}
ManagedHostsSnapshot binds a strict read to the existing file and exact bytes.
func ReadManagedHostsStrict ¶ added in v0.59.0
func ReadManagedHostsStrict(path string) (ManagedHostsSnapshot, error)
ReadManagedHostsStrict uses /etc/hosts when path is empty. An absent file is distinct from unreadable or malformed managed state; it is never created.
func (ManagedHostsSnapshot) Hostnames ¶ added in v0.59.0
func (s ManagedHostsSnapshot) Hostnames() []string
type PortForwardStatus ¶ added in v0.40.0
type PortForwardStatus struct {
// ConfiguredOnDisk is true when pf.conf (macOS) or saved iptables rules
// (Linux) reference our redirect.
ConfiguredOnDisk bool
// LoadedInKernel is true when the running ruleset includes our redirect.
// On macOS this requires both pf to be enabled AND our rdr rule to be
// in the active anchor.
LoadedInKernel bool
// PfEnabled (macOS only) reports the running pf status. Meaningful only
// when PfStateKnown is true. Meaningless on Linux.
PfEnabled bool
// PfStateKnown (macOS only) is true when `pfctl -s info` succeeded —
// without it, callers must not interpret PfEnabled=false as proof that
// pf is actually disabled. On modern macOS, `pfctl -s info` may
// require sudo; without it, returns an error.
PfStateKnown bool
// KernelStateKnown is true when we successfully queried the kernel rule
// list. On macOS, `pfctl -s nat` (and per-anchor variants) typically
// require sudo — if our calls fail with permission errors, this is
// false, and LoadedInKernel should not be trusted as authoritative.
KernelStateKnown bool
// ConfReverted (macOS only) is true when the anchor file exists but
// pf.conf no longer references it — the signature of a macOS update
// reverting /etc/pf.conf to the stock file. Distinguishes "was
// installed, then wiped by Apple" (repair with 'gtl serve reload-pf')
// from "never installed" (run 'gtl serve install').
ConfReverted bool
// Detail is a one-line human-readable summary, or "" when everything is
// healthy.
Detail string
}
PortForwardStatus reports whether the port-forwarding rules are not just configured on disk but actually loaded in the kernel right now. This is what determines whether traffic to :443 will reach the router.
func CheckPortForward ¶ added in v0.40.0
func CheckPortForward(routerPort int) PortForwardStatus
CheckPortForward queries the running kernel state of our port-forwarding rules. Requires sudo on macOS for full accuracy; without sudo, falls back to the on-disk check and returns LoadedInKernel = false (unknown).
routerPort is the port the rdr should forward to; used to detect a rule that exists but points to the wrong port.