跳至主要内容

[Mac] macOS 定時排程全攻略:Crontab 與 LaunchDaemon

在 macOS 系統中設定定時執行任務(如每 30 分鐘執行一次腳本、定期備份、定時健康檢查等),常見的方式有傳統的 crontab 以及 macOS 原生的 launchd(LaunchDaemon / LaunchAgent)。

本文整理兩種方式的運作機制差異、開機未登入狀態下的執行策略,以及實作步驟。


機制比較:哪種方式適合你?

macOS 底層由 launchd 接管所有行程管理,官方已將傳統 cron 標記為舊機制(Deprecated)。

機制設定位置執行身分使用者未登入時是否執行?適用場景
User Crontabcrontab -e一般使用者❌ 否(受限於使用者 session)個人登入期間的簡單週期任務
Root Crontabsudo crontab -eroot⚠️ 部分支援(需 cron 服務常駐)習慣 Linux crontab 語法的系統排程
LaunchAgent~/Library/LaunchAgents/當前使用者❌ 否(僅在 GUI 登入後執行)與桌面、通知或使用者檔案相關任務
LaunchDaemon/Library/LaunchDaemons/root / 指定使用者是(開機即在系統底層運行)系統級背景服務、無人值守排程

[!NOTE] 為什麼「開機未登入」推薦 LaunchDaemon? LaunchDaemon 由 macOS 系統根行程(PID 1)直接管理。電腦只要開機通電,無需任何使用者在登入畫面輸入密碼,排程就會準時觸發,且不易受 macOS TCC(權限防護)干擾。


範例情境說明

假設我們有一個簡單的測試腳本 /usr/local/bin/hello_task.sh,目標是每 30 分鐘自動執行一次,並記錄執行日誌。

準備示範腳本

建立範例腳本 /usr/local/bin/hello_task.sh

sudo tee /usr/local/bin/hello_task.sh << 'EOF'
#!/bin/bash

NOW=$(date "+%Y-%m-%d %H:%M:%S")
echo "[${NOW}] Hello World: Task executed successfully."
EOF

# 賦予可執行權限
sudo chmod +x /usr/local/bin/hello_task.sh

方案一:使用 macOS 原生 LaunchDaemon(⭐ 推薦)

步驟 1:建立 plist 設定檔

/Library/LaunchDaemons/ 底下建立屬性列表檔案(以 com.example.hellotask.plist 為例):

sudo tee /Library/LaunchDaemons/com.example.hellotask.plist << 'EOF'
<?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.hellotask</string>

<!-- 執行的程式與參數 -->
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/usr/local/bin/hello_task.sh</string>
</array>

<!-- 間隔時間(秒):1800 秒 = 30 分鐘 -->
<key>StartInterval</key>
<integer>1800</integer>

<!-- 載入服務時立即先執行一次(可選) -->
<key>RunAtLoad</key>
<true/>

<!-- 標準輸出與錯誤日誌路徑 -->
<key>StandardOutPath</key>
<string>/var/log/hello_task.log</string>
<key>StandardErrorPath</key>
<string>/var/log/hello_task_err.log</string>
</dict>
</plist>
EOF

步驟 2:設定權限並載入服務

LaunchDaemon 要求設定檔擁有者必須是 root:wheel,且權限必須為 644

# 1. 修正權限
sudo chown root:wheel /Library/LaunchDaemons/com.example.hellotask.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.hellotask.plist

# 2. 載入並啟用排程
sudo launchctl load -w /Library/LaunchDaemons/com.example.hellotask.plist

步驟 3:驗證與管理

檢查服務狀態

sudo launchctl list | grep hellotask

輸出格式範例:

- 0 com.example.hellotask
  • 第一欄(PID):顯示 - 表示該任務為間隔觸發型排程,執行完畢即退出等待下一次觸發。
  • 第二欄(Status)0 代表上次執行成功;非 0 代表上次執行時腳本返回的 Exit Code。
  • 第三欄(Label):服務標籤名稱。

查看日誌輸出

cat /var/log/hello_task.log

停止與移除排程

# 卸載排程
sudo launchctl unload -w /Library/LaunchDaemons/com.example.hellotask.plist

# 刪除設定檔
sudo rm /Library/LaunchDaemons/com.example.hellotask.plist

方案二:使用 Root Crontab

如果偏好使用標準 Cron 表達式,且希望在未登入狀態下依然執行,必須使用 Root 權限的 Crontab

步驟 1:編輯 Root Crontab

sudo crontab -e

步驟 2:設定排程

在編輯器中加入排程規則(每 30 分鐘執行一次):

# 每 30 分鐘執行一次
*/30 * * * * /usr/local/bin/hello_task.sh >> /var/log/hello_task.log 2>&1

常用 Cron 表達式速查

  • */30 * * * *:每 30 分鐘
  • 0 * * * *:每整點
  • 0 2 * * *:每天凌晨 2:00
  • 0 9 * * 1-5:每週一至週五早上 9:00

步驟 3:確認排程清單

sudo crontab -l

進階技巧與注意事項

1. 固定時間觸發(Calendar Interval)

若希望像 Cron 指定「每天固定時分」而非間隔秒數,可在 plist 中使用 StartCalendarInterval

<!-- 每天 03:30 執行 -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>30</integer>
</dict>

2. 環境變數(PATH)問題

LaunchDaemon 預設的 PATH 非常精簡(通常僅 /usr/bin:/bin:/usr/sbin:/sbin)。若腳本需使用 Homebrew 工具(如 /opt/homebrew/bin/usr/local/bin),建議在 plist 中補齊 EnvironmentVariables

<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>

3. Mac 休眠(Sleep)影響

  • 當 Mac 進入深度休眠時,CPU 會暫停運作。
  • launchd 的特性:若休眠期間錯過了觸發時間,Mac 在喚醒後會自動補執行一次錯過的任務。

Agent Prompt 快速設定模版

將以下 Prompt 複製給 AI Agent,即可讓 Agent 自動為任何腳本建立對應的 LaunchDaemon 定時排程:

請幫我在 macOS 上為腳本 `<腳本完整路徑,例如:/usr/local/bin/my_script.sh>` 設定每 `<時間間隔,例如:30 分鐘>` 自動執行的背景定時任務。

請遵循以下要求:
1. 採用 macOS 系統級 LaunchDaemon(位於 `/Library/LaunchDaemons/com.<識別名稱>.plist`),確保即使「電腦開機但未登入任何帳號」時依然能定時執行。
2. 設定 `StartInterval``<秒數,例如:1800>`,並加入 `RunAtLoad: true` 與標準輸出/錯誤日誌設定(輸出至 `/var/log/<名稱>.log`)。
3. 確保設定檔權限為 `root:wheel 644`,並產生載入指令 (`launchctl load -w`) 與狀態驗證指令 (`launchctl list`)。
4. 示範或產出時請使用安全且通用的環境變數設定。

相關連結