macOS 定时任务(自动化任务)

在 macOS 上实现定时任务的推荐方式之一是使用系统自带的 launchd 守护进程。你可以通过配置 plist 文件并使用 launchctl 命令进行任务的加载、卸载和管理。本文将详细介绍使用 launchd 的方法,包含定时执行、系统重启后仍能生效、日志输出等完整配置。

💡 推荐先阅读:「macOS 的守护程序和服务」了解 launchd 的基础知识。

macOS 的守护程序为 launchd,任务的生命周期通过 .plist 配置文件进行管理。

mkdir -p ~/Library/LaunchAgents
nano ~/Library/LaunchAgents/com.example.run_task.plist

将以下内容粘贴进去,并根据实际情况修改路径:

<?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.run_task</string>

    <!-- 要执行的命令及参数 -->
    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>/path/to/task.sh</string>
    </array>

    <!-- 每天 16:00 运行 -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>16</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <!-- 在加载(如重启后)立即运行一次 -->
    <key>RunAtLoad</key>
    <true/>

    <!-- 日志输出位置(可选) -->
    <key>StandardOutPath</key>
    <string>/tmp/com.example.task.out</string>
    <key>StandardErrorPath</key>
    <string>/tmp/com.example.task.err</string>
</dict>
</plist>

<key>StartInterval</key>
<integer>3600</integer>
<key>StartCalendarInterval</key>
<dict>
    <key>Minute</key>
    <integer>45</integer>
    <key>Hour</key>
    <integer>13</integer>
    <key>Day</key>
    <integer>7</integer>
</dict>
<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>9</integer>
    <key>Minute</key>
    <integer>0</integer>
    <key>Weekday</key>
    <integer>1</integer> <!-- 1=周一, 7=周日 -->
</dict>
<key>WatchPaths</key>
<array>
    <string>/path/to/watch/directory</string>
</array>

更多配置选项详见:Creating Launchd Jobs – Apple Developer


launchctl load ~/Library/LaunchAgents/com.example.run_task.plist
launchctl unload ~/Library/LaunchAgents/com.example.run_task.plist

⚠️ 修改 .plist 文件后必须先 unloadload 以使更改生效。

launchctl start com.example.run_task
launchctl stop com.example.run_task
launchctl list | grep com.example.run_task

  • ~/Library/LaunchAgents:仅当前用户自动加载(推荐)

  • /Library/LaunchAgents:系统所有用户自动加载(需要 sudo 权限)

  • /Library/LaunchDaemons:系统级后台服务,适合无用户登录场景


launchctl unload ~/Library/LaunchAgents/com.example.run_task.plist
launchctl load ~/Library/LaunchAgents/com.example.run_task.plist

确保你使用的可执行文件路径正确,例如:

which bash
# 或
which node

替换 .plist 中的 <string>路径</string>,避免因路径错误导致任务执行失败。


launchd 运行的任务默认环境变量很少,可能导致脚本找不到命令。
推荐解决方法:在脚本中设置 PATH 环境变量

#!/bin/zsh
export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export HOME=/Users/yourusername

# 你的脚本内容

# 查看任务列表和状态
launchctl list | grep com.example.run_task

# 查看系统日志(过滤相关任务)
log show --predicate 'subsystem == "com.apple.launchd"' --last 1h | grep com.example.run_task

# 查看自定义日志文件
tail -f /tmp/com.example.task.out
tail -f /tmp/com.example.task.err
  • 0:任务成功执行
  • 1:一般错误
  • 126:文件不可执行
  • 127:命令未找到

<?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.backup</string>
    
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/rsync</string>
        <string>-av</string>
        <string>/Users/username/Documents/</string>
        <string>/Volumes/Backup/Documents/</string>
    </array>
    
    <!-- 每天凌晨2点执行 -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    
    <key>StandardOutPath</key>
    <string>/tmp/backup.out</string>
    <key>StandardErrorPath</key>
    <string>/tmp/backup.err</string>
</dict>
</plist>

创建清理脚本 cleanup.sh

#!/bin/bash
# 清理临时文件
find /tmp -name "*.tmp" -mtime +7 -delete
find ~/Downloads -name "*.dmg" -mtime +30 -delete

echo "$(date): 清理完成" >> /tmp/cleanup.log

对应的 plist 文件:

<?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.cleanup</string>
    
    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>/Users/username/scripts/cleanup.sh</string>
    </array>
    
    <!-- 每周日凌晨1点执行 -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>1</integer>
        <key>Minute</key>
        <integer>0</integer>
        <key>Weekday</key>
        <integer>0</integer>
    </dict>
</dict>
</plist>

  1. 卸载任务

    launchctl unload ~/Library/LaunchAgents/com.example.run_task.plist
    
  2. 删除配置文件

    rm ~/Library/LaunchAgents/com.example.run_task.plist
    
  3. 验证删除

    launchctl list | grep com.example.run_task
    # 应该没有输出结果
    
  4. 清理日志文件(可选):

    rm /tmp/com.example.task.out
    rm /tmp/com.example.task.err
    

可能原因

  • 脚本路径错误
  • 没有执行权限
  • 环境变量缺失

解决方案

# 检查脚本是否存在和可执行
ls -la /path/to/script.sh
chmod +x /path/to/script.sh

# 手动测试脚本
/path/to/script.sh

推荐解决方案: 在 shell 脚本中设置 PATH 环境变量,例如:

export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

# 剩余脚本内容

可能原因:

  • .plist 文件路径错误
  • .plist 文件内容错误,例如语法错误(标签错误、标签重复、标签缺失等)

原因: 通过 cron 执行shell脚本任务(例如包含clear命令),需要通过环境变量配置指定运行时使用的终端,否则会报错。

解决方案

  • 首先查看本地 TERM 环境变量内容 echo $TERM,例如 xterm-256color
  • 然后在 .plist 文件中添加 TERM 环境变量,例如:
<key>EnvironmentVariables</key>
<array>
    <dict>
        <key>TERM</key>
        <string>xterm-256color</string>
    </dict>
</array>

<key>SoftResourceLimits</key>
<dict>
    <key>NumberOfFiles</key>
    <integer>1024</integer>
</dict>
<key>WorkingDirectory</key>
<string>/Users/username/workspace</string>
<key>KeepAlive</key>
<true/>
<key>StartOnMount</key>
<true/>

除了 launchd,你也可以考虑以下工具:

  • pm2:适用于 Node.js 应用的进程管理器
  • Automator + Calendar:图形化界面的自动化工具

通过以上步骤,你可以方便地在 macOS 上创建稳定、可靠的定时任务,并支持开机自启与日志记录。


参考资料: