#!/bin/bash # daemons - observe and control the homegrown launchd daemons: activity logs + lifecycle # # Usage: # daemons append [detail|--detail-stdin] Append one activity record # daemons status [--since T] Summarize all daemons over a rolling window # daemons check Health+activity gate (returns nonzero on problem) # daemons log [name|--all] [--follow] Render the log (human-readable) # daemons query [name|--all] [--event E] [--since T] [--jq EXPR] Filter/extract JSONL records # daemons load [--plist PATH] [--dry-run] Bootstrap (reload) a daemon into launchd; logs a load record # daemons unload [--dry-run] Bootout a daemon from launchd; logs an unload record # daemons registry [--edit|--open] Print the registry path, or open it (--edit: $VISUAL/$EDITOR, --open: macOS open) # daemons -h | --help # # Environment: # DAEMONS_LOG_DIR Directory of .log files (default: $XDG_STATE_HOME/daemons or ~/.local/state/daemons) # DAEMONS_REGISTRY Registry TSV (default: $XDG_CONFIG_HOME/daemons/daemons.tsv or ~/.config/daemons/daemons.tsv) PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" # LaunchDaemons get a minimal PATH; need jq _daemons() ( local SCRIPT_NAME="${BASH_SOURCE[0]##*/}" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="daemons" ;; esac local LOG_DIR="${DAEMONS_LOG_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/daemons}" local REGISTRY="${DAEMONS_REGISTRY:-${XDG_CONFIG_HOME:-$HOME/.config}/daemons/daemons.tsv}" local DEFAULT_STATUS_WINDOW="24h" local REGISTRY_NONE="-" local SECONDS_PER_MINUTE=60 local SECONDS_PER_HOUR=3600 local SECONDS_PER_DAY=86400 _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } _dry() { printf '[DRY][%s] %s\n' "$SCRIPT_NAME" "$*"; } _shell_join() { local out="" local arg local quoted for arg in "$@"; do printf -v quoted '%q' "$arg" if [ -n "$out" ]; then out="$out "; fi out="$out$quoted" done printf '%s' "$out" } _dry_command() { local command; command="$(_shell_join "$@")" _dry "Would run: $command" } _expand_short_opts() { # $1 = string of short-opt letters that take a value (e.g. "nXHd"); "" for flag-only scripts # $2..$N = "$@" # Populates _EXPANDED; caller does: set -- "${_EXPANDED[@]}"; unset _EXPANDED local value_opts="$1"; shift _EXPANDED=() local passthru="" local arg local rest local c for arg in "$@"; do if [ -n "$passthru" ]; then _EXPANDED+=("$arg"); continue; fi case "$arg" in --) passthru=1; _EXPANDED+=("$arg") ;; --*|-|"") _EXPANDED+=("$arg") ;; -[a-zA-Z]?*) rest="${arg#-}" while [ -n "$rest" ]; do c="${rest%"${rest#?}"}"; rest="${rest#?}" _EXPANDED+=("-$c") case "$value_opts" in *"$c"*) [ -n "$rest" ] && _EXPANDED+=("$rest") rest="" ;; esac done ;; *) _EXPANDED+=("$arg") ;; esac done } _show_help() { local s="" local r="" if { [ -t 1 ] || [ -n "${CLICOLOR_FORCE:-}" ]; } && [ -z "${NO_COLOR:-}" ]; then s=$'\033[4m' r=$'\033[24m' fi cat < <${s}event${r}> [${s}detail${r}|--detail-stdin] $SCRIPT_NAME status [--since ${s}T${r}] $SCRIPT_NAME check $SCRIPT_NAME log [${s}name${r}|--all] [--follow] $SCRIPT_NAME query [${s}name${r}|--all] [--event ${s}E${r}] [--since ${s}T${r}] [--jq ${s}EXPR${r}] $SCRIPT_NAME load <${s}name${r}> [--plist ${s}PATH${r}] [--dry-run] $SCRIPT_NAME unload <${s}name${r}> [--dry-run] $SCRIPT_NAME registry [--edit|--open] DESCRIPTION Writes and reads per-daemon activity logs (JSONL) under \$DAEMONS_LOG_DIR. Each record is {ts, daemon, event, detail}; event is trigger|noop|change|error|load|unload. 'append' is the write path daemons call; 'status'/'check'/'log'/'query' read; 'load'/'unload' control launchd and log their own record; 'registry' reveals the registry. SUBCOMMANDS append Append one activity record (creates the log dir on first write). Pass --detail-stdin in place of [detail] to read the detail from stdin status Per-daemon summary: loaded state, last activity, rolling event counts, and expected-silence health check Health+activity gate; nonzero + loud alerts on any problem log Render one log human-readably, or merge all logs; optionally follow new records query Filter structured JSONL records by daemon, event, or time, then optionally reshape them with jq load Bootstrap a daemon into launchd (bootout-then-bootstrap, so it also reloads). Resolves the plist by --plist > registry column > convention, then verifies it exists. A system/ target elevates via sudo when not root. Logs a 'load' record unload Bootout a daemon from launchd (idempotent; "already unloaded" is not an error). Logs an 'unload' record registry Print the resolved \$DAEMONS_REGISTRY path; --edit opens it in \$VISUAL/\$EDITOR (default vi), --open uses macOS open. --edit and --open are mutually exclusive STATUS OPTIONS -s, --since T Count activity since T (default: $DEFAULT_STATUS_WINDOW) LOG OPTIONS -a, --all Merge every daemon log instead of selecting one -f, --follow Follow new records like tail -f QUERY OPTIONS -a, --all Query every daemon log instead of selecting one -e, --event E Keep only event E -s, --since T Keep records at or after T; no time filter by default -j, --jq EXPR Apply EXPR to each matching JSON record T accepts NNN[smhd] (e.g. 1h, 30m, 7d), YYYY-MM-DD (midnight UTC), or YYYY-MM-DDTHH:MM:SSZ LOAD AND UNLOAD OPTIONS -n, --dry-run Print the launchctl commands without running them --plist PATH Use PATH instead of the registry or conventional plist path (load only) REGISTRY OPTIONS --edit Open the registry in \$VISUAL or \$EDITOR --open Open the registry with macOS open ENVIRONMENT DAEMONS_LOG_DIR Directory of .log files (default: \$XDG_STATE_HOME/daemons or ~/.local/state/daemons) DAEMONS_REGISTRY Registry TSV (default: \$XDG_CONFIG_HOME/daemons/daemons.tsv or ~/.config/daemons/daemons.tsv) FILES \$DAEMONS_REGISTRY Whitespace-separated table that 'status', 'check', 'load', and 'unload' read, and 'registry' prints (append/log/query don't need it). A header row, then one row per daemon with four required columns and two optional columns: name log name; matches the passed to append domain launchd domain: gui/\$UID/ (per-user agent; \$UID is replaced with your numeric uid) or system/ label launchd label; state read via launchctl print