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
2
3
4
5
6
7
8
9
10
11
12
<?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.app</string>
<key>Program</key>
<string>/Users/Me/Scripts/cleanup.sh</string>
<key>RunAtLoad</key>
<true/>
</dict>
</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
2
<key>Program</key>
<string>/path/to/program</string>
1
2
3
4
5
6
7
8
<key>ProgramArguments</key>
<array>
<string>/usr/bin/rsync</string>
<string>--archive</string>
<string>--compress-level=9</string>
<string>/Volumes/Macintosh HD</string>
<string>/Volumes/Backup</string>
</array>

环境变量

1
2
3
4
5
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/bin:/usr/bin:/usr/local/bin</string>
</dict>

输入输出

1
2
3
4
5
6
<key>StandardInPath</key>
<string>/tmp/test.stdin</string>
<key>StandardOutPath</key>
<string>/tmp/test.stdout</string>
<key>StandardErrorPath</key>
<string>/tmp/test.stderr</string>

工作目录

1
2
<key>WorkingDirectory</key>
<string>/tmp</string>

限制资源

SoftResourceLimits HardResourceLimits

1
2
3
4
5
6
7
8
9
10
<key>HardResourceLimits</key>
<dict>
<key>FileSize</key>
<integer>1048576</integer>
</dict>
<key>SoftResourceLimits</key>
<dict>
<key>FileSize</key>
<integer>524288</integer>
</dict>
Key Description
CPU
FileSize
NumberOfFiles
Core
Data
MemoryLock
NumberOfProcesses
ResidentSetSize
Stack

When

即刻启动 RunAtLoad

1
2
<key>RunAtLoad</key>
<true/>

周期启动

StartInterval

1
2
<key>StartInterval</key>
<integer>3600</integer>

日历周期启动

StartCalendarInterval

1
2
3
4
5
6
7
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>

可以多个日历周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Minute</key>
<integer>0</integer>
<key>Weekday</key>
<integer>0</integer>
</dict>
</array>

可以用cron来定义

当设备被mount的时候启动

StartOnMount

不过好像不能指定device

1
2
<key>StartOnMount</key>
<true/>

当路径改变的时候

WatchPath

1
2
3
4
<key>WatchPaths</key>
<array>
<string>/path/to/directory_or_file</string>
</array>

如果目标是文件的话, 如果新建,删除,写入都会触发。
如果是目录,则是目录以及目录内的文件新建删除写入都会触发。

也可以用 QueueDirectories
当任意一个目录不为空的时候则会触发,任务需要自己负责清楚处理过的文件。
很适合来跑一些batch processing的任务

KeepAlive

1
2
<key>KeepAlive</key>
<true/>

也可以加上条件

1
2
3
4
5
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<true/>
</dict>
1
2
3
4
5
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
</dict
1
2
3
4
5
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
<true/>
</dict>
1
2
3
4
5
6
7
8
<key>KeepAlive</key>
<dict>
<key>PathState</key>
<dict>
<key>/tmp/runJob</key>
<true/>
</dict>
</dict>

或者依赖另外一个任务

1
2
3
4
5
6
7
8
<key>KeepAlive</key>
<dict>
<key>OtherJobEnabled</key>
<dict>
<key>local.otherJob</key>
<false/>
</dict>
</dict>
1
2
3
4
5
6
7
8
<key>KeepAlive</key>
<dict>
<key>AfterInitialDemand</key>
<dict>
<key>local.otherJob</key>
<true/>
</dict>
</dict>

权限和安全

UserName, GroupName, InitGroups

1
2
3
4
5
6
<key>UserName</key>
<string>nobody</string>
<key>GroupName</key>
<string>nobody</string>
<key>InitGroups</key>
<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一起用

如何使用

启用一个服务

1
launchctl load -w ~/Library/LaunchAgents/com.example.app.plist
1
2
launchctl start com.example.app
launchctl stop com.example.app

一个完整的例子

Jupyter Lab

I use Jupyter Lab as my personal notebook.

Here is a plist file

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>Disabled</key>
<false/>
<key>KeepAlive</key>
<false/>
<key>Label</key>
<string>local.jupyter.lab</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jupyter-lab</string>
<string>/Users/x/Github/MyNotebook</string>
<string>--no-browser</string>
</array>
<key>StandardErrorPath</key>
<string>/Users/x/Library/LaunchAgents/jupyter-lab.stderr</string>
<key>StandardOutPath</key>
<string>/Users/x/Library/LaunchAgents/jupyter-lab.stdout</string>
<key>WorkingDirectory</key>
<string>/Users/x/Github/MyNotebook</string>
</dict>
</plist>

References

  1. https://www.launchd.info/