Fork me on GitHub

launchd——macOS自动调度神器

launchd 是macOS(以前的Mac OS X)中的一个系统级进程管理框架,它负责启动、停止和管理系统和用户级别的进程、服务(守护进程)和脚本。可以将其视为类似Linux 系统下的systemd 或supervisor,它通过解析位于特定目录下的XML 格式的 .plist 配置文件来定义要执行的任务。

macOS launchd 通用教程

macOS launchd 通用教程**,适合你以后自己写定时任务或服务。内容分为 基础概念 → 常见用法 → 常见问题 → 调试技巧,这样你可以按需选择写合适的 shell 脚本来跑。

1. 基础概念

  • launchd:macOS 的系统任务调度器,Apple 用它替代了 cron

  • plist 文件:任务的配置文件,扩展名 .plist,用 XML 格式写。

  • 位置

    • ~/Library/LaunchAgents/ → 用户级任务(推荐放这里)
    • /Library/LaunchAgents/ → 所有用户可用
    • /Library/LaunchDaemons/ → 系统级任务,需要 root

2. plist 文件结构

一个最小可运行的 plist 文件长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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.example.task</string>

<!-- 执行命令(必须用绝对路径) -->
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>/Users/you/scripts/my_task.sh</string>
</array>

<!-- 定时方式之一:每隔多少秒执行一次 -->
<key>StartInterval</key>
<integer>300</integer> <!-- 每 300 秒执行一次 -->
</dict>
</plist>

3. 定时方式

launchd 有几种常见的定时触发方式:

方式 1:间隔执行

1
2
<key>StartInterval</key>
<integer>300</integer>

→ 每 5 分钟执行一次。

方式 2:指定时间点执行

1
2
3
4
5
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>

→ 每天 09:00 执行一次。
支持的键有:Minute Hour Day Weekday Month

方式 3:登录时执行

1
2
<key>RunAtLoad</key>
<true/>

方式 4:文件变化触发

1
2
3
4
<key>WatchPaths</key>
<array>
<string>/Users/you/Documents/target_folder</string>
</array>

→ 目录变化时执行。


4. 常用配置项

  • 日志(推荐加上,方便调试)
1
2
3
4
<key>StandardOutPath</key>
<string>/tmp/my_task.out</string>
<key>StandardErrorPath</key>
<string>/tmp/my_task.err</string>
  • 环境变量(如果需要)
1
2
3
4
5
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/Users/you/.local/bin</string>
</dict>

5. 操作命令

加载任务

1
launchctl load ~/Library/LaunchAgents/com.example.task.plist

卸载任务

1
launchctl unload ~/Library/LaunchAgents/com.example.task.plist

立即运行一次

1
launchctl start com.example.task

停止正在运行的任务

1
launchctl stop com.example.task

查看任务是否存在

1
launchctl list | grep example

6. 常见问题 & 解决方法

  1. 脚本没执行?

    • 检查是否用绝对路径(脚本、python、venv)。
    • 检查脚本是否 chmod +x
  2. 命令找不到?

    • EnvironmentVariables 或在脚本里写绝对路径。
  3. 运行了但没效果?

    • 打开 /tmp/my_task.out /tmp/my_task.err 看日志。
  4. plist 文件修改后没生效?

    • launchctl unloadlaunchctl load

7. 调试技巧

  • 在脚本开头打印环境:

    1
    2
    env >> /tmp/my_task.env.log
    which python >> /tmp/my_task.env.log
  • log show 查看系统日志:

    1
    log show --predicate 'process == "sh"' --last 1h
  • 简单测试:把任务改成 echo "Hello" >> /tmp/test.log,确认 launchd 能正常运行。


8. 踩坑记录

在 macOS 10.15 及以上版本(Catalina、Big Sur、Ventura 等),访问 Documents、Desktop、Downloads 或外部磁盘 的文件,需要 TCC(透明访问控制)授权。
1
2
3
4
比如你的脚本在 ~/Documents/software/XXX/ 目录下
launchd 以 GUI 用户身份运行,但 没有权限访问 Documents
手动在终端运行可以成功,因为终端已经在你的用户会话下,有访问权限
launchd 运行时 环境不同,TCC 会拦截,导致 Operation not permitted

解决方案:改脚本目录(最简单),远离Documents、Desktop、Downloads 或外部磁盘

✅ 总结:

  • cron → 用 launchd(Apple 推荐)
  • 绝对路径 > 环境变量(最稳妥)
  • 调试靠日志StandardOutPath/StandardErrorPath
-------------The End-------------