Launchd,如何在Mac上运行服务

6 minute read

在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]中的一个表格,让你一眼弄清楚。

TypeLocationRun on behalf of
User Agents~/Library/LaunchAgentsCurrently logged in user
Global Agents/Library/LaunchAgentsCurrently logged in user
Global Daemons/Library/LaunchDaemonsroot or the user specified with the key UserName
System Agents/System/Library/LaunchAgentsCurrently logged in user
System Daemons/System/Library/LaunchDaemonsroot or the user specified with the key UserName

常用的键值

这里来简单介绍一些常用的键值

KeyRequredDescription
LabelYes用的是 reverse domain notation,必须是唯一的,虽然Agent和Daemon可以重用,但是还是推荐全局唯一。对于私人的可以用local开头
ProgramYes, if no ProgramArguments提供到executable的路径
ProgramArgumentsYes, 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>
KeyDescription
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

DigitGranted permissions
0read, write and execute/search
1read and write
2read and execute/search
3read only
4write and execute
5write only
6execute/search only
7none

编排和linux一样,第一个 user access 第二个是 group access 第三个是 其他

MISC

KeyDescription
Timeoutidle 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>

References

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

wechat-qrcode