mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
Fix async concurrency issues without semaphores
- Replace problematic DispatchSemaphore usage with NSCondition-based async bridge - Revert to ParsableCommand for compatibility while maintaining async operations - Use CGWindowListCopyWindowInfo for sync permission checking instead of async ScreenCaptureKit - Remove all RunLoop workarounds in favor of proper Task.runBlocking pattern - Eliminate all deadlock sources while preserving async capture functionality
This commit is contained in:
parent
d2fb50b289
commit
50984f8dc2
3 changed files with 41 additions and 53 deletions
33
peekaboo-cli/Sources/peekaboo/AsyncUtils.swift
Normal file
33
peekaboo-cli/Sources/peekaboo/AsyncUtils.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Task where Success == Void, Failure == Never {
|
||||||
|
/// Runs an async operation synchronously by blocking the current thread.
|
||||||
|
/// This is a safer alternative to using DispatchSemaphore with Swift concurrency.
|
||||||
|
static func runBlocking<T>(operation: @escaping () async throws -> T) throws -> T {
|
||||||
|
var result: Result<T, Error>?
|
||||||
|
let condition = NSCondition()
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let value = try await operation()
|
||||||
|
condition.lock()
|
||||||
|
result = .success(value)
|
||||||
|
condition.signal()
|
||||||
|
condition.unlock()
|
||||||
|
} catch {
|
||||||
|
condition.lock()
|
||||||
|
result = .failure(error)
|
||||||
|
condition.signal()
|
||||||
|
condition.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
condition.lock()
|
||||||
|
while result == nil {
|
||||||
|
condition.wait()
|
||||||
|
}
|
||||||
|
condition.unlock()
|
||||||
|
|
||||||
|
return try result!.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,37 +56,15 @@ struct ImageCommand: ParsableCommand {
|
||||||
Logger.shared.setJsonOutputMode(jsonOutput)
|
Logger.shared.setJsonOutputMode(jsonOutput)
|
||||||
do {
|
do {
|
||||||
try PermissionsChecker.requireScreenRecordingPermission()
|
try PermissionsChecker.requireScreenRecordingPermission()
|
||||||
let savedFiles = try runAsyncCapture()
|
// Use Task.runBlocking pattern for proper async-to-sync bridge
|
||||||
|
let savedFiles = try Task.runBlocking {
|
||||||
|
try await performCapture()
|
||||||
|
}
|
||||||
outputResults(savedFiles)
|
outputResults(savedFiles)
|
||||||
} catch {
|
} catch {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func runAsyncCapture() throws -> [SavedFile] {
|
|
||||||
// Create a new event loop using RunLoop to handle async properly
|
|
||||||
var result: Result<[SavedFile], Error>?
|
|
||||||
let runLoop = RunLoop.current
|
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let savedFiles = try await performCapture()
|
|
||||||
result = .success(savedFiles)
|
|
||||||
} catch {
|
|
||||||
result = .failure(error)
|
|
||||||
}
|
|
||||||
// Stop the run loop
|
|
||||||
CFRunLoopStop(runLoop.getCFRunLoop())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the event loop until the task completes
|
|
||||||
runLoop.run()
|
|
||||||
|
|
||||||
guard let result = result else {
|
|
||||||
throw CaptureError.captureCreationFailed(nil)
|
|
||||||
}
|
|
||||||
return try result.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performCapture() async throws -> [SavedFile] {
|
private func performCapture() async throws -> [SavedFile] {
|
||||||
let captureMode = determineMode()
|
let captureMode = determineMode()
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,10 @@ import ScreenCaptureKit
|
||||||
|
|
||||||
class PermissionsChecker {
|
class PermissionsChecker {
|
||||||
static func checkScreenRecordingPermission() -> Bool {
|
static func checkScreenRecordingPermission() -> Bool {
|
||||||
// ScreenCaptureKit requires screen recording permission
|
// Use a simpler approach - check CGWindowListCreateImage which doesn't require async
|
||||||
// We check by attempting to get shareable content using RunLoop to avoid semaphore deadlock
|
// This is the traditional way to check screen recording permission
|
||||||
var result: Result<Bool, Error>?
|
let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID)
|
||||||
let runLoop = RunLoop.current
|
return windowList != nil && CFArrayGetCount(windowList) > 0
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
// This will fail if we don't have screen recording permission
|
|
||||||
_ = try await SCShareableContent.current
|
|
||||||
result = .success(true)
|
|
||||||
} catch {
|
|
||||||
// If we get an error, we don't have permission
|
|
||||||
Logger.shared.debug("Screen recording permission check failed: \(error)")
|
|
||||||
result = .success(false)
|
|
||||||
}
|
|
||||||
// Stop the run loop
|
|
||||||
CFRunLoopStop(runLoop.getCFRunLoop())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the event loop until the task completes
|
|
||||||
runLoop.run()
|
|
||||||
|
|
||||||
guard let result = result else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return (try? result.get()) ?? false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func checkAccessibilityPermission() -> Bool {
|
static func checkAccessibilityPermission() -> Bool {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue