Launchd,如何在Mac上运行服务
在Linux中,通常我们会利用service/systemctl和systemd对话来运行服务 在MacOS中, 与之对应的是launchctl和Launchd。这是苹果自己弄的一套服务管理框架,最早出现在MacOS X Tiger中。它利用的是 plist(property list)XML.
那么,如何在mac上定义一个服务呢?
简介
Launchd 定义的服务有两种,一种叫做Daemons,一种叫做Agents。 两者之间的区别并不大,主要是Agents一定是当前登陆的用户的角色来运行的,而Daemon则可以是root用户,或者任意指定的用户。 也就是说,Agents,是只给当前用户跑的服务,Daemons更像是系统服务。
一个服务通常定义如下
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4 <dict>
5 <key>Label</key>
6 <string>com.example.app</string>
7 <key>Program</key>
8 <string>/Users/Me/Scripts/cleanup.sh</string>
9 <key>RunAtLoad</key>
10 <true/>
11 </dict>
12</plist>
服务的定义是利用plist。至于这个服务是Daemon还是Agent,其实是取决于定义的位置。。。
这里是来自[1]中的一个表格,让你一眼弄清楚。
Type | Location | Run on behalf of |
---|---|---|
User Agents | ~/Library/LaunchAgents | Currently logged in user |
Global Agents | /Library/LaunchAgents | Currently logged in user |
Global Daemons | /Library/LaunchDaemons | root or the user specified with the key UserName |
System Agents | /System/Library/LaunchAgents | Currently logged in user |
System Daemons | /System/Library/LaunchDaemons | root or the user specified with the key UserName |
常用的键值
这里来简单介绍一些常用的键值
Key | Requred | Description |
---|---|---|
Label | Yes | 用的是 reverse domain notation,必须是唯一的,虽然Agent和Daemon可以重用,但是还是推荐全局唯一。对于私人的可以用local开头 |
Program | Yes, if no ProgramArguments | 提供到executable的路径 |
ProgramArguments | Yes, if no Program | 如果你的executable需要提供命令行参数 |
What & How
主要是靠 Program or ProgramArguments
1<key>Program</key>
2<string>/path/to/program</string>
1<key>ProgramArguments</key>
2<array>
3 <string>/usr/bin/rsync</string>
4 <string>--archive</string>
5 <string>--compress-level=9</string>
6 <string>/Volumes/Macintosh HD</string>
7 <string>/Volumes/Backup</string>
8</array>
环境变量
1<key>EnvironmentVariables</key>
2<dict>
3 <key>PATH</key>
4 <string>/bin:/usr/bin:/usr/local/bin</string>
5</dict>
输入输出
1<key>StandardInPath</key>
2<string>/tmp/test.stdin</string>
3<key>StandardOutPath</key>
4<string>/tmp/test.stdout</string>
5<key>StandardErrorPath</key>
6<string>/tmp/test.stderr</string>
工作目录
1<key>WorkingDirectory</key>
2<string>/tmp</string>
限制资源
SoftResourceLimits HardResourceLimits
1<key>HardResourceLimits</key>
2<dict>
3 <key>FileSize</key>
4 <integer>1048576</integer>
5</dict>
6<key>SoftResourceLimits</key>
7<dict>
8 <key>FileSize</key>
9 <integer>524288</integer>
10</dict>
Key | Description |
---|---|
CPU | |
FileSize | |
NumberOfFiles | |
Core | |
Data | |
MemoryLock | |
NumberOfProcesses | |
ResidentSetSize | |
Stack |
When
即刻启动 RunAtLoad
1<key>RunAtLoad</key>
2<true/>
周期启动
StartInterval
<key>StartInterval</key>
<integer>3600</integer>
日历周期启动
StartCalendarInterval
1<key>StartCalendarInterval</key>
2<dict>
3 <key>Hour</key>
4 <integer>3</integer>
5 <key>Minute</key>
6 <integer>0</integer>
7</dict>
可以多个日历周期
1<key>StartCalendarInterval</key>
2<array>
3 <dict>
4 <key>Hour</key>
5 <integer>3</integer>
6 <key>Minute</key>
7 <integer>0</integer>
8 </dict>
9 <dict>
10 <key>Minute</key>
11 <integer>0</integer>
12 <key>Weekday</key>
13 <integer>0</integer>
14 </dict>
15</array>
可以用cron来定义
当设备被mount的时候启动
StartOnMount
不过好像不能指定device
1<key>StartOnMount</key>
2<true/>
当路径改变的时候
WatchPath
1<key>WatchPaths</key>
2<array>
3 <string>/path/to/directory_or_file</string>
4</array>
如果目标是文件的话, 如果新建,删除,写入都会触发。 如果是目录,则是目录以及目录内的文件新建删除写入都会触发。
也可以用 QueueDirectories
当任意一个目录不为空的时候则会触发,任务需要自己负责清楚处理过的文件。
很适合来跑一些batch processing的任务
KeepAlive
1<key>KeepAlive</key>
2<true/>
也可以加上条件
1<key>KeepAlive</key>
2<dict>
3 <key>SuccessfulExit</key>
4 <true/>
5</dict>
1<key>KeepAlive</key>
2<dict>
3 <key>Crashed</key>
4 <true/>
5</dict
1<key>KeepAlive</key>
2<dict>
3 <key>NetworkState</key>
4 <true/>
5</dict>
1<key>KeepAlive</key>
2<dict>
3 <key>PathState</key>
4 <dict>
5 <key>/tmp/runJob</key>
6 <true/>
7 </dict>
8</dict>
或者依赖另外一个任务
1<key>KeepAlive</key>
2<dict>
3 <key>OtherJobEnabled</key>
4 <dict>
5 <key>local.otherJob</key>
6 <false/>
7 </dict>
8</dict>
1<key>KeepAlive</key>
2<dict>
3 <key>AfterInitialDemand</key>
4 <dict>
5 <key>local.otherJob</key>
6 <true/>
7 </dict>
8</dict>
权限和安全
UserName, GroupName, InitGroups
1<key>UserName</key>
2<string>nobody</string>
3<key>GroupName</key>
4<string>nobody</string>
5<key>InitGroups</key>
6<true/>
Umask
Digit | Granted permissions |
---|---|
0 | read, write and execute/search |
1 | read and write |
2 | read and execute/search |
3 | read only |
4 | write and execute |
5 | write only |
6 | execute/search only |
7 | none |
编排和linux一样,第一个 user access 第二个是 group access 第三个是 其他
MISC
Key | Description |
---|---|
Timeout | idle time that |
ExitTimeout | 结束的timeout |
Nice | 调度优先级 -20 to 20 |
AbandonProcessGroup | 不发送SIGTERM 给子进程 |
ThrottleInterval | 每次触发之间等待的秒数,同KeepAlive 一起用 |
如何使用
启用一个服务
1launchctl load -w ~/Library/LaunchAgents/com.example.app.plist
1launchctl start com.example.app
2launchctl stop com.example.app
一个完整的例子
Jupyter Lab
I use Jupyter Lab as my personal notebook.
Here is a plist file
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>Disabled</key>
6 <false/>
7 <key>KeepAlive</key>
8 <false/>
9 <key>Label</key>
10 <string>local.jupyter.lab</string>
11 <key>ProgramArguments</key>
12 <array>
13 <string>/usr/local/bin/jupyter-lab</string>
14 <string>/Users/x/Github/MyNotebook</string>
15 <string>--no-browser</string>
16 </array>
17 <key>StandardErrorPath</key>
18 <string>/Users/x/Library/LaunchAgents/jupyter-lab.stderr</string>
19 <key>StandardOutPath</key>
20 <string>/Users/x/Library/LaunchAgents/jupyter-lab.stdout</string>
21 <key>WorkingDirectory</key>
22 <string>/Users/x/Github/MyNotebook</string>
23</dict>
24</plist>