mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
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:
parent
a491adbdf1
commit
de5a0cb97e
3 changed files with 143 additions and 63 deletions
|
|
@ -322,38 +322,55 @@ 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 {
|
||||||
// Get available content
|
do {
|
||||||
let availableContent = try await SCShareableContent.current
|
// Get available content
|
||||||
|
let availableContent = try await SCShareableContent.current
|
||||||
|
|
||||||
// Find the display by ID
|
// Find the display by ID
|
||||||
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
||||||
throw CaptureError.captureCreationFailed
|
throw CaptureError.captureCreationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create content filter for the entire display
|
||||||
|
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
||||||
|
|
||||||
|
// Configure capture settings
|
||||||
|
let configuration = SCStreamConfiguration()
|
||||||
|
configuration.width = scDisplay.width
|
||||||
|
configuration.height = scDisplay.height
|
||||||
|
configuration.backgroundColor = .black
|
||||||
|
configuration.shouldBeOpaque = true
|
||||||
|
configuration.showsCursor = true
|
||||||
|
|
||||||
|
// Capture the image
|
||||||
|
let image = try await SCScreenshotManager.captureImage(
|
||||||
|
contentFilter: filter,
|
||||||
|
configuration: configuration
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create content filter for the entire display
|
|
||||||
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
|
||||||
|
|
||||||
// Configure capture settings
|
|
||||||
let configuration = SCStreamConfiguration()
|
|
||||||
configuration.width = scDisplay.width
|
|
||||||
configuration.height = scDisplay.height
|
|
||||||
configuration.backgroundColor = .black
|
|
||||||
configuration.shouldBeOpaque = true
|
|
||||||
configuration.showsCursor = true
|
|
||||||
|
|
||||||
// Capture the image
|
|
||||||
let image = try await SCScreenshotManager.captureImage(
|
|
||||||
contentFilter: filter,
|
|
||||||
configuration: configuration
|
|
||||||
)
|
|
||||||
|
|
||||||
try saveImage(image, to: path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func captureWindow(_ window: WindowData, to path: String) throws(CaptureError) {
|
private func captureWindow(_ window: WindowData, to path: String) throws(CaptureError) {
|
||||||
|
|
@ -375,38 +392,55 @@ 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 {
|
||||||
// Get available content
|
do {
|
||||||
let availableContent = try await SCShareableContent.current
|
// Get available content
|
||||||
|
let availableContent = try await SCShareableContent.current
|
||||||
|
|
||||||
// Find the window by ID
|
// Find the window by ID
|
||||||
guard let scWindow = availableContent.windows.first(where: { $0.windowID == window.windowId }) else {
|
guard let scWindow = availableContent.windows.first(where: { $0.windowID == window.windowId }) else {
|
||||||
throw CaptureError.windowNotFound
|
throw CaptureError.windowNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create content filter for the specific window
|
||||||
|
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||||
|
|
||||||
|
// Configure capture settings
|
||||||
|
let configuration = SCStreamConfiguration()
|
||||||
|
configuration.width = Int(window.bounds.width)
|
||||||
|
configuration.height = Int(window.bounds.height)
|
||||||
|
configuration.backgroundColor = .clear
|
||||||
|
configuration.shouldBeOpaque = true
|
||||||
|
configuration.showsCursor = false
|
||||||
|
|
||||||
|
// Capture the image
|
||||||
|
let image = try await SCScreenshotManager.captureImage(
|
||||||
|
contentFilter: filter,
|
||||||
|
configuration: configuration
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create content filter for the specific window
|
|
||||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
|
||||||
|
|
||||||
// Configure capture settings
|
|
||||||
let configuration = SCStreamConfiguration()
|
|
||||||
configuration.width = Int(window.bounds.width)
|
|
||||||
configuration.height = Int(window.bounds.height)
|
|
||||||
configuration.backgroundColor = .clear
|
|
||||||
configuration.shouldBeOpaque = true
|
|
||||||
configuration.showsCursor = false
|
|
||||||
|
|
||||||
// Capture the image
|
|
||||||
let image = try await SCScreenshotManager.captureImage(
|
|
||||||
contentFilter: filter,
|
|
||||||
configuration: configuration
|
|
||||||
)
|
|
||||||
|
|
||||||
try saveImage(image, to: path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveImage(_ image: CGImage, to path: String) throws(CaptureError) {
|
private func saveImage(_ image: CGImage, to path: String) throws(CaptureError) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue