launchd Intro:macOS 自动化实践

launchd Intro:macOS 自动化实践(以 send_daily_2330.sh 为例)

本文面向 macOS 环境,介绍如何使用 launchd 将一个项目脚本稳定地"每天 23:30 自动执行",并给出排错方法与最佳实践。

1. 为什么用 launchd(而不是 cron)

launchd 是 macOS 原生的作业调度与守护进程管理系统,适合做长期、稳定的定时任务。

对比 cron

  • launchd 与系统更一致(用户登录、权限、沙盒、安全策略、日志系统)
  • launchd 支持更丰富的触发条件与更好的状态查询(launchctl print
  • cron 在 macOS 上更容易遇到 PATH/环境变量缺失、权限与唤醒不稳定等问题

如果你的任务需要访问 Apple Calendar(EventKit)、钥匙串、用户目录文件等,优先使用 LaunchAgents(用户态)而不是 LaunchDaemons(系统态)。

2. LaunchAgents vs LaunchDaemons:放哪里?

通常建议用 LaunchAgents(只对当前用户生效):

  • plist 路径:~/Library/LaunchAgents/
  • 运行身份:当前登录用户
  • 常见用途:桌面自动化、访问用户日历/文件、发送邮件、个人定时脚本

LaunchDaemons(全局、系统态):

  • plist 路径:/Library/LaunchDaemons/
  • 运行身份:root
  • 常见用途:系统服务、全局守护进程(不建议用于个人日历与邮件)

3. 创建 launchd 配置(plist)

创建文件(建议 label 规范化、全小写、域名式命名):

  • ~/Library/LaunchAgents/com.aiworkflow.senddaily2330.plist

内容示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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>com.aiworkflow.senddaily2330</string>

<key>ProgramArguments</key>
<array>
<string>/path/to/your/script.sh</string>
</array>

<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>23</integer>
<key>Minute</key><integer>30</integer>
</dict>

<key>RunAtLoad</key><true/>

<key>WorkingDirectory</key>
<string>/path/to/your/project</string>

<key>StandardOutPath</key>
<string>/path/to/logs/launchd.out</string>
<key>StandardErrorPath</key>
<string>/path/to/logs/launchd.err</string>
</dict>
</plist>

4. conda 环境注意事项(非交互环境)

非交互环境(launchd)里,conda 可能不在 PATH,导致脚本里 command -v conda 失败。

推荐策略:

  • 最稳:在脚本里固定 CONDA_BASE(例如 /Users/lianbin/miniconda3),再 source "$CONDA_BASE/etc/profile.d/conda.sh"conda activate ...
  • 次稳:在 plist 的 EnvironmentVariables 里补 PATH,把 conda bin 加进去

5. 安装、启动、查看状态(命令)

macOS 新版推荐使用 bootstrap/bootout(而不是旧的 load/unload)。

5.1 启动

1
2
3
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.yourlabel.plist
launchctl enable gui/$(id -u)/com.yourlabel
launchctl kickstart -k gui/$(id -u)/com.yourlabel

5.2 查看状态

1
launchctl print gui/$(id -u)/com.yourlabel

5.3 停止与卸载

1
2
launchctl disable gui/$(id -u)/com.yourlabel
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.yourlabel.plist

6. 日志与排错

常见排错顺序:

  1. 先看 launchd.err(多为环境/权限/路径问题)
  2. 再看业务日志
  3. 执行一次手动运行确认脚本本身可跑
  4. launchctl print ... 查看最近一次退出码与原因

7. 推荐的验证流程

  1. 手动运行脚本确认可执行
  2. 创建 plist,并用 kickstart 立即触发一次
  3. 检查日志确认执行成功