I have Home Assistant Core (a Python server) running as a LaunchDaemon on an OSX 11.6
(Big Sur) Mac Mini. I am trying to build a plugin for it that directly accesses a camera attached to the machine. This requires OSX Camera permissions.
Unfortunately there is no way to add an arbitrary binary (e.g. python from the server's virtualenv) to Camera permissions; there is no
icon as with other permissions. When I run my code from a terminal I get the camera prompt, which adds Terminal.app
(or iTerm2.app, or sshd-keygen-wrapper) to Camera permissions, and everything works. But since none of these is the launchd
root process, it fails when running under the Home Assistant daemon.
I found this question whose accepted answer suggests wrapping an Automator app around the binary:
Running python script in Mac OSX launchd permission issue
I created the app, and when I use /usr/bin/open -a
to run it from a terminal, I get the Camera permissions prompt and the .app
is added to the Camera permissions list, exactly as desired. However, when I then modify the LaunchDaemon .plist
to run (via ProgramArguments
) /usr/bin/open -a /opt/homeassistant/bin/hass.app
I get this error:
The application /opt/homeassistant/bin/hass.app cannot be opened for an unexpected reason, error=Error Domain=NSOSStatusErrorDomain Code=-10826 "kLSNoLaunchPermissionErr: User doesn't have permission to launch the app (managed networks)" UserInfo={_LSFunction=_LSLaunchWithRunningboard, _LSLine=2488, NSUnderlyingError=0x126309f40 {Error Domain=RBSRequestErrorDomain Code=5 "Launch failed." UserInfo={NSLocalizedFailureReason=Launch failed., NSUnderlyingError=0x12630b350 {Error Domain=OSLaunchdErrorDomain Code=125 "Domain does not support specified action" UserInfo={NSLocalizedFailureReason=Domain does not support specified action}}}}}
I verified that hass.app
and everything within it is owned by the LaunchDaemon's UserName
and GroupName
, homeassistant:homeassistant
, and that its Contents/MacOS/Automator Application Stub
has x
. I tried giving the app Full Disk Access. I don't see anything useful in the system.log; just that the daemon is crash-looping.
I found questions about similar permissions issues whose answers suggested re-signing the app, removing quarantine xattrs, etc. but that's not the issue here, since it runs just fine from the terminal.
What is causing this permissions error, and how can I resolve it?
CodePudding user response:
While this probably isn't the answer you wanted to hear, it appears that accessing the camera through a LaunchDaemon is actually not possible anymore, at least according to this answer given by Apple staff member "eskimo" over at Apple's own developer forums:
I’m sorry to so that there’s no supported way to make this work, because camera access is based on an array of frameworks that are not daemon safe.
Note that since I don't know precisely how Apple is prohibiting camera access, it might still be possible to run external cameras through external frameworks within a LaunchDaemon - the post above is in repsonse to accessing the internal camera.
I fear you'll likely not get a better answer here, at least without some example to work with (i.e. some code this community could try to reproduce your error with).
CodePudding user response:
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 ...