一、使用场景

使用 Mac 的时候常常遇到这种情况,辛辛苦苦写了个脚本,可以在后台自动处理某些任务,但要一直挂在前台,或者使用 nohup 在后台运行,然后一重启电脑,又得在来一遍,很麻烦。能不能开机也能让脚本自动运行呢?

自然可以的,如果你不嫌有个控制台挂在任务栏可以选择用户登录启动,这个方法就是配置简单,只需要在设置->登录项里添加一下即可。

但,有更好的方法,那就是通过 MacOS 的 LaunchDaemon 启动,下面会重点讲这个的知识。

二、基本用法

LaunchDaemon 是什么 ?

官方文档:”Wikipedia defines launchd as "a unified, open-source service management framework for starting, stopping and managing daemons, applications, processes, and scripts. Written and designed by Dave Zarzycki at Apple, it was introduced with Mac OS X Tiger and is licensed under the Apache License."

可以理解为,launchd 是一套统一的开源服务管理框架,它用于启动、停止以及管理后台程序、应用程序、进程和脚本。Launchd 是 macOS 第一个启动的进程,该进程的 PID 为 1,整个系统的其他进程都是它创建的。当 launchd 启动后,它会扫描/System/Library/LaunchDaemons 和/Library/LaunchDaemons 中的 plist 文件并加载他们;当输入密码登录系统后,launchd 会扫描/System/Library/LaunchdAgents、/Library/LaunchAgents、~/Library/LaunchAgents 这三个目录中的 plist 文件并加载它们。每个 plist 文件都是一个任务,加载不代表立即运行,只有设置了 RunAtLoad 为 true 或 keepAlive 为 true 时,才会加载并同时启动这些任务。

Daemons 和 Agents

守护进程,英⽂叫 Daemon,守护进程其实就是在后台运⾏的程序,它没有界⾯,你看不到它,⼀般使⽤命令来对它进程管理控制,守护进程常被设置为开机⾃动启动 (当然也可以开机后⼿动⽤命令启动),很多软件的“服务器端”⼀般都是以守护进程的⽅式运⾏,⽐如数据库、内存缓存、Web 服务器等等都是以守护进程⽅式运⾏的,它们通过“接⼝”对外提供服务 (如 Unix socket / tcp ⽅式等等)。

Daemons 和 Agents 都是 launchd 所管理的后台程序,它们的区别是 Agent 是属于当前登录⽤户 (就是你开机后输⼊密码时的那个⽤户名),它们是以当前登录的⽤户权限启动的,⽽Daemon 则属于 root⽤户,但由于有 root⽤户权限,所以它可以指定以什么⽤户运⾏,也可以不指定(不指定就是以 root⽤户运⾏)。

Launchd 如何管理后台进程

Launchd 是通过以“. plist”后缀结尾的 xml⽂件来定义⼀个程序的开机⾃启动的,我们⼀般称它为 plist⽂件。

具体操作步骤

1 . 新建 plist⽂件文件

使用你喜欢的文本编辑器创建一个新的 plist 文件(如 com.Yourcompany.Startupscript.Plist),放置在/Library/LaunchDaemons/目录下或~/Library/LaunchAgents 目录下。这个文件需要具备适当的权限,确保 root 用户可以读取和执行。

如: `/Library/LaunchDaemons/com.Yourcompany.Startupscript.Plist·

2 . 编辑 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>Label</key>
    <string>com.yourcompany.startupscript</string>
    <key>Program</key>
    <string>/path/to/your/script.sh</string>
    <key>ProgramArguments</key>
    <array>
        <string>/path/to/your/script.sh</string>
        <string>param1</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/> <!-- 根据你的需求决定是否保持长期运行 -->
    <key>StandardErrorPath</key>
    <string>/var/log/my_script_error.log</string>
    <key>StandardOutPath</key>
    <string>/var/log/my_script_output.log</string>
</dict>
</plist>

确保将/path/to/your/script.Sh替换为你的 shell 脚本的实际路径。

保存并关闭编辑器。

3. 设置文件权限

给这个 plist 文件设置适当的权限,让它可以被系统读取和执行:

sudo chmod 644 /Library/LaunchDaemons/com.yourcompany.startupscript.plist
sudo chown root:wheel /Library/LaunchDaemons/com.yourcompany.startupscript.plist

4 . 加载并启动这个 LaunchDaemon

sudo launchctl load /Library/LaunchDaemons/com.yourcompany.startupscript.plist

确认脚本已经在后台运行且不会显示窗口。日志输出会被写入到你指定的日志文件中。以上步骤创建的 LaunchDaemon 将在下次系统启动时自动运行你的 shell 脚本,并且脚本的输出将被重定向到指定的日志文件,从而达到后台静默运行的目的。

如果需要立即启动脚本,可以使用 sudo launchctl start com.yourcompany.startupscript 命令。

三、参数解释

Label:标识 LaunchAgent 或 LaunchDaemon 的唯一名称,必须是唯一的,并且遵循命名规范。
Label 为需要启动的进程设置一个标签,这里起名为 com.yourcompany.startupscript

Program 设置为需要启动的可执行程序的路径

ProgramArguments 设置为程序运行需要的参数,第一个参数即为 main 函数中的 argv[0]

RunAtLoad:如果设置为,意味着当 launchd 加载这个配置文件时(对于 LaunchDaemons 来说就是在系统启动时,对于 LaunchAgents 来说就是在用户登录时),会立刻运行指定的程序。

一般用户代理进程,我们将. Plist 文件放到~/Library/LaunchAgents. 文件中,对于守护进程,我们将其放到/Library/LaunchDaemons 文件中

KeepAlive:决定程序是否应持续运行。如果设置为,即使程序退出,launchd 也会尝试重新启动它;如果设置为,程序运行结束后就不会再被重启。

StandardErrorPath 和 StandardOutPath:这两个参数用于指定程序的标准错误输出和标准输出重定向到哪个文件,有助于日志记录和调试。

四、加载、卸载、启动、停止、查看 launchd 列表

列出所有由 launchd 管理的进程

launchctl list
``
查看某个具体的进程

aunchctl list | grep com.example.app

创建完.plist文件后,需要加载该文件,

launchctl load ~/Library/LaunchAgents/local.launchd_test.app.plist

launchd启动代理进程

launchctl start local.launchd_test.app

此时,可以通过 launchctl list 查看到 launchd_test 的进程号

launchctl list | grep local.launchd_test.app

卸载、关闭命令

launchctl unload ~/Library/LaunchAgents/local.launchd_test.app.plist
launchctl stop local.launchd_test.app

四、常见问题及注意事项

1 . WatchPaths 参数是什么

WatchPaths 参数是用来监视一组指定的文件系统路径的。当这些路径下的任何文件或子目录发生变化(如文件被修改、创建或删除)时,关联的进程将会被启动或唤醒执行,,这里的关联的进程就是上面你配置的进程或脚本。

<key>WatchPaths</key>
<array>
    <string>/path/to/watch/for/changes</string>
    <!-- 可以添加多个路径 -->
    <!-- <string>/another/path/to/watch</string> -->
</array>

每当 watched paths 下的文件发生变动时,launchd 会触发相关的 StartInterval、StartCalendarInterval、StartOnMount 等触发机制,来启动或重启与之关联的程序。

StartInterval: 这个键值设定的是间隔多少秒后重复执行任务。例如:

<key>StartInterval</key>
<integer>3600</integer>

StartCalendarInterval: 这个键值允许你基于日历时间来安排任务。它可以设定每天、每周或每月的特定时刻执行任务。例如:

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>0</integer>
    <key>Minute</key>
    <integer>0</integer>
</dict>

StartOnMount: 当指定的卷被挂载时,启动任务。这个特性通常与卷路径一起使用。例如监控外接硬盘连接时执行任务:

<key>StartOnMount</key>
<true/>
<key>WatchPaths</key>
<array>
    <string>/Volumes/ExternalDrive</string>
</array>

这些参数的顺序在 XML 配置文件中一般没有严格的要求,因为它们都是键值对的形式出现,关键是键名和对应的值要正确配置。不过建议按照一定的逻辑顺序组织配置文件,使得代码易于理解和维护。

2 . 关于 plist 文件的权限

对于 LaunchDaemons,配置文件通常需要属于 root: wheel,并具有 644 的权限。

对于 LaunchAgents,配置文件应属于相应用户和组,并具有读写的权限。

3 . 启动服务、卸载和服务状态

创建完.plist 文件后,需要使用 launchctl 命令加载配置文件,例如:

对于 LaunchDaemons,使用:do launchctl load /Library/LaunchDaemons/<your_plist_file>.plist

对于 LaunchAgents,切换到相应用户后,使用:launchctl load ~/Library/LaunchAgents/<your_plist_file>.plist

如果不再需要服务运行,可以使用 unload 命令移除配置,例如:

sudo launchctl unload /Library/LaunchDaemons/<your_plist_file>.plist

对于 LaunchAgents,使用:launchctl unload ~/Library/LaunchAgents/<your_plist_file>.plist

看服务状态: 你可以使用 launchctl list 命令来查看服务是否已经被加载并正在运行。

4 . Root: wheel 这个 wheel 是什么

Root: wheel 是在类 Unix 操作系统(包括 macOS)中常见的用户和组的概念。在 Unix/Linux 系统中,每个文件、目录和进程都有所属的用户和组。

Root:这是 Unix 和类 Unix 系统中的超级用户(或系统管理员),具有最高权限,可以执行任何系统操作。

Wheel:在某些 Unix 变体中,wheel 是一个传统的特殊组,最初设计用来控制谁可以使用 su 命令切换到 root 用户。在 macOS 中,wheel 组仍然保留,且默认情况下,root 用户属于 wheel 组。在过去的某些系统中,只有 wheel 组的成员才能切换到 root 用户,这是一种安全性措施。但现在大多数现代系统(包括 macOS)的安全模型已经更加复杂,并不强制依赖 wheel 组来进行 root 权限的管理。

因此,在 Unix/Linux 系统的文件权限、用户和组管理中,root: wheel 通常指的是文件或目录的所有者为 root 用户,所属组为 wheel 组。在讨论 LaunchDaemons 时,也常常看到 root: wheel,因为这些系统级的服务通常需要以 root 权限运行,并且归根结底由系统管理,所以它们的配置文件通常属于 root 用户和 wheel 组。

5 . Plist 的文件里可以用注释吗

plist(Property List)文件本质上是 XML 格式的文件,因此它支持 XML 风格的注释。在 XML 中,注释使用包裹

6 . 可能的错误

执行命令 sudo launchctl load /Users/your name/Library/LaunchAgents/com.mdwatch.plist 报错怎么回事?

错误信息:
Warning: Expecting a LaunchDaemons path since the command was ran as root. Got LaunchAgents instead. launchctl bootstrap is a recommended alternative. Load failed: 5: Input/output error Try running launchctl bootstrap as root for richer errors.

当您以 root 用户身份运行 sudo launchctl load 命令加载一个 LaunchAgents 的配置文件时,系统会给出警告,因为 LaunchAgents 通常是为特定用户设计的,而非 root 用户。当 root 用户尝试加载 LaunchAgents 的配置文件时,可能会导致错误或不可预见的行为。

因此用户代理下应使用 launchctl load 去掉 sudo 即可。