菜单栏上的一颗红点——LaunchAgent 的被动监控
Pier 复盘:LaunchAgent 挂了从来不会通知你。plist 文件当真相、launchctl 回填状态、退出码非零亮红点,每 5 分钟刷一次——监控的价值在于你不看它的时候它也在看。
macOS 的 LaunchAgent 有个和 cron 一模一样的毛病:静默失败。同步脚本、备份任务、开机自启的小 daemon——挂了就是挂了,没有通知,没有红点,直到某天你发现三个星期的备份是空的。launchctl list 能查,但没人天天查。
Pier 2.0 的 Dev tab 加了 LaunchAgents 管理,配套一个菜单栏红点。这篇讲两个设计决定:数据从哪来、以及为什么是「被动」监控。
谁是真相:plist 文件,不是 launchctl
第一版直觉是把 launchctl list 的输出直接当数据源,马上发现不对:这个列表里混着几百个系统服务和一次性任务,而且已经 bootout 的 agent 根本不在里面——恰恰是「我停掉了但还装着」的那批,用户最需要看到。
正确的合并策略是分清两个数据源各自能回答什么:
~/Library/LaunchAgents/*.plist回答「装了什么」——用户主动放进去的每一个 plist,无论跑没跑,这是清单的真相;launchctl list只回答「现在什么状态」——用来回填 PID 和上次退出码。
以 plist 文件为主表、launchctl 回填状态,结果正好是用户心智里的「我的后台服务」:在跑的显示 PID,停了的显示已停止,装着但异常退出的显示退出码——第三种就是要抓的静默失败。
清单再过两道过滤:com.apple.* 是系统的事,用户管不着;homebrew.mxcl.* 由 Brew Services 区显示,不重复出现。
操作:bootstrap / bootout,不是 load / unload
启停走的是现代接口:
launchctl bootstrap gui/$UID <plist 路径> # 启动
launchctl bootout gui/$UID/<label> # 停止
load/unload 是 Apple 明文弃用的旧命令,行为在新系统上越来越玄学。两个小防御:对已在跑的 agent 执行 bootstrap 会报错,直接吞掉(结果符合预期就不是错误);plist 路径为空的记录拒绝操作——UI 上按钮本来就是禁用的,这层是防御性兜底。
红点:把「查」变成「瞟」
菜单栏徽章的判定一行就够:
/// 有 LaunchAgent 上次异常退出 = 红点。
var hasFailedAgents: Bool {
agents.contains { !$0.isRunning && $0.lastExitStatus != 0 }
}
配一个 5 分钟的定时刷新。成本算过账:一次 launchctl list 约 50ms,5 分钟一次的 CPU 开销约等于零;而 5 分钟的检测延迟,对「备份挂了三个星期没人知道」这个问题来说绰绰有余。
这里的关键词是被动。市面上的监控工具默认假设你会打开它看——但没人会为了「说不定有东西挂了」每天打开一个面板。资源监视器长驻菜单栏的真正价值就在这:你不看它的时候它也在看。红点出现之前,这个功能应该是隐形的。
复盘
- 数据源要按「它能回答什么问题」分工。launchctl 回答不了「装了什么」,文件系统回答不了「跑得怎么样」,谁的问题谁回答,合并才干净;
- 异常退出是三态,不是二态。「在跑 / 停了」的二分法会把「挂了」归进「停了」,而「挂了」才是唯一需要人介入的状态;
- 常驻工具的监控要设计成「瞟一眼」而不是「查一下」。判定做成布尔(有没有红点),细节点开再看——注意力是比 CPU 贵得多的资源;
- 轮询周期用故障容忍度反推,不是越快越好。50ms × 每 5 分钟是无感,50ms × 每秒就是又一个需要被监控的进程了。
留言