[macOS翻译]使用 Swift 在 macOS 上创建可提供 XPC 服务的启动代理

87 阅读9分钟

本文由 简悦 SimpRead 转码, 原文地址 rderik.com

上周我们讨论了如何在 macOS 应用程序中构建 XPC 服务(.xpc 捆绑程序)。Th......

上周我们讨论了如何在 macOS 应用程序中构建 XPC 服务(the .xpc bundles)。本周我们将探讨如何提供可供其他应用程序或工具使用的 XPC 服务。

为了让其他进程也能使用 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。要添加
  1. 打开系统偏好设置
  2. 转到 "安全和隐私
  3. 向下滚动到 "全磁盘访问"
  4. 在 Finder 中打开 bin 目录。你可以在终端使用 open(1)命令。
$ open /bin
  1. 将 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 的其他方法。我喜欢学习别人的做法。

好的,下次再见。

相关主题/感兴趣的注释


www.deepl.com 翻译