From d2fb50b2895c6a2d84ab669dd8ff3b2ad37e5a5e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Jun 2025 09:53:11 +0100 Subject: [PATCH] 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 --- .../Sources/peekaboo/PermissionsChecker.swift | 27 ++++++++++--------- peekaboo-cli/Sources/peekaboo/Version.swift | 2 +- .../ScreenshotValidationTests.swift | 22 ++++++++++++--- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift b/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift index c04b581..b37b3af 100644 --- a/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift +++ b/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift @@ -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? + 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 { diff --git a/peekaboo-cli/Sources/peekaboo/Version.swift b/peekaboo-cli/Sources/peekaboo/Version.swift index a0c6f32..882d344 100644 --- a/peekaboo-cli/Sources/peekaboo/Version.swift +++ b/peekaboo-cli/Sources/peekaboo/Version.swift @@ -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" } diff --git a/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift b/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift index 58fb3c5..4e8ef63 100644 --- a/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift @@ -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)