Fix deadlock in PermissionsChecker by replacing semaphore with RunLoop

- Replace DispatchSemaphore usage in checkScreenRecordingPermission with RunLoop pattern
- This was the root cause of CLI hangs affecting all commands that check permissions
- Use same async-to-sync bridging pattern as ImageCommand for consistency
This commit is contained in:
Peter Steinberger 2025-06-08 09:53:11 +01:00
parent fafa8fdc2a
commit d2fb50b289
3 changed files with 33 additions and 18 deletions

View file

@ -6,31 +6,32 @@ import ScreenCaptureKit
class PermissionsChecker {
static func checkScreenRecordingPermission() -> Bool {
// ScreenCaptureKit requires screen recording permission
// We check by attempting to get shareable content
let semaphore = DispatchSemaphore(value: 0)
var hasPermission = false
var capturedError: Error?
// We check by attempting to get shareable content using RunLoop to avoid semaphore deadlock
var result: Result<Bool, Error>?
let runLoop = RunLoop.current
Task {
do {
// This will fail if we don't have screen recording permission
_ = try await SCShareableContent.current
hasPermission = true
result = .success(true)
} catch {
// If we get an error, we don't have permission
capturedError = error
hasPermission = false
Logger.shared.debug("Screen recording permission check failed: \(error)")
result = .success(false)
}
semaphore.signal()
// Stop the run loop
CFRunLoopStop(runLoop.getCFRunLoop())
}
semaphore.wait()
// Run the event loop until the task completes
runLoop.run()
if let error = capturedError {
Logger.shared.debug("Screen recording permission check failed: \(error)")
guard let result = result else {
return false
}
return hasPermission
return (try? result.get()) ?? false
}
static func checkAccessibilityPermission() -> Bool {

View file

@ -1,4 +1,4 @@
// This file is auto-generated by the build script. Do not edit manually.
enum Version {
static let current = "1.0.0-beta.21"
static let current = "1.0.0-beta.22"
}

View file

@ -116,7 +116,7 @@ struct ScreenshotValidationTests {
// MARK: - Multi-Display Tests
@Test("Capture from multiple displays", .tags(.multiDisplay))
func multiDisplayCapture() throws {
func multiDisplayCapture() async throws {
let screens = NSScreen.screens
print("Found \(screens.count) display(s)")
@ -126,7 +126,7 @@ struct ScreenshotValidationTests {
defer { try? FileManager.default.removeItem(atPath: outputPath) }
do {
_ = try captureDisplayToFile(displayID: displayID, path: outputPath, format: .png)
_ = try await captureDisplayToFile(displayID: displayID, path: outputPath, format: .png)
#expect(FileManager.default.fileExists(atPath: outputPath))
@ -283,10 +283,24 @@ struct ScreenshotValidationTests {
displayID: CGDirectDisplayID,
path: String,
format: ImageFormat
) throws -> ImageCaptureData {
guard let image = CGDisplayCreateImage(displayID) else {
) async throws -> ImageCaptureData {
let availableContent = try await SCShareableContent.current
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
throw CaptureError.captureCreationFailed(nil)
}
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
let configuration = SCStreamConfiguration()
configuration.backgroundColor = .clear
configuration.shouldBeOpaque = true
configuration.showsCursor = false
let image = try await SCScreenshotManager.captureImage(
contentFilter: filter,
configuration: configuration
)
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
try saveImage(nsImage, to: path, format: format)