前言

  1. iCloud 貌似只能同步文稿和桌面的内容,而我 SSH 连接的 config 和密钥都存在 ~/.ssh 下,故写了个备份脚本,把 ~/.ssh 下的内容复制到 ~/Documents/backupssh

    1
    2
    3
    4
    5
    # 前往根目录
    cd ~

    # 创建并编辑脚本
    nano backupssh.sh

    脚本内容如下

    1
    2
    #!/usr/bin/env zsh
    rsync -av --update --delete --checksum ~/.ssh/ ~/Documents/backupssh/ssh

    control + X 退出,输入 y 回车保存

  2. 使用 launchd 方式配置启动任务及定时任务

    Ref: https://hanleylee.com/articles/manage-process-and-timed-task-by-launchd/

什么是 launchd

launchd 是 MacOS 用来管理系统和用户级别的守护进程的工具. 该工具由两部分组成:

  • launchd,该工具主要有两个功能:
    • 启动系统
    • 加载和维护服务
  • launchctl: 是 launchd 提供的用于对用户交互的工具

对于 launchd 来说,每一个 plist 文件即为一个任务。系统启动时,launchd 会加载 /System/Library/LaunchDaemons/Library/LaunchDaemons 中的所有 plist 文件,用户登录后,会扫描 /System/Library/LaunchAgents/Library/LaunchAgents~/Library/LaunchAgents 这三个目录的文件并加载它们

plist 放在不同位置时的区别

对于 launchd 来说,有如下五个路径的 plist 文件会被读取加载,他们被触发的时机并不相同,总结如下:

位置 类型 以什么用户权限运行 运行时机 Provided
/System/Library/LaunchDaemons System Daemons root / 指定用户 开机时 Apple
/System/Library/LaunchAgents System Agents 当前登录用户 任意用户登录 Apple
/Library/LaunchDaemons Global Daemons root / 指定用户 开机时 Administrator
/Library/LaunchAgents Global Agents 当前登录用户 任意用户登录 Administrator
~/Library/LaunchAgents User Agents 当前登录用户 指定用户登录时 User

总的来说,LaunchDaemonsLaunchAgents 主要有以下两个区别:

  1. 运行时机
    • LaunchDaemons 在按下开机按钮后,用户还未输入密码时,就已经运行了
    • LaunchAgents 在用户输入密码后,才开始运行
  2. 运行用户
    • LaunchDaemons 是以 root / 其他指定用户运行
    • LaunchAgents 是以当前登录用户的权限运行

实操

  1. 一般是我们的备份脚本肯定是要在还未输入密码时就运行了,那就直接来到 /Library/LaunchDaemons,写个每天 23:00 执行的定时任务

    1
    2
    3
    4
    5
    # 前往文件夹
    cd /Library/LaunchDaemons

    # 编辑文件
    sudo nano com.lan.backupssh.plist

    plist 内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?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.lan.backupssh</string>
    <key>ProgramArguments</key>
    <array>
    <string>/Users/lan/backupssh.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
    <key>Minute</key>
    <integer>00</integer>
    <key>Hour</key>
    <integer>23</integer>
    </dict>
    <key>KeepAlive</key>
    <false/>
    <key>StandardOutPath</key>
    <string>/Users/lan/Documents/backupssh/ssh/logs/backupssh.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/lan/Documents/backupssh/ssh/logs/backupssh-err.err</string>
    </dict>
    </plist>

    control + X 退出,输入 y 回车保存

  2. 加载服务:sudo launchctl bootstrap gui/501 /Library/LaunchDaemons/com.lan.backupssh.plist

  3. 启动服务:sudo launchctl start com.lan.backupssh

  4. 检查是否成功启用:launchctl list | grep 'com.lan'image-20240412045036575

踩坑

  • 重启之后不知道为什么没有自动启用服务image-20240412050129905

  • 排除了半天问题也没瞧出个什么名堂,索性还是丢 /Library/LaunchAgents

    1
    2
    3
    4
    5
    # 复制文件
    sudo cp /Library/LaunchDaemons/com.lan.backupssh.plist /Library/LaunchAgents

    # 删除源文件
    sudo rm -rf /Library/LaunchDaemons/com.lan.backupssh.plist
    1. 加载服务:launchctl load -w /Library/LaunchAgents/com.lan.backupssh.plist
    2. 启动服务:sudo launchctl start com.lan.backupssh
    3. 检查是否成功启用:launchctl list | grep 'com.lan'image-20240412045036575
    4. 重启之后检查是否启用成功:launchctl list | grep 'com.lan'image-20240412045036575

后话

KEYS

上述实例中, 我们使用到了 LABELProgramArguments 之类的 KEYS,常用的 KEYS 如下:

  • Label:标签的值,不能与其他 plist 文件中的 Label 标签中的值完全重复
  • ProgramArguments: 标签中放入 shell 所在的路径,用于控制在指定的时间执行 shell
  • RunAtLoad:表示加载定时任务即开始执行脚本
  • KeepAlive:是否设置程序是一直存活着,如果退出就重启
  • Disabled:指定默认情况下该服务是否应该被加载,如果用户使用 launchctl disable / launchctl enable 设置了服务状态,那么此值会被忽略
  • StartInterval:定时任务的周期,单位为秒
  • StartCalendarInterval:指定命令在多少分钟、小时、 天、 星期几,、月时间上执行
  • StandardOutPath:填写脚本运行日志输出的路径
  • StandardErrorPath:填写脚本运行错误日志输出的路径
  • WorkingDirectory:设置工作目录
  • UserName:只有指定用户下才会执行(只在 Daemons 可用)
  • GroupName:只有指定用户组下才会执行(只在 Daemons 可用)

全部 KEYS 可使用 man 5 launchd.plist 查看;使用 plutil -lint xxx.plist 可以验证 plist 文件的正确性

常用 launchctl 命令

launchctllaunchd 提供的用于对用户交互的工具,我们可以方便的 启动/停止/启用/禁用 相关服务。

使用 man launchctl 可以查看到具体用法,其中使用了大量的 specifier 作为命令一部分,主要可以分为三种:

  1. service-name:服务名,如 com.apple.example
  2. domain-target:域名,如 gui/501,其中 501 可以通过 launchctl manageruid 获取
  3. service-target:指定域名下的服务名,是服务名与域名的结合,如 gui/501/com.apple.example

我将实际经常会用到的使用命令列出如下:

  • 加载 / 卸载:卸载加载是启动的前提,只有加载了之后才能执行任务
    • launchctl bootstrap gui/501 ~/Library/LaunchAgents/com.hanleylee.test_timer.plist加载 指定服务
    • launchctl bootout gui/501 ~/Library/LaunchAgents/com.hanleylee.test_timer.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,再重新 load
    • launchctl 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/com.hanleylee.xuexiqiangguo
    • launchctl kickstart -k <path_of_plist>:杀死进程并重启服务,对一些停止响应的服务有效
  • Other
    • launchctl print gui/501
    • launchctl print-disabled gui/501
    • launchctl list:列出已加载的所有服务
    • launchctl list | grep 'com.hello':筛选任务列表

相关注意事项:

  • 一个服务,必须在被加载后才能使用 start 进行启动,如果使用了 RunAtLoadKeepAlive 则在加载时就启动
  • 在执行 start 和 unload 前,任务必须先 load 过,否则报错

GUI 工具

如果对每次都要敲以上命令感到恐惧的话,可以使用一款好用的 GUI 软件:https://www.soma-zone.com/LaunchControl/

img

这款软件基本涵盖了以上全部命令操作,可视化的方式让你更方便一览当前所有定时任务的状态