A Red Dot in the Menu Bar — Passive Monitoring for LaunchAgents
A Pier postmortem: LaunchAgents fail silently, always. Treat plist files as the source of truth, backfill state from launchctl, light a red dot on a nonzero exit code, refresh every 5 minutes — monitoring earns its keep precisely when you're not looking.
macOS LaunchAgents share an old disease with cron: silent failure. Sync scripts, backup jobs, little daemons that start at login — when they die, they just die. No notification, no red dot, until the day you discover three weeks of backups are empty. launchctl list can tell you, but nobody runs it daily.
Pier 2.0 adds LaunchAgents management to the Dev tab, paired with a menu-bar red dot. This post covers the two design decisions: where the data comes from, and why the monitoring is passive.
The source of truth: plist files, not launchctl
The first instinct was to use launchctl list output as the data source. That’s wrong in an obvious way once you look: the list mixes in hundreds of system services and one-shot jobs, and agents you’ve booted out don’t appear at all — which is exactly the set a user most needs to see (“stopped, but still installed”).
The right merge strategy is to be clear about which question each source can answer:
~/Library/LaunchAgents/*.plistanswers “what’s installed” — every plist the user deliberately put there, running or not. This is the truth for the roster;launchctl listonly answers “what state is it in right now” — used purely to backfill PIDs and last exit codes.
Plist files as the primary table, launchctl as backfill: the result matches the user’s mental model of “my background services.” Running ones show a PID, stopped ones show stopped, and installed-but-crashed ones show their exit code — that third category is the silent failure we’re hunting.
Two filters on top: com.apple.* is the system’s business, not the user’s; homebrew.mxcl.* already has its own Brew Services section, so no duplicates.
Operations: bootstrap / bootout, not load / unload
Start and stop go through the modern interface:
launchctl bootstrap gui/$UID <plist path> # start
launchctl bootout gui/$UID/<label> # stop
load/unload is explicitly deprecated and increasingly superstition on newer systems. Two small defenses: bootstrapping an already-running agent returns an error, which gets swallowed (if the outcome matches the intent, it isn’t an error); records with an empty plist path refuse to operate — the UI already disables the button, this is the defensive backstop.
The red dot: turning “check” into “glance”
The badge condition is one line:
/// Any LaunchAgent that last exited abnormally = red dot.
var hasFailedAgents: Bool {
agents.contains { !$0.isRunning && $0.lastExitStatus != 0 }
}
Backed by a 5-minute refresh timer. The math checks out: one launchctl list costs ~50ms, so every 5 minutes rounds to zero CPU; and a 5-minute detection delay is more than good enough for a problem whose baseline is “the backup was dead for three weeks and nobody knew.”
The keyword is passive. Monitoring tools implicitly assume you’ll open them — but nobody opens a panel every day on the off chance something died. The entire point of a monitor that lives in the menu bar is that it watches while you don’t. Until the red dot appears, the feature should be invisible.
Takeaways
- Assign data sources by the question they can answer. launchctl can’t tell you what’s installed; the filesystem can’t tell you how it’s running. Let each answer its own question and the merge stays clean;
- Abnormal exit is a third state, not a flavor of “stopped.” A running/stopped binary classification files “crashed” under “stopped” — and “crashed” is the only state that needs a human;
- Design monitoring in a resident tool as a glance, not a check. Boil the verdict down to a boolean (dot or no dot); details are one click deeper. Attention is a far scarcer resource than CPU;
- Derive the polling interval from failure tolerance, not from “faster is better.” 50ms every 5 minutes is invisible; 50ms every second is one more process that needs monitoring.
Comments