Mac使用 launchctl 创建定时任务 & Operation not permitted问题处理
回头来看,这是个很简单的工具使用,但是却因为一个问题查了半天,因此记录一下。
使用launchctl创建定时任务
这里以定时清空某个文件夹内容为例。
创建shell脚本
脚本文件命名为delete.sh
#!/bin/bash
rm -rf /待删除的文件路径
设置脚本文件权限
sudo chmod 777 /delete.sh
创建com.delete.plist
不同创建目录的区别:
位置 | 类型 | 以什么用户权限运行 | 运行时机 |
---|---|---|---|
/System/Library/LaunchDaemons | System Daemons | root / 指定用户 | 开机时 |
/System/Library/LaunchAgents | System Agents | 当前登录用户 | 任意用户登录 |
/Library/LaunchDaemons | Global Daemons | root / 指定用户 | 开机时 |
/Library/LaunchAgents | Global Agents | 当前登录用户 | 任意用户登录 |
~/Library/LaunchAgents | User Agents | 当前登录用户 | 指定用户登录时 |
这里以在 /Library/LaunchAgents 创建为例
<?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.delete.plist</string>
<!-- 指定要运行的脚本 -->
<key>ProgramArguments</key>
<array>
<string>/Users/xxx/demo/delete.sh</string>
</array>
<!-- 指定要运行的时间 当前为每天早晨5:00-->
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>0</integer>
<key>Hour</key>
<integer>5</integer>
</dict>
<!-- 标准输出文件 -->
<key>StandardOutPath</key>
<string>/Users/xxx/Downloads/demo/run.log</string>
<!-- 标准错误输出文件,错误日志 -->
<key>StandardErrorPath</key>
<string>/Users/xxx/Downloads/demo//run.err</string>
</dict>
</plist>
加载com.delete.plist
launchctl load -w /Library/LaunchAgents/com.delete.plist
到这里,通过 launchctl 创建定时任务的过程已经结束了。
报错: Operation not permitted
记录了一下自己分析的过程,着急要解决方案的可以跳过。
分析过程
上述流程结束后,检查了plist文件语法,shell脚本语法,发现都没有问题,但是却不能正常删除文件,run.error日志输出以下内容:
/bin/bash: /Users/xxxxx/demo/delete.sh: Operation not permitted
检查delete.sh权限确认是所有用户可执行
ls -l | grep delete.sh
-rwxrwxrwx@ 1 xxxx staff 198 8 17 10:28 delete.sh
google无果,好像没人碰到类似问题,冷静分析!!!
- 报错不发生在delete.sh内部!
- delete.sh脚本是由哪个对象去调起的? 假设这个对象叫plist;
- 是不是plist这个对象没有权限执行delete.sh文件,但是delete.sh文件是所有用户可执行,应该不存在权限问题,那是不是在去执行delete.sh的过程中出现的问题;
- 执行delete.sh用的是/bin/bash,问题出在plist对象调用/bin/bash这个过程?
带着这个猜想去google了下,很容易就发现其他场景下的类似问题,
确认原因为: /bin/bash没有权限去调用delete.sh
解决方案
赋予/bin/bash完全磁盘访问权限
系统设置-隐私与安全性-完全磁盘访问权限-添加/bin/bash
附相关常用命令
- 加载 / 卸载: 卸载加载是启动的前提, 只有加载了之后才能执行任务
launchctl bootstrap gui/501 /Library/LaunchAgents/com.demo.test.plist
: 加载 指定服务launchctl bootout gui/501 /Library/LaunchAgents/com.demo.test.plist
: 卸载 指定服务launchctl load <path_of_plist>
: 加载 一个 plist 文件, 只会加载没有被 disable 的任务, 添加-w
会 enable 状态并加载, 这导致下次启动也会加载该任务launchctl unload <path_of_plist>
: 停止并 卸载 一个 plist 任务, 添加-w
会 disable 状态, 这导致下次启动也不会加载该任务launchctl unload <path_of_plist> && launchctl load <path_of_plist>
: 修改配置后重载配置, 如果任务被修改了, 那么必须先 unload, 再重新 loadlaunchctl remove <label>
: 通过服务名进行 卸载
- 启动 / 停止: 启动与停止只会影响当次执行的任务, 不会影响下次的计时任务执行
launchctl start <label>
: 在不修改 Disabled 状态的前提下根据service_name
值 启动 一个已加载的 service(效果为立即执行, 无论时间是否符合条件)launchctl stop <label>
: 在不修改 Disabled 状态的前提下根据service_name
值 停止 一个正在执行中的任务(不会影响其下次的定时启动功能, 只会取消当前执行的当次行为)
- 启用 / 禁用: 表示该服务下次启动后会不会被加载, 不会影响当前已加载的服务
launchctl enable gui/501/com.hanleylee.test_timer
: 启用服务, 启用之后再次启动系统会加载该服务launchctl disable gui/501/com.hanleylee.test_timer
: 禁用服务, 禁用之后再次启动系统也不会加载该服务了
- 杀掉服务
launchctl kill gui/501/demo.test
launchctl kickstart -k <path_of_plist>
: 杀死进程并重启服务, 对一些停止响应的服务有效
- Other
launchctl print gui/501
launchctl print-disabled gui/501
launchctl list
: 列出已加载的所有服务launchctl list | grep 'com.hello'
: 筛选任务列表
相关注意事项:
- 一个服务, 必须在被加载后才能使用 start 进行启动, 如果使用了
RunAtLoad
或KeepAlive
则在加载时就启动. - 在执行 start 和 unload 前, 任务必须先 load 过, 否则报错
参考链接: https://hanleylee.com/articles/manage-process-and-timed-task-by-launchd/