macOS · Swift · Pier

菜单栏上的一颗红点——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 × 每秒就是又一个需要被监控的进程了。

留言

  • 加载中…

留言先审后发,通过后公开显示;邮箱只有站主可见。