Fix Screen Recording permission detection and improve error reporting

- Replace broken CGDisplayBounds check with ScreenCaptureKit API
- Add proper error handling to detect permission-related failures
- Add server_status subcommand to expose permission status via JSON
- Ensure users get clear error messages when permissions are missing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-06-07 22:51:49 +01:00
parent a491adbdf1
commit de5a0cb97e
3 changed files with 143 additions and 63 deletions

View file

@ -322,12 +322,21 @@ struct ImageCommand: ParsableCommand {
if let error = captureError { if let error = captureError {
throw error throw error
} }
} catch let error as CaptureError {
// Re-throw CaptureError as-is
throw error
} catch { } catch {
// Check if this is a permission error from ScreenCaptureKit
let errorString = error.localizedDescription.lowercased()
if errorString.contains("screen recording") || errorString.contains("permission") {
throw CaptureError.screenRecordingPermissionDenied
}
throw CaptureError.captureCreationFailed throw CaptureError.captureCreationFailed
} }
} }
private func captureDisplayWithScreenCaptureKit(_ displayID: CGDirectDisplayID, to path: String) async throws { private func captureDisplayWithScreenCaptureKit(_ displayID: CGDirectDisplayID, to path: String) async throws {
do {
// Get available content // Get available content
let availableContent = try await SCShareableContent.current let availableContent = try await SCShareableContent.current
@ -354,6 +363,14 @@ struct ImageCommand: ParsableCommand {
) )
try saveImage(image, to: path) try saveImage(image, to: path)
} catch {
// Check if this is a permission error
let errorString = error.localizedDescription.lowercased()
if errorString.contains("screen recording") || errorString.contains("permission") {
throw CaptureError.screenRecordingPermissionDenied
}
throw error
}
} }
private func captureWindow(_ window: WindowData, to path: String) throws(CaptureError) { private func captureWindow(_ window: WindowData, to path: String) throws(CaptureError) {
@ -375,12 +392,21 @@ struct ImageCommand: ParsableCommand {
if let error = captureError { if let error = captureError {
throw error throw error
} }
} catch let error as CaptureError {
// Re-throw CaptureError as-is
throw error
} catch { } catch {
// Check if this is a permission error from ScreenCaptureKit
let errorString = error.localizedDescription.lowercased()
if errorString.contains("screen recording") || errorString.contains("permission") {
throw CaptureError.screenRecordingPermissionDenied
}
throw CaptureError.windowCaptureFailed throw CaptureError.windowCaptureFailed
} }
} }
private func captureWindowWithScreenCaptureKit(_ window: WindowData, to path: String) async throws { private func captureWindowWithScreenCaptureKit(_ window: WindowData, to path: String) async throws {
do {
// Get available content // Get available content
let availableContent = try await SCShareableContent.current let availableContent = try await SCShareableContent.current
@ -407,6 +433,14 @@ struct ImageCommand: ParsableCommand {
) )
try saveImage(image, to: path) try saveImage(image, to: path)
} catch {
// Check if this is a permission error
let errorString = error.localizedDescription.lowercased()
if errorString.contains("screen recording") || errorString.contains("permission") {
throw CaptureError.screenRecordingPermissionDenied
}
throw error
}
} }
private func saveImage(_ image: CGImage, to path: String) throws(CaptureError) { private func saveImage(_ image: CGImage, to path: String) throws(CaptureError) {

View file

@ -6,7 +6,7 @@ struct ListCommand: ParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "list", commandName: "list",
abstract: "List running applications or windows", abstract: "List running applications or windows",
subcommands: [AppsSubcommand.self, WindowsSubcommand.self], subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self],
defaultSubcommand: AppsSubcommand.self defaultSubcommand: AppsSubcommand.self
) )
} }
@ -177,3 +177,44 @@ struct WindowsSubcommand: ParsableCommand {
} }
} }
} }
struct ServerStatusSubcommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "server_status",
abstract: "Check server permissions status"
)
@Flag(name: .long, help: "Output results in JSON format")
var jsonOutput = false
func run() throws {
Logger.shared.setJsonOutputMode(jsonOutput)
let screenRecording = PermissionsChecker.checkScreenRecordingPermission()
let accessibility = PermissionsChecker.checkAccessibilityPermission()
let permissions = ServerPermissions(
screen_recording: screenRecording,
accessibility: accessibility
)
let data = ServerStatusData(permissions: permissions)
if jsonOutput {
outputSuccess(data: data)
} else {
print("Server Permissions Status:")
print(" Screen Recording: \(screenRecording ? "✅ Granted" : "❌ Not granted")")
print(" Accessibility: \(accessibility ? "✅ Granted" : "❌ Not granted")")
}
}
}
struct ServerPermissions: Codable {
let screen_recording: Bool
let accessibility: Bool
}
struct ServerStatusData: Codable {
let permissions: ServerPermissions
}

View file

@ -1,24 +1,29 @@
import AVFoundation import AVFoundation
import CoreGraphics import CoreGraphics
import Foundation import Foundation
import ScreenCaptureKit
class PermissionsChecker { class PermissionsChecker {
static func checkScreenRecordingPermission() -> Bool { static func checkScreenRecordingPermission() -> Bool {
// Check if we can capture screen content by trying to get display bounds // ScreenCaptureKit requires screen recording permission
var displayCount: UInt32 = 0 // We check by attempting to get shareable content
let result = CGGetActiveDisplayList(0, nil, &displayCount) let semaphore = DispatchSemaphore(value: 0)
if result != .success || displayCount == 0 { var hasPermission = false
return false
Task {
do {
// This will fail if we don't have screen recording permission
_ = try await SCShareableContent.current
hasPermission = true
} catch {
// If we get an error, we don't have permission
hasPermission = false
}
semaphore.signal()
} }
// Try to capture a small image to test permissions semaphore.wait()
guard let mainDisplayID = CGMainDisplayID() as CGDirectDisplayID? else { return hasPermission
return false
}
// Test by trying to get display bounds - this requires screen recording permission
let bounds = CGDisplayBounds(mainDisplayID)
return bounds != CGRect.zero
} }
static func checkAccessibilityPermission() -> Bool { static func checkAccessibilityPermission() -> Bool {