本文由 简悦 SimpRead 转码, 原文地址 rderik.com
上周我们讨论了如何在 macOS 应用程序中构建 XPC 服务(.xpc 捆绑程序)。Th......
上周我们讨论了如何在 macOS 应用程序中构建 XPC 服务(the .xpc
bundles)。本周我们将探讨如何提供可供其他应用程序或工具使用的 XPC 服务。
为了让其他进程也能使用 XPC 服务,我们将创建一个启动代理。因此,让我们先来了解一下启动代理是如何工作的。
-
你可以在 this GitHub repository 找到启动代理的代码。
-
你可以在 this GitHub repository 找到连接到 XPC 服务的工具代码。
了解启动代理
Launch Daemon(launchd(8)
)是所有服务管理和进程加载的核心。Launch Daemon 的首要任务是启动系统,它是 PID(进程 ID)为 1 的进程,所有其他进程都由它产生。加载后,它负责管理和启动其他守护进程和代理。
在苹果的生态系统中,守护进程和代理进程是有区别的。守护进程通常是在后台运行的进程,执行特定任务,无需用户交互。它通常在系统启动时以 root 身份运行。代理是为登录用户运行的守护进程,也就是说,只有当用户登录并拥有用户权限时,它才会运行。我们将重点讨论代理。
launchd
使用多个目录来保存守护进程和代理配置。对于代理
/System/Library/LaunchAgents/
系统代理。/Library/LaunchAgents/
用于第三方代理。~/Library/LaunchAgents/
用于用户定义的代理。
配置文件写在 PList
文件中,plist 定义可在 launchd.plist(5)
手册中找到。
让我们从创建一个简单的 Launch Agent 开始,了解代理是如何工作的。
试水的基本启动代理
我们将创建一个代理,检查并记录特定目录中磁盘空间的使用情况。我们的代理将每 10 秒运行一次。
我们将使用一个简单的 bash 脚本来实现这一目标。请在方便的地方创建脚本。我将在我的桌面上创建它,命名为 duer.sh
(因为我们将使用 du(1)
命令),添加以下内容:
#!/bin/bash
WATCH_DIR=~/Desktop/testDir
{ date | tr -d '\n' && printf " --" && du -sh ${WATCH_DIR} ;} >> ~/Desktop/duer.log
确保您的脚本可执行,并确保您的 WACH_DIR
存在:
$ chmod u+x ~/Desktop/duer.sh
$ mkdir -p ~/Desktop/testDir
- 注:如果使用的是 Catalina (macOS 10.15),则可能需要在系统偏好设置中的 "安全和隐私 "的 "全盘访问 "授权应用程序中添加
bash
。要添加
- 打开系统偏好设置
- 转到 "安全和隐私
- 向下滚动到 "全磁盘访问"
- 在 Finder 中打开 bin 目录。你可以在终端使用
open(1)
命令。
$ open /bin
- 将 bash 可执行文件拖到 "系统偏好设置 "窗口。
如果安装 macOS 10.15 后守护进程或代理出现问题,请检查它们是否被允许访问文件系统。
好了,回到我们的启动代理。
在 ~/Library/LaunchAgents/
目录中创建文件 com.rderik.duer.plist
(你可以随意命名,但 DNS 反向命名是惯例),内容如下:
<?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.rderik.duer</string>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>10</integer>
<key>Program</key>
<string>/Users/derik/Desktop/duer.sh</string>
</dict>
</plist>
所需的 Label
应该是我们代理的唯一标识符。我们的脚本将在加载时运行,并每 10 秒运行一次。让我们使用 launchctl(1)
命令加载它:
$ launchctl load ~/Library/LaunchAgents/com.rderik.duer.plist
你可以用下面的命令验证它是否在运行:
$ launchctl list | grep rderik
- 0 com.rderik.duer
我们可以检查桌面上的文件 duer.log
.
$ tail -f ~/Desktop/duer.log
运行该命令将显示代理的输出,并在日志发生变化时显示日志(可以按 Control+C
键停止,但暂时不用)。让我们测试一下在 testDir
目录中添加更多文件时脚本是否能正常工作。你可以在不同的 shell 中使用以下命令在 testDir
目录中创建一个新文件:
$ dd if=/dev/zero of=~/Desktop/testDir/temp.txt bs=10m count=1
"dd"命令将在我们的目录中创建一个 10 兆字节的文件,日志将显示这一差异。
一切都应该运行正常,你应该看到日志显示了目录的新大小。
让我们停止执行 tail
命令,然后从启动守护进程中删除代理。你可以使用以下命令 "注销 "代理:
$ launchctl unload ~/Library/LaunchAgents/com.rderik.duer.plist
这将卸载代理,您可以使用以下命令验证代理是否不再运行:
$ launchctl list | grep rderik
如果它仍在运行,你会看到它。否则,会再次出现提示。
你可以删除 ~/Library/LaunchAgents/
目录中的文件 com.rderik.duer.plist
,这样以后启动电脑时就不会加载它了。你也可以从桌面上删除这些文件,我们不会再使用它们。
守护进程和代理按需运行。这意味着我们将配置加载到 "launchd "后,启动守护进程就会监听已注册守护进程和代理的任何请求,并在需要时启动它们。如果不再需要守护进程和代理,它也会将其关闭。
好了,希望这能让你清楚地了解如何向启动守护进程添加代理。现在让我们创建一个 Swift 命令行应用程序,它将提供一个 XPC 服务,其他应用程序可以连接到该服务。
使用 Swift 创建 XPC 服务
我将尽量不重复我们在上一篇文章 MacOS 应用程序使用 Swift 的 XPC 服务 中介绍的 XPC 背后的所有概念。但是,如果您需要复习 XPC 概念,我建议您查看上一篇文章。
我将快速复习一下,让我们从相同的基础开始。
XPC 概念摘要
XPC 服务是一种进程间通信(IPC)机制。XPC 允许我们依靠 "launchd "来管理所有服务和通信,从而轻松实现进程间通信。应用程序可以在应用程序捆绑目录结构中的 "Contents/XPCServices/"目录下拥有多个 XPC 服务捆绑包。
每个 XPC 服务包都能提供由 launchd
按需生成的服务。这样,我们就能将应用程序模块化,通过将功能分割到 XPC 服务中来减少内存占用。这也让我们的主程序更加健壮。如果 XPC 服务的进程崩溃或被杀死,也不会影响我们的主程序。在需要时,XPC 服务可以通过 launchd
重新生成。还有其他好处。如果想了解更多,请阅读上一篇文章。
创建 XPC 服务捆绑包时,我们会在 Info.plist
中定义服务的名称,这样主程序就能找到它。
但 XPC 服务捆绑包提供的 XPC 服务只能由我们自己的应用程序访问。如果我们想创建一个能被其他进程访问的服务,该怎么办呢?这就是我们现在要做的,创建一个提供 XPC 服务的代理。
创建提供 XPC 服务的启动代理
我们的启动代理将提供一个 XPC 服务,该服务将接收一个字符串,并使用控制转义序列返回一个彩色字符串。如果我们使用的是支持 ANSI/VT100 Escape Sequences 的终端,字符串将显示为彩色。
为了测试 XPC 服务,我们将创建一个命令行工具,用红色和绿色显示用户输入的文本。我们希望能在不了解控制 Escape 序列的情况下使用这项服务。如果您想了解更多有关 Control Escape Sequences 的信息,请访问 此链接。
我们将使用 Swift 软件包管理器,现在就开始吧。创建名为 rdConsoleSequencer
的目录,并使用 SPM 对其进行初始化:
$ mkdir rdConsoleSequencer
$ cd rdConsoleSequencer
$ swift package init --type executable
创建 XPC 服务的一般工作流程与 XPC 服务捆绑包相同。
(1) 我们创建一个监听器,(2) 设置其委托对象。委托对象负责接受和设置新的传入连接。一旦我们的监听器有了委托对象,我们就调用 resume 来指示我们的监听器 (3) 开始 "监听 "连接。
不同之处在于,我们将公开一个可以从其他应用程序访问的马赫服务。我们的 XPC 服务捆绑主程序看起来是这样的
import Foundation
let listener = NSXPCListener.service()
let delegate = ServiceDelegate()
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()
我们通过调用 NSXPCListener.service()
来创建监听器,这将返回 XPC 服务捆绑包的单例监听器。但我们也可以使用初始化器init(machServiceName:)
来创建监听器,它会初始化启动代理或启动守护进程的监听器。因此,我们的 main.swift
将看起来像这样:
import Foundation
let delegate = ServiceDelegate()
let listener = NSXPCListener(machServiceName: "com.rderik.ConsoleSequencerXPC" )
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()
其余部分保持不变。让我们创建服务委托类。在 Sources 目录中创建一个名为 ServiceDelegate.swift
的文件,并添加以下内容:
import Foundation
class ServiceDelegate : NSObject, NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let exportedObject = ConsoleSequencerXPC()
newConnection.exportedInterface = NSXPCInterface(with: ConsoleSequencerXPCProtocol.self)
newConnection.exportedObject = exportedObject
newConnection.resume()
return true
}
}
我们的服务接口将由我们的协议 ConsoleSequencerXPCProtocol
来定义。与之前一样,导出对象是实现 ConsoleSequencerXPCProtocol
的对象的实例。让我们先定义协议。在 Sources 目录下创建文件 ConsoleSequencerXPCProtocol.swift
,并添加以下内容:
import Foundation
@objc(ConsoleSequencerXPCProtocol) protocol ConsoleSequencerXPCProtocol {
func toRedString(_ text: String, withReply reply: @escaping (String) -> Void)
func toGreenString(_ text: String, withReply reply: @escaping (String) -> Void)
}
这里我们定义了 XPC 服务提供的两个函数。现在让我们创建一个实现协议的类。在 Sources 目录下创建一个名为 ConsoleSequencerXPC.swift
的文件,并添加以下内容:
import Foundation
@objc class ConsoleSequencerXPC: NSObject, ConsoleSequencerXPCProtocol{
func toRedString(_ text: String, withReply reply: @escaping (String) -> Void) {
reply("\u{1B}[31m\(text)\u{1B}[0m")
}
func toGreenString(_ text: String, withReply reply: @escaping (String) -> Void) {
reply("\u{1B}[32m\(text)\u{1B}[0m")
}
}
好了,就是这样。这样,我们就可以构建代理代码了:
$ swift build
这应该没有问题。现在来注册我们的代理。如果你还记得,我们需要在 ~/Library/LaunchAgents/
中创建一个 plist 文件来注册我们的代理。在 ~/Library/LaunchAgents/
中新建一个名为 com.rderik.rdconsolesequencerxpc.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.rderik.ServiceProviderXPC</string>
<key>Program</key>
<string>PATH_TO_YOUR_PROJECT/rdConsoleSequencer/.build/debug/rdConsoleSequencer</string>
<key>MachServices</key>
<dict>
<key>com.rderik.ConsoleSequencerXPC</key>
<true/>
</dict>
</dict>
</plist>
看过我们为监控目录大小的脚本创建的格式后,您应该对该格式不陌生了。但其中也有一些不同之处,而且有一个字段需要修正。让我们看看这些不同之处:
- Plist 上的马赫服务字段定义了代理提供的马赫服务。服务名称与我们在
main.swift
中创建监听器时注册的名称相同:
let listener = NSXPCListener(machServiceName: "com.rderik.ConsoleSequencerXPC" )
我们没有间隔字段或启动时运行字段,因为我们的马赫服务将在请求到达 launchd
时按需加载。
好了,让我们加载代理:
$ launchctl load ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
我们可以用下面的命令检查它是否正在运行:
$ launchctl list | grep rderik
你会看到如下内容
- 0 com.rderik.ServiceProviderXPC
服务名称是我们在 com.rderik.rdconsolesequencerxpc.plist
中作为 Label 注册的名称。
好了,代理已经运行,现在我们需要创建一个使用它的工具来测试它。
使用我们的 XPC 服务
我们将使用 SPM(Swift 包管理器)创建另一个 Swift 可执行文件,以使用我们的服务。这个可执行文件很简单。它将是一个 REPL,读取用户键入的内容,然后以红色和绿色回显。
好了,让我们为新应用程序创建另一个目录。将其命名为 "rdConsoleSequencerClient",并使用 SPM 对其进行初始化:
$ mkdir rdConsoleSequencerClient
$ cd rdConsoleSequencerClient
$ swift package init --type executable
请记住消费 XPC 服务的工作流程:
(1) 我们创建一个与要使用的服务的连接。通过名称查找服务(我们为代理注册的 Mach 服务)。(2) 设置远程对象接口(Interface),这里我们需要知道描述接口的协议,也就是我们在代理上创建服务时使用的协议。(3) 获取实现接口的对象实例(使用 remoteObjectProxy)。(4) 最后,使用服务。请记住,调用总是异步的,也就是说,如果我们要使用用户界面,就需要通过主队列发送,这样就不会阻塞主线程。
好了,让我们编辑 main.swift
并添加以下代码:
import Foundation
print("Welcome to our simple REPL")
let connection = NSXPCConnection(machServiceName: "com.rderik.ConsoleSequencerXPC")
connection.remoteObjectInterface = NSXPCInterface(with: ConsoleSequencerXPCProtocol.self)
connection.resume()
let service = connection.remoteObjectProxyWithErrorHandler { error in
print("Received error:", error)
} as? ConsoleSequencerXPCProtocol
while true {
print("Insert text: ", terminator: "")
let text = readLine(strippingNewline: true)!
service!.toRedString(text) { (texto) in
print(texto, terminator: " ")
}
service!.toGreenString(text) { (texto) in
print(texto)
}
if text == "exit" {
break
}
}
请注意我们是如何创建马赫服务连接的,名称为 "com.rderik.ConsoleSequencerXPC",与我们在代理中注册的名称相同。
我们还需要创建 ConsoleSequencerXPCProtocol.swift
文件。您可以将其从代理中复制到源代码目录。以下是代码:
import Foundation
@objc(ConsoleSequencerXPCProtocol) protocol ConsoleSequencerXPCProtocol {
func toRedString(_ text: String, withReply reply: @escaping (String) -> Void)
func toGreenString(_ text: String, withReply reply: @escaping (String) -> Void)
}
好了,让我们确保代理正在运行:
$ launchctl list | grep rderik
如果没有加载,那就添加吧:
$ launchctl load ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
现在让我们构建并运行我们的客户端:
可能会提示你授予代理权限,允许它访问你的文档,这是因为它需要访问可执行文件。
现在,当你输入内容时,你会看到红色和绿色的提示。
恭喜你,你的 XPC 服务正在运行。
清理
玩完 XPC 服务后,我们应该注销该服务,并将其从 ~/Library/LaunchAgents/
目录中删除。
首先,让我们使用 launchctl
取消注册:
$ launchctl unload ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
确保不再注册:
$ launchctl list | grep rderik
现在我们可以安全地从 ~/Library/LaunchAgents
中删除它了:
$ rm ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
就这样,回到干净的状态。
最后的想法
现在你知道了如何创建启动代理,以及如何通过启动代理提供 XPC 服务。
XPC 服务是沟通应用程序的有用机制。现在,你可以创建一个与主应用程序交互的服务,并让代理成为其他应用程序与主应用程序之间的桥梁。如果要通过 App Store 发布应用程序,就必须使用权限。不过,你可以在开发过程中添加一个临时权限来进行测试:
com.apple.security.temporary-exception.mach-lookup.global-name
这是一个数组,你可以在其中添加应用程序可以连接的 XPC 服务。在我们的示例中,数组将包含以下项目: com.rderik.ConsoleSequencerXPC
。
希望这两篇文章对你有所帮助。现在你知道了如何在应用程序中创建 XPC 服务捆绑包,以及如何在启动代理或守护进程中提供 XPC 服务。
请告诉我你使用 XPC 的其他方法。我喜欢学习别人的做法。
好的,下次再见。
相关主题/感兴趣的注释
- 描述如何创建 XPC 服务捆绑包并解释 XPC 基础知识的文章链接 如果你还没有读过,我鼓励你读一读。
- 苹果公司关于创建 XPC 服务的文档,以及守护进程和服务编程指南
- 介绍 "launchd "和 plist 配置的便捷网站
- 维基百科上关于 launchd 的词条
- ANSI/VT100 终端控制 Escape 序列
- docs.microsoft.com 提供的更详细的控制转义序列资源