mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
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:
parent
fafa8fdc2a
commit
d2fb50b289
3 changed files with 33 additions and 18 deletions
|
|
@ -6,31 +6,32 @@ import ScreenCaptureKit
|
||||||
class PermissionsChecker {
|
class PermissionsChecker {
|
||||||
static func checkScreenRecordingPermission() -> Bool {
|
static func checkScreenRecordingPermission() -> Bool {
|
||||||
// ScreenCaptureKit requires screen recording permission
|
// ScreenCaptureKit requires screen recording permission
|
||||||
// We check by attempting to get shareable content
|
// We check by attempting to get shareable content using RunLoop to avoid semaphore deadlock
|
||||||
let semaphore = DispatchSemaphore(value: 0)
|
var result: Result<Bool, Error>?
|
||||||
var hasPermission = false
|
let runLoop = RunLoop.current
|
||||||
var capturedError: Error?
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
// This will fail if we don't have screen recording permission
|
// This will fail if we don't have screen recording permission
|
||||||
_ = try await SCShareableContent.current
|
_ = try await SCShareableContent.current
|
||||||
hasPermission = true
|
result = .success(true)
|
||||||
} catch {
|
} catch {
|
||||||
// If we get an error, we don't have permission
|
// If we get an error, we don't have permission
|
||||||
capturedError = error
|
Logger.shared.debug("Screen recording permission check failed: \(error)")
|
||||||
hasPermission = false
|
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 {
|
guard let result = result else {
|
||||||
Logger.shared.debug("Screen recording permission check failed: \(error)")
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasPermission
|
return (try? result.get()) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
static func checkAccessibilityPermission() -> Bool {
|
static func checkAccessibilityPermission() -> Bool {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// This file is auto-generated by the build script. Do not edit manually.
|
// This file is auto-generated by the build script. Do not edit manually.
|
||||||
enum Version {
|
enum Version {
|
||||||
static let current = "1.0.0-beta.21"
|
static let current = "1.0.0-beta.22"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ struct ScreenshotValidationTests {
|
||||||
// MARK: - Multi-Display Tests
|
// MARK: - Multi-Display Tests
|
||||||
|
|
||||||
@Test("Capture from multiple displays", .tags(.multiDisplay))
|
@Test("Capture from multiple displays", .tags(.multiDisplay))
|
||||||
func multiDisplayCapture() throws {
|
func multiDisplayCapture() async throws {
|
||||||
let screens = NSScreen.screens
|
let screens = NSScreen.screens
|
||||||
print("Found \(screens.count) display(s)")
|
print("Found \(screens.count) display(s)")
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ struct ScreenshotValidationTests {
|
||||||
defer { try? FileManager.default.removeItem(atPath: outputPath) }
|
defer { try? FileManager.default.removeItem(atPath: outputPath) }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try captureDisplayToFile(displayID: displayID, path: outputPath, format: .png)
|
_ = try await captureDisplayToFile(displayID: displayID, path: outputPath, format: .png)
|
||||||
|
|
||||||
#expect(FileManager.default.fileExists(atPath: outputPath))
|
#expect(FileManager.default.fileExists(atPath: outputPath))
|
||||||
|
|
||||||
|
|
@ -283,11 +283,25 @@ struct ScreenshotValidationTests {
|
||||||
displayID: CGDirectDisplayID,
|
displayID: CGDirectDisplayID,
|
||||||
path: String,
|
path: String,
|
||||||
format: ImageFormat
|
format: ImageFormat
|
||||||
) throws -> ImageCaptureData {
|
) async throws -> ImageCaptureData {
|
||||||
guard let image = CGDisplayCreateImage(displayID) else {
|
let availableContent = try await SCShareableContent.current
|
||||||
|
|
||||||
|
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
||||||
throw CaptureError.captureCreationFailed(nil)
|
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))
|
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
|
||||||
try saveImage(nsImage, to: path, format: format)
|
try saveImage(nsImage, to: path, format: format)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue