a73x

scripts/eitri-agent-launchagent.sh

Ref:   Size: 4.4 KiB   History

#!/usr/bin/env bash
#
# Install this Mac's eitri-agent as a LaunchAgent, so it starts with the
# session and is restarted if it crashes. The macOS counterpart of
# eitri-agent.service.
#
#   ./eitri-agent-launchagent.sh install [/path/to/eitri-agent]
#   ./eitri-agent-launchagent.sh uninstall
#   ./eitri-agent-launchagent.sh status
#
# No privileges anywhere. A Mac runs guests through vfkit, which needs none, so
# the agent needs none either — and the two things every agent does need, a
# state directory it can create and a binary it can replace when it upgrades,
# are both satisfied by keeping them in the running account's own space.
#
# A LaunchAgent and not a LaunchDaemon, because a Mac host is almost always
# somebody's Mac. An agent runs in the login session, which is where the person
# using that machine already is. A dedicated always-on host that must run guests
# with nobody logged in wants a LaunchDaemon instead — same plist, moved to
# /Library/LaunchDaemons with a UserName key naming this account, and the
# binary and state left exactly where they are here.
#
# A generated plist rather than a shipped one: launchd does not expand ~ or any
# variable in ProgramArguments, so every path has to be absolute and resolved on
# the machine it will run on.
set -euo pipefail

LABEL="sh.eitri.agent"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
LOG_DIR="$HOME/Library/Logs"
DOMAIN="gui/$(id -u)"

die() { printf 'error: %s\n' "$*" >&2; exit 1; }

cmd_install() {
	local bin="${1:-}"
	if [[ -z $bin ]]; then
		bin="$(command -v eitri-agent || true)"
		[[ -n $bin ]] || die "eitri-agent is not on PATH — pass its path: $0 install /path/to/eitri-agent"
	fi
	bin="$(cd "$(dirname "$bin")" && pwd)/$(basename "$bin")"
	[[ -x $bin ]] || die "not executable: $bin"

	# The agent replaces this file when it upgrades itself, so the account
	# running it has to own it. A root-owned prefix like /usr/local/bin looks
	# tidy and quietly disables every future upgrade.
	[[ -w $bin ]] || die "$bin is not writable by $(id -un) — self-upgrade would fail. Keep the binary somewhere this account owns."

	command -v vfkit >/dev/null || printf 'warning: vfkit is not on PATH; guests will fail Preflight until `brew install vfkit`\n' >&2

	mkdir -p "$(dirname "$PLIST")" "$LOG_DIR"
	cat > "$PLIST" <<-PLIST
		<?xml version="1.0" encoding="UTF-8"?>
		<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
		<plist version="1.0">
		<dict>
			<key>Label</key>
			<string>$LABEL</string>
			<key>ProgramArguments</key>
			<array>
				<string>$bin</string>
			</array>
			<key>RunAtLoad</key>
			<true/>
			<!-- Restart a crash, but let a clean stop be a stop: the agent exits 0
			     on SIGTERM, so bootout stays a stop rather than a fight. -->
			<key>KeepAlive</key>
			<dict>
				<key>SuccessfulExit</key>
				<false/>
			</dict>
			<key>ThrottleInterval</key>
			<integer>5</integer>
			<!-- Homebrew's directory is absent from a launchd job's default PATH,
			     and vfkit lives there. Without this the agent starts and then
			     refuses every create, correctly but avoidably. -->
			<key>EnvironmentVariables</key>
			<dict>
				<key>PATH</key>
				<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
			</dict>
			<key>StandardOutPath</key>
			<string>$LOG_DIR/eitri-agent.log</string>
			<key>StandardErrorPath</key>
			<string>$LOG_DIR/eitri-agent.log</string>
		</dict>
		</plist>
	PLIST
	plutil -lint "$PLIST" >/dev/null || die "generated plist is malformed: $PLIST"

	# bootout first so install is idempotent; ignore "not loaded".
	launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
	launchctl bootstrap "$DOMAIN" "$PLIST"
	printf 'installed %s\n  binary %s\n  log    %s/eitri-agent.log\n' "$PLIST" "$bin" "$LOG_DIR"
	cmd_status
}

cmd_uninstall() {
	# bootout, NOT `unload -w`: the -w flag writes to the persistent override
	# database and leaves the label DISABLED, so every later load fails with an
	# error that says nothing about why.
	launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
	rm -f "$PLIST"
	printf 'removed %s (state and binary left alone)\n' "$PLIST"
}

cmd_status() {
	launchctl print "$DOMAIN/$LABEL" 2>/dev/null | grep -E '^\s+(state|pid|last exit code) ' ||
		printf 'not loaded\n'
}

case "${1:-}" in
	install) shift; cmd_install "$@" ;;
	uninstall) cmd_uninstall ;;
	status) cmd_status ;;
	*) sed -n '3,10p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
esac