The now somewhat older and no longer updated Technical Note TN2083 Daemons and Agents states:
Apple's solution to this problem is layering: we divide our frameworks into layers and decide for each layer whether that layer supports operations in the global bootstrap namespace. The basic rule is that everything in CoreServices and below (including System, IOKit, System Configuration, Foundation) should work in any bootstrap namespace (these are daemon-safe frameworks), while everything above CoreServices (including ApplicationServices, Carbon, and AppKit) requires a GUI-per-session bootstrap namespace.
Which lines up with Asmus' interesting find from Apple's own developer forums regarding support for non-daemon-safe frameworks.
The appropriately named section Living Dangerously also describes that when using frameworks that are not daemon-safe, it is entirely possible that certain things may or may not work to some degree.
In particular, the following statements are very revealing:
- Some frameworks fail at load time. That is, the framework has an initialization routine that assumes it's running in a per-session context and fails if it's not.
This problem is rare on current systems because most frameworks are initialized lazily.
If the framework doesn't fail at load time, you may still encounter problems as you call various routines from that framework.
- A routine might fail benignly. For example, the routine might fail silently, or print a message to stderr, or perhaps return a meaningful error code.
- A routine might fail hostilely. For example, it's quite common for the GUI frameworks to call abort if they're run by a daemon!
- A routine might work even though its framework is not officially daemon-safe.
- A routine might behave differently depending on its input parameters. For example, an image decompression routine might work for some types of images and fail for others.
The behavior of any given framework, and the routines within that framework, can change from release-to-release.
Also it says:
The upshot of this is that, if your daemon links with a framework that's not daemon-safe, you can't predict how it will behave in general. It might work on your machine, but fail on some other user's machine, or fail on a future system release, or fail for different input data. You are living dangerously!
Depending on the exact requirements, using a LaunchAgent might be an alternative. The downside, of course, is that LaunchAgents are only invoked when the user logs into a graphical session. As one can test for oneself in the following small example, accessing the camera is no problem, as expected.
Launch Agent
An experiment with a small, self-contained example without a storyboard, even using AppKit (for image conversion) in addition to AVFoundation, and taking and saving a photo as a .png, might look like this:
Camera.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)
In addition to the com.apple.security.device.camera
permission, LSUIElement
in Info.plist
is set to true
and a NSCameraUsageDescription
key with text added.
This is certainly not a productively applicable generic solution, but should at least allow experiments with lower overall complexity.
com.software7.camera.plist in ~/Library/LaunchAgents:
Here the app is triggered every 30 seconds:
<?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>
Assuming id -u
for the target user is 503, the setup is done with:
launchctl bootstrap gui/503 ~/Library/LaunchAgents/com.software7.camera.plist
and could be removed again with
launchctl bootout gui/503 ~/Library/LaunchAgents/com.software7.camera.plist
Splitting into Daemon and Agent component
If you write such a LaunchAgent, you can link it to any framework, as shown in the example above with AppKit.
There is also a good suggestion in Apple's Technical Note that it is possible to split the code if it is not possible to do without a daemon completely. Apple writes about this:
If you're writing a daemon and you must link with a framework that's not daemon-safe, consider splitting your code into a daemon component and an agent component. If that's not possible, be aware of the potential issues associated with linking a daemon to unsafe frameworks ...
Some Tests
I would not consider the following as proof, rather as a strong indication that applications using AppKit should not be used for LaunchDemons as recommended by Apple:
A test was run with 4 variants, all of which write an entry to the same log file named /tmp/daemonlog.txt
when they are called, and then exit:
- Application with AppKit
- Application without AppKit
- Shell script called directly from launchd
- Shell script called by automator application
In /Library/LaunchDaemons
the variants were set up with startup intervals between 25 and 35 seconds.
Observation:
As long as the user is logged in, all 4 variants write out their messages periodically according to their specified start interval. As soon as the user logs out, the log entries should continue to be created. However, only variants 2) and 3), which do not use AppKit, do this. Variants 1) and 4) no longer work. In the Activity Monitor you can see that both applications are hanging, but actually they are programmed to quit immediately after writing the log output. When both applications are terminated manually, they start working normally again, but only as long as the user remains logged in.
This can be easily seen by the yellow highlighted area (=user logged out) in the log file:

Test Source Codes
Writer.swift
Writer.swift
is used by 1) and 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
The application in case 1 uses an AppDelegate. The corresponding main.swift that uses this AppDelegate looks like the one shown above and is not repeated here:
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
The application without AppKit is case 2 and looks like this:
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)
daemonscript.sh
For case 3 daemonscript.sh
is called directly by launchd
.
#!/bin/sh
echo "`date`: shell script directly called from launchd" >> /tmp/daemonlog.txt
Automator Config
In Automator, a Run Shell Script
action is used, which looks like this:
echo "`date`: run shell script via Automator app" >> /tmp/daemonlog.txt