现在有些旧且不再更新的技术说明 TN2083守护进程和代理状态:
Apple 对这个问题的解决方案是分层:我们将框架划分为层,并为每一层决定该层是否支持全局引导命名空间中的操作。基本规则是 CoreServices 及以下的所有内容(包括 System、IOKit、System Configuration、Foundation)都应该在任何引导命名空间(这些是守护进程安全框架)中工作,而 CoreServices 之上的所有内容(包括 ApplicationServices、Carbon 和 AppKit)都需要一个 GUI-per-session 引导命名空间。
这与 Asmus 在 Apple 自己的开发者论坛中关于支持非守护进程安全框架的有趣发现一致。
适当命名的危险生活部分还描述了当使用不是守护进程安全的框架时,某些事情完全有可能在某种程度上可能工作或可能不工作。
特别是,以下陈述非常具有启发性:
- 一些框架在加载时失败。也就是说,框架有一个初始化例程,假设它在每个会话上下文中运行,如果不是,则失败。这个问题在当前系统上很少见,因为大多数框架都是延迟初始化的。如果框架在加载时没有失败,当您从该框架调用各种例程时,您可能仍然会遇到问题。
- 例程可能会良性失败。例如,该例程可能会静默失败,或者向 stderr 打印一条消息,或者可能返回一个有意义的错误代码。
- 例程可能会恶意失败。例如,如果 GUI 框架由守护程序运行,则调用 abort 是很常见的!
- 即使它的框架不是正式的守护进程安全的,例程也可能工作。
- 例程的行为可能会根据其输入参数而有所不同。例如,图像解压缩例程可能对某些类型的图像有效,而对其他类型的图像则失败。任何给定框架的行为,以及该框架内的例程,都可以随着发布而改变。
它还说:
这样做的结果是,如果你的守护进程链接到一个不是守护进程安全的框架,你就无法预测它的一般行为方式。它可能在您的机器上工作,但在其他用户的机器上失败,或者在未来的系统版本上失败,或者因不同的输入数据而失败。你活得很危险!
根据具体要求,使用 LaunchAgent 可能是一种替代方法。当然,缺点是 LaunchAgent 仅在用户登录到图形会话时才被调用。在下面的小例子中可以自己测试,访问相机没有问题,正如预期的那样。
启动代理
一个没有故事板的小型、独立示例的实验,即使使用 AppKit(用于图像转换)以及 AVFoundation,并将照片保存为 .png,可能如下所示:
相机.swift
import AVFoundation
import AppKit
enum CameraError: Error {
case notFound
case noVideInput
case noValidImageData
case fetchImage
case imageRepresentation
case pngCreation
}
class Camera: NSObject, AVCapturePhotoCaptureDelegate {
private var completion: (Result<Void, Error>) -> Void = { _ in }
private var targetURL: URL?
private var cameraDevice: AVCaptureDevice?
private var captureSession: AVCaptureSession?
private var photoOutput: AVCapturePhotoOutput?
func prepare() -> Result<Void, Error> {
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
mediaType: AVMediaType.video,
position: AVCaptureDevice.Position.front)
guard let cameraDevice = deviceDiscoverySession.devices.first else {
return .failure(CameraError.notFound)
}
self.cameraDevice = cameraDevice
guard let videoInput = try? AVCaptureDeviceInput(device: cameraDevice) else {
return .failure(CameraError.notFound)
}
let captureSession = AVCaptureSession()
self.captureSession = captureSession
captureSession.sessionPreset = AVCaptureSession.Preset.photo
captureSession.beginConfiguration()
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
}
let photoOutput = AVCapturePhotoOutput()
self.photoOutput = photoOutput
if captureSession.canAddOutput(photoOutput) {
captureSession.addOutput(photoOutput)
}
_ = AVCaptureConnection(inputPorts: videoInput.ports, output: photoOutput)
captureSession.commitConfiguration()
captureSession.startRunning()
return .success(Void())
}
func savePhoto(after: TimeInterval, at targetURL: URL, completion: @escaping (Result<Void, Error>) -> Void) {
self.completion = completion
self.targetURL = targetURL
DispatchQueue.main.asyncAfter(deadline: .now() + after) {
self.photoOutput?.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
}
}
// MARK: - AVCapturePhotoCaptureDelegate
internal func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
if let error = error {
completion(.failure(error))
return
}
guard let captureSession = captureSession,
let imageData = photo.fileDataRepresentation(),
let targetURL = targetURL else {
completion(.failure(CameraError.fetchImage))
return
}
captureSession.stopRunning()
completion(Self.writePNG(imageData, to: targetURL))
}
// MARK: - Private
private static func writePNG(_ imageData: Data, to url: URL) -> Result<Void, Error> {
guard let image = NSImage(data: imageData) else { return .failure(CameraError.noValidImageData) }
guard let bitmapImageRep = image.representations[0] as? NSBitmapImageRep else { return .failure(CameraError.imageRepresentation) }
guard let pngData = bitmapImageRep.representation(using: .png, properties: [:]) else { return .failure(CameraError.pngCreation) }
do {
try pngData.write(to: url)
} catch {
return .failure(error as Error)
}
return .success(Void())
}
}
AppDelegate.swift
import AppKit
class AppDelegate: NSObject, NSApplicationDelegate {
private let camera = Camera()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let imageUrl = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("test.png")
switch camera.prepare() {
case .success():
self.camera.savePhoto(after: 1, at: imageUrl, completion: { result in
switch result {
case .success():
NSLog("success")
exit(0)
case .failure(let error):
NSLog("error: \(error)")
exit(1)
}
})
case .failure(let error):
NSLog("error: \(error)")
exit(1);
}
}
func applicationWillTerminate(_ aNotification: Notification) {
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
main.swift:
import AppKit
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
除了com.apple.security.device.camera
权限之外,还设置了LSUIElement
in并添加了带有文本的键。Info.plist
true
NSCameraUsageDescription
这当然不是一个有效适用的通用解决方案,但至少应该允许进行总体复杂性较低的实验。
~/Library/LaunchAgents 中的 com.software7.camera.plist:
这里应用程序每 30 秒触发一次:
<?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.software7.camera</string>
<key>ProgramArguments</key>
<array>
<string>/Users/stephan/test/WebcamPhoto.app/Contents/MacOS/WebcamPhoto</string>
</array>
<key>StartInterval</key>
<integer>30</integer>
</dict>
</plist>
假设id -u
目标用户是 503,设置完成:
launchctl bootstrap gui/503 ~/Library/LaunchAgents/com.software7.camera.plist
并且可以再次删除
launchctl bootout gui/503 ~/Library/LaunchAgents/com.software7.camera.plist
拆分为 Daemon 和 Agent 组件
如果您编写这样的 LaunchAgent,您可以将其链接到任何框架,如上面使用 AppKit 的示例所示。
Apple 的技术说明中也有一个很好的建议,如果完全没有守护程序就无法完成,则可以拆分代码。苹果对此写道:
如果您正在编写一个守护程序并且您必须与一个非守护程序安全的框架链接,请考虑将您的代码拆分为一个守护程序组件和一个代理程序组件。如果这不可能,请注意与将守护程序链接到不安全框架相关的潜在问题......
一些测试
我不会将以下内容视为证据,而是强烈表明使用 AppKit 的应用程序不应按照 Apple 的建议用于 LaunchDemons:
使用 4 个变体运行测试,所有变体都在调用它们时将条目写入同一个日志文件/tmp/daemonlog.txt
,然后退出:
- 使用 AppKit 的应用程序
- 没有 AppKit 的应用程序
- 直接从 launchd 调用的 Shell 脚本
- 自动化应用程序调用的 Shell 脚本
在/Library/LaunchDaemons
变体中,启动间隔设置在 25 到 35 秒之间。
观察:只要用户登录,所有 4 个变种都会根据它们指定的开始间隔定期写出它们的消息。用户注销后,应继续创建日志条目。但是,只有不使用 AppKit 的变体 2) 和 3) 会这样做。变体 1) 和 4) 不再起作用。在活动监视器中,您可以看到两个应用程序都挂起,但实际上它们被编程为在写入日志输出后立即退出。当手动终止这两个应用程序时,它们会再次开始正常工作,但前提是用户保持登录状态。
日志文件中的黄色突出显示区域(=用户已注销)可以很容易地看到这一点:

测试源代码
作家.swift
Writer.swift
由 1) 和 2) 使用:
import Foundation
extension String {
func append(to url: URL) throws {
let line = self + "\n"
if let fh = FileHandle(forWritingAtPath: url.path) {
fh.seekToEndOfFile()
fh.write(line.data(using: .utf8)!)
fh.closeFile()
}
else {
try line.write(to: url, atomically: true, encoding: .utf8)
}
}
}
extension Date {
func logDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss z yyyy"
return dateFormatter.string(from: self)
}
}
AppDelegate.swift
案例 1 中的应用程序使用 AppDelegate。对应的使用这个 AppDelegate 的 main.swift 和上图类似,这里不再赘述:
import AppKit
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
let url = URL(string: "file:///tmp/daemonlog.txt")!
do {
try "\(Date().logDate()): AppKit application called from launchd".append(to: url)
} catch {
print("error: \(error)")
}
exit(0)
}
func applicationWillTerminate(_ aNotification: Notification) {
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
main.swift
没有 AppKit 的应用程序是案例 2,如下所示:
import Foundation
let url = URL(string: "file:///tmp/daemonlog.txt")!
do {
try "\(Date().logDate()): application without AppKit called from launchd".append(to: url)
} catch {
print("error: \(error)")
}
exit(0)
守护进程脚本.sh
对于案例 3 daemonscript.sh
,由 直接调用launchd
。
#!/bin/sh
echo "`date`: shell script directly called from launchd" >> /tmp/daemonlog.txt
自动化配置
在 Automator 中,Run Shell Script
使用了一个动作,如下所示:
echo "`date`: run shell script via Automator app" >> /tmp/daemonlog.txt