利用 launchd 实现 Hexo 博客定时自动推送

利用 launchd 实现 Hexo 博客定时自动推送
flowwalker利用 launchd 实现 Hexo 博客定时自动推送
零、 选择工具:Crontab vs Launchd
在 macOS 上实现自动化推送,通常有两种主流选择:
crontab
Launchd
1. Crontab (传统的 Unix 定时任务)
操作指令:
- 在终端输入
crontab -e进入编辑界面。- 添加一行配置(例如每天 22:30 执行):
30 22 * * * /bin/bash /Users/focus/Library/LaunchAgents/auto_push.sh >> /tmp/hexo_cron.log 2>&1- 保存并退出即可。
特点: 语法极其简单,配置迅速。但它的致命缺点是不支持“补跑”。如果 22:30 你的 Mac 盖上了盖子处于休眠状态,crontab 任务就会直接跳过,直到第二天同一时间。
2. Launchd (Apple 官方)
特点: 虽然.plist的 XML 格式比 crontab 复杂,但其拥有 StartCalendarInterval 机制。如果设定时间点电脑在休眠,launchd会在系统唤醒后立即补跑任务。对于笔记备份和博客推送这种“必须成功”的任务,launchd是更可靠的选择。
一、确认环境
在编写脚本前,需在终端执行以下诊断,确保命令在“纯净环境”下依然有效。
1. 确认命令的物理路径
launchd 不识别 hexo 或 git 这种简写,需提供绝对路径。
- 输入指令:
which hexo - 预期结果:记录返回的路径(如
/opt/homebrew/bin/hexo)。 - 在脚本中替换所有
hexo字样,防止command not found。
2. 确认环境变量 $PATH
脚本需要知道去哪里寻找执行引擎(如 Node.js)。
- 输入指令:
echo $PATH - 预期结果:一串由冒号分隔的路径(如
/opt/homebrew/bin:/usr/local/bin...)。 - 将这串路径复制,写在脚本开头的
export PATH="..."后面。
3. 确认 SSH 密钥授权状态
后台运行无法弹出密码框,必须确认密钥已托管给系统。
- 输入指令:
ssh-add -l - 预期结果:看到密钥指纹。如果显示
The agent has no identities,则需要在脚本中手动加载。 - 手动执行
hexo d。如果无需输入密码即可推送,则配置正确。
二、自动化 Shell 脚本
我们需要创建一个封装脚本,手动重构环境变量并定位物理路径。
脚本位置: ~/Library/LaunchAgents/auto_push.sh
1 |
|
Shell 脚本逻辑框架极速学习
模块 对应 Python 概念 作用 细节 #! /bin/bash \ 告诉系统用哪个“翻译官”来读这个文件。 必须放在第一行。 export PATH os.environ['PATH']重构环境。让脚本能找到 node,hexo等工具。后台运行不读 .zshrc,必须手动声明。ssh-add Authentication免密授权。从系统钥匙串读取 Git 推送所需的私钥。 后台运行无法手动输密码,需靠此指令。 Variables Variable(变量)存储路径、URL、检索结果。 注意:等号两边不能有空格。 $(command) subprocess.check_output命令替换。执行一个命令并把输出结果存入变量。 如 CHANGES=$(find...)。$ (逻辑判断) if not CHANGES:短路逻辑。根据变量是否为空生成不同的通知文案。 [ -z "$VAR" ]是检查字符串是否为空。$? (状态码) try...except或return code捕获上一步状态。 0代表成功,非0代表报错。if [ $? -eq 0 ]决定是否发钉钉消息。curl requests.post()网络请求。向钉钉 API 发送 JSON 数据。 使用 -d参数传递 JSON 字符串。三元运算符。
1 [ -z "$CHANGES" ] && CONTENT="A" || CONTENT="B"
[ -z "$CHANGES" ]:这是一个判断题。-z表示 “Zero”,意思是“检查字符串长度是否为 0”。
- 如果今天没写文章,
$CHANGES是空的,判断为 真(True)。- 如果今天写了文章,判断为 假(False)。
&&(逻辑与):只有当前面的判断为 真 时,才执行。||(逻辑或):只有当前面的判断为 假 时,才执行。
- 路径 A (没写文章):
判断为真 → 触发&&后面的赋值 → 跳过||。
结果:CONTENT变成了“今日无新文章”。- 路径 B (写了文章):
判断为假 → 跳过&&→ 触发||后面的赋值。
结果:CONTENT变成了“今日新推送文章:…”。
$()先运行内部,然后命令替换
三、配置调度:launchd Plist 文件
在 ~/Library/LaunchAgents/ 下创建 com.user.pushblog.plist,定义触发时间。
1. plist 基础结构
plist 是 XML 格式的键值对列表。所有配置必须包裹在 <dict> 标签内。
XML
1 |
|
2. 参数表
| 键名 (Key) | 数据类型 | 说明 | 示例 |
|---|---|---|---|
| Label | string | 唯一身份标识。任务的“身份证号”,不可重复。 | com.focus.blog |
| ProgramArguments | array | 执行指令序列。第一个值是解释器,第二个是脚本路径。 | /bin/bash, /path/to/sh |
| StartCalendarInterval | dict | 定时触发器。设定具体的执行时间点。 | Hour: 22, Minute: 30 |
| StandardOutPath | string | 标准输出日志。保存脚本运行时的正常提示。 | /tmp/blog.log |
| StandardErrorPath | string | 标准错误日志。保存脚本运行失败的报错信息。 | /tmp/blog.err |
3. 格式例子
定点触发(每天 22:30):
1
2
3
4
5<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>22</integer>
<key>Minute</key><integer>30</integer>
</dict>周期触发(每隔 1 小时/3600 秒):
1
2<key>StartInterval</key>
<integer>3600</integer>
4. 增强功能
- RunAtLoad:
bool类型。设为<true/>则在任务加载或开机时立即执行一次。 - WorkingDirectory:
string类型。指定脚本运行时的根目录,免去脚本内cd的麻烦。 - KeepAlive:
bool类型。如果程序崩溃,系统会自动重启它(适用于常驻服务)。
5. 编写
语法校验:编写完成后,使用终端指令检查 XML 格式是否合法:
plutil -lint ~/Library/LaunchAgents/your_filename.plist绝对路径原则:
plist内部禁止使用~或相对路径,必须使用全路径(如/Users/focus/...)。生效逻辑:修改
plist内容后,必须执行unload再load,系统才会读取新配置。
6. 操作速查
- 加载:
launchctl load [文件路径] - 卸载:
launchctl unload [文件路径] - 启动:
launchctl start [Label] - 停止:
launchctl stop [Label]
四、常用操作指令集
赋权脚本:
chmod +x ~/Library/LaunchAgents/auto_push.sh加载任务:
launchctl load ~/Library/LaunchAgents/com.user.pushblog.plist手动强制触发测试:(停止stop)
launchctl start com.user.pushblog卸载任务(修改配置后必须执行):
launchctl unload ~/Library/LaunchAgents/com.user.pushblog.plist查看运行日志:
tail -f /tmp/hexo_push.log完整查看日志:
cat /tmp/hexo_push.log
Launchd 运维速查笔记(发疯记录)
1. 操作顺序(先下后上)
原则:修改 plist 文件或脚本内容后,必须遵循“先卸载、再加载”的闭环,否则系统缓存会导致配置不生效或报错。
- 卸载 (Unload):
launchctl unload ~/Library/LaunchAgents/com.user.pushblog.plist - 修改 (Modify):编辑
.plist或.sh文件。 - 加载 (Load):
launchctl load ~/Library/LaunchAgents/com.user.pushblog.plist - 测试 (Test):
launchctl start com.user.pushblog(手动触发一次验证逻辑)。
2. 报错处理:Load failed: 5 (I/O Error)
- 现象:执行
load时提示Input/output error。 - 病因:任务已在内存中加载,或在未卸载时修改了
Label导致冲突。 - 药方:
- 先执行
unload强行清理。 - 检查 XML 语法:
plutil -lint [路径]。 - 若仍不行,多unload几次
- 先执行
3. 静默卸载 (2>/dev/null)
- 用法:
launchctl unload [路径] 2>/dev/null - 作用:将错误输出(Standard Error)重定向到不输出。
- 场景:当你编写自动化脚本(如
deploy.sh)时,不确定任务是否已加载。加上这一句可以确保无论任务是否存在,都不会弹出干扰报错,直接执行后续的load。
4. 状态检查:launchctl list | grep pushblog
观察任务“活得好不好”。输出通常包含三列:
| PID (进程号) | Last Status (状态码) | Label (任务名) |
|---|---|---|
12345 | 0 | com.user.pushblog |
- | 0 | com.user.pushblog |
- | 78 | com.user.pushblog |
- PID 为数字:说明脚本正在后台运行。
- PID 为
-:说明任务已加载,正在等待定时触发。 - Status 为
0:上次运行完美成功。 - Status 非
0:上次运行失败(例如78通常代表路径错误或权限不足)。
5. 核心Bug:macOS 隐私沙盒 (FDA)
这是本次任务“手动行,自动不行”的头号杀手。
- 现象:脚本在终端运行完美,但通过
launchd(plist) 运行时,find无法读取Desktop目录下的文章。 - 原理:后台进程默认被禁止访问用户敏感目录。即便脚本路径正确,系统也会静默拦截
find的深入探测。 - 对策:在 系统设置 -> 隐私与安全性 -> 完全磁盘访问权限 中,手动添加
/bin/bash(或你的脚本解释器)并开启开关。
6. 工具差异:BSD find 的“洁癖”
macOS 自带的是 BSD 版 find,它比 Linux 的 GNU 版更固执。
- 坑点:不支持浮点数(如
-mtime -1.1会报错bad unit)。 - 对策:改用分钟单位
-mmin。例如用-mmin -1560代替-mtime -1.1,实现更精准、兼容性更好的时间窗口控制。
7. 环境隔离:极简的 Shell 环境
launchd 运行脚本时不会加载你的个人配置文件(.zshrc 或 .bash_profile)。
- 坑点:缺少
PATH可能导致找不到hexo;缺少LANG可能导致处理中文路径时sed正则失效。 - 对策:在脚本开头硬编码
export PATH和export LANG=en_US.UTF-8。













