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:
Peter Steinberger 2025-06-08 10:10:04 +01:00
parent d2fb50b289
commit 50984f8dc2
3 changed files with 41 additions and 53 deletions

View 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()
}
}

View file

@ -56,37 +56,15 @@ struct ImageCommand: ParsableCommand {
Logger.shared.setJsonOutputMode(jsonOutput)
do {
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)
} catch {
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] {
let captureMode = determineMode()

View file

@ -5,33 +5,10 @@ import ScreenCaptureKit
class PermissionsChecker {
static func checkScreenRecordingPermission() -> Bool {
// ScreenCaptureKit requires screen recording permission
// 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
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
// Use a simpler approach - check CGWindowListCreateImage which doesn't require async
// This is the traditional way to check screen recording permission
let windowList = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID)
return windowList != nil && CFArrayGetCount(windowList) > 0
}
static func checkAccessibilityPermission() -> Bool {