现在各大 Linux 发行版很多都变成 systemd 的形状了,连 Synology DSM 7 都开始用 systemd 了,可以说是 Learn once, operate anywhere。不过有时候还是要在 Mac 机器上部署一些服务,需要用到 launchd/launchctl。

本文以一个熟悉 systemd/systemctl 工具的运维视角,整理了常用的 launchd 相关命令备忘。(说实话我每次都会忘记怎么用,然后得去翻手册……而且网上有时候还会教你用 load/unload 子命令,这些其实都已经标记为过时了,有新的命令替代它们)

list 查看所有

launchctl list

# 类比 systemctl list-units

status 查看状态

launchctl print gui/501/uploadserver
# -> ~/Library/LaunchAgents/uploadserver.plist

launchctl print system/com.openssh.sshd
# -> /System/Library/LaunchDaemons/ssh.plist

# 类比 systemctl status uploadserver.service
# -> /etc/systemd/system/uploadserver.service

这里的 system/ gui/501/ 叫做 domain-target,com.openssh.sshduploadserver 叫做 service-name,合起来之后叫做 service-target。service-name 一般就是下面 .plist 配置文件中的 Label。

其中 system/ 顾名思义就是系统级的,system domain,需要 root 权限才能修改。而 gui/501/ user/501/ 则是用户级的 user domain,其中 501 就是用户 uid。gui/<uid>/login/<asid>/ 下的服务只有用户登录了才会运行。

通常来说需要 GUI 运行的放在 gui/<uid>/ 下面,不需要的放在其他下面。不过考虑到 macOS 作为桌面操作系统的特性,其实一股脑都放 gui 下面也没啥问题。

start 启动

launchctl bootstrap gui/501 ~/Library/LaunchAgents/uploadserver.plist

# 或者系统级的服务,需要 sudo 运行
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.plist

# 类比 systemctl start uploadserver.service
# 不需要 systemctl daemon-reload

如果 bootstrap 提示下面的报错,就说明很可能是服务器已经在运行了,或者服务被 disable 掉了:

Bootstrap failed: 5: Input/output error
Try re-running the command as root for richer errors.

stop 停止

launchctl bootout gui/501 ~/Library/LaunchAgents/uploadserver.plist

# 或者系统级的服务,需要 sudo 运行
sudo bootout bootstrap system /Library/LaunchDaemons/com.example.plist

# 除了 .plist 文件路径,也可以用 label 指定
launchctl bootout gui/501/uploadserver

# 类比 systemctl stop uploadserver.service

如果 bootout 提示下面的报错,就说明很可能是服务本来就没有在运行:

Boot-out failed: 3: No such process

restart 重启

launchctl kickstart -k -p gui/501/uploadserver
# service spawned with pid: 17247

# 参数:
# -k       如果服务已经在运行了,先杀死现有的进程再重启
# -p       成功后输出进程的 PID

# 类比 systemctl restart uploadserver.service

如果 kickstart 提示下面的报错,就说明服务没有 bootstrap:

Could not find service "uploadserver" in domain for user gui: 501

enable 开机启动

一般来说你的 .plist 里配置了 RunAtLoad 的话,bootstrap 了之后默认就是开机启动的,不需要再 enable 一遍。

不过你也可以通过 disable 主动关闭服务的开机启动,这个是否 enable 的状态是 launchd 自己维护的,不会更改 .plist 文件的内容。使用 disable 禁止开机启动之后,想要恢复开机启动就需要用 enable。

launchctl enable gui/501/uploadserver

# 类比 systemctl enable uploadserver.service

disable 禁止开机启动

launchctl disable gui/501/uploadserver

# 类比 systemctl disable uploadserver.service

config 配置文件

类似于 systemd 的 Unit File,以 .plist/XML 文件的形式存在:

~/Library/LaunchAgents   Per-user agents provided by the user.
/Library/LaunchAgents    Per-user agents provided by the administrator.
/Library/LaunchDaemons   System-wide daemons provided by the administrator.
/System/Library/LaunchAgents   Per-user agents provided by Apple.
/System/Library/LaunchDaemons  System-wide daemons provided by Apple.

通常来说我们只会用到前三个。Daemon 和 Agent 的区别:

  • Daemon:系统级、单个实例服务多个用户、不应显示 UI、没有用户登录也会运行、可以以 root 权限运行。
  • Agent:每个用户各自运行实例、直接与用户交互、用户不登录则不会运行、以普通用户权限运行。

这里以 ~/Library/LaunchAgents/uploadserver.plist 为例:

<?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>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>uploadserver</string>
    <key>LimitLoadToSessionType</key>
    <array>
        <string>Aqua</string>
        <string>Background</string>
        <string>LoginWindow</string>
        <string>StandardIO</string>
        <string>System</string>
    </array>
    <key>ProcessType</key>
    <string>Background</string>
    <key>ProgramArguments</key>
    <array>
        <string>/opt/homebrew/bin/uvx</string>
        <string>uploadserver</string>
        <string>8888</string>
        <string>-d</string>
        <string>/tmp/upload</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>EnvironmentVariables</key>
    <dict>
        <key>FOO</key>
        <string>BAR</string>
    </dict>
    <key>StandardErrorPath</key>
    <string>/tmp/uploadserver.log</string>
    <key>StandardOutPath</key>
    <string>/tmp/uploadserver.log</string>
    <key>TimeOut</key>
    <integer>5</integer>
</dict>
</plist>

类比 /etc/systemd/system/uploadserver.service

[Unit]
Description=Upload Server
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/root/.local/bin/uvx uploadserver 8888 -d /tmp/upload
Restart=always
RestartSec=5
Environment="FOO=BAR"
StandardOutput=file:/tmp/uploadserver.log
StandardError=file:/tmp/uploadserver.log

[Install]
WantedBy=multi-user.target

具体怎么写就不多说了,大部分开机自启 + 守护进程需求用上面的模板就够了,有其他需求的直接看文档。

名字基本可以随便取。另外注意有多个命令行参数的话,要拆成多个 <string>

不再推荐使用的 Legacy 命令

现在很多教程里还在用 load/unload,不过在 macOS 10.11 之后,官方手册中已经不再推荐使用这些老的命令,应该使用上面的命令替代。

另外一点需要注意的是,load/unload 通过执行 launchctl 命令时的权限来决定 domain。如果使用 sudo launchctl 执行的,算作 system domain,否则算作 user domain。之前被坑过一次……💩

launchctl load ~/Library/LaunchAgents/uploadserver.plist
# 应该替换为 launchctl bootstrap gui/501 ~/Library/LaunchAgents/uploadserver.plist

launchctl load -w ~/Library/LaunchAgents/uploadserver.plist
# 相当于先 enable 再 bootstrap

launchctl unload ~/Library/LaunchAgents/uploadserver.plist
# 应该替换为 launchctl bootout gui/501 ~/Library/LaunchAgents/uploadserver.plist

sudo launchctl load /Library/LaunchDaemons/com.example.plist
# 应该替换为 sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.plist

sudo launchctl unload /Library/LaunchDaemons/com.example.plist
# 应该替换为 sudo bootout bootstrap system /Library/LaunchDaemons/com.example.plist

可视化管理工具

推荐 Lingon X,有点像以前 Windows 上的那种开机进程管理,可以查看当前系统上有哪些 Agent/Deamon,以及它们的配置,挺方便的。

小尾巴

我记得以前 macOS 设置页面里的启动项管理很垃圾,只能显示部分启动项,但实际上程序想要开机自启有很多种方法:

  • LoginItems (Helper App with SMLoginItemSetEnabled)
  • LaunchAgents (~/Library/LaunchAgents or /Library/LaunchAgents)
  • LaunchDaemons (/Library/LaunchDaemons)
  • 直接拖动到设置页面「登录时打开」列表中

搞得有些流氓软件开机自启了你都不知道,比之 Windows 还不如。当时我还准备写篇博客,在不安装第三方清理软件的情况下如何删除这些启动项,但后来不了了之了。

不过好在后来的 macOS 更新把这块的管理补上了,现在在设置页面就可以直接管理上面那些方式添加的启动项,挺好。